diff --git a/electron.html b/electron.html index 489364b..bddbda9 100644 --- a/electron.html +++ b/electron.html @@ -543,13 +543,22 @@ function getPermssions(e=null){ }); listed=true; audioOutputSelect.focus(); - - }).catch(function(){ - document.getElementById("messageDiv").innerHTML = "Failed to list available output devices\n\nPlease ensure you allowed the microphone permissions."; + + }).catch(function(err){ + var errorMessage = "Failed to list available audio devices\n\n"; + if (err && err.name === "NotFoundError") { + errorMessage += "No microphone detected. Please connect a microphone and refresh."; + } else if (err && err.name === "NotAllowedError") { + errorMessage += "Microphone permission denied. Please allow microphone access."; + } else if (err && err.name === "NotReadableError") { + errorMessage += "Microphone is in use by another application."; + } else { + errorMessage += "Please ensure you have a microphone connected and allowed permissions."; + } + document.getElementById("messageDiv").innerHTML = errorMessage; document.getElementById("messageDiv").style.display="block"; setTimeout(function(){document.getElementById("messageDiv").style.opacity="1.0";},0); - - }); + }); } function gotDevices(deviceInfos) { @@ -637,6 +646,77 @@ function createSpecialDeviceLink(deviceLabel) { }; } +function checkForAsioDevices() { + if (navigator.userAgent.toLowerCase().indexOf(' electron/') > -1) { + try { + // Try async first (works in sandbox mode) + if (window.electronApi && window.electronApi.isAsioAvailableAsync) { + window.electronApi.isAsioAvailableAsync().then(function(available) { + if (!available) return; + window.electronApi.getAsioDevicesAsync().then(function(asioDevices) { + if (asioDevices && asioDevices.length > 0) { + createAsioNotice(asioDevices); + } + }).catch(function(e) { console.warn("ASIO devices check failed:", e); }); + }).catch(function(e) { console.warn("ASIO availability check failed:", e); }); + } + // Fallback to sync (works when --node flag used) + else if (window.electronApi && window.electronApi.isAsioAvailable && window.electronApi.isAsioAvailable()) { + var asioDevices = window.electronApi.getAsioDevices(); + if (asioDevices && asioDevices.length > 0) { + createAsioNotice(asioDevices); + } + } + } catch (e) { + console.warn("ASIO detection check failed:", e); + } + } +} + +function createAsioNotice(asioDevices) { + var noticeText = asioDevices.length === 1 + ? "ASIO: " + asioDevices[0].name + : "ASIO: " + asioDevices.length + " devices"; + + var notice = document.createElement('div'); + notice.id = 'asioNotice'; + notice.style.position = 'fixed'; + notice.style.bottom = '45px'; + notice.style.left = '50%'; + notice.style.transform = 'translateX(-50%)'; + notice.style.backgroundColor = 'rgba(0, 40, 0, 0.8)'; + notice.style.color = '#6f6'; + notice.style.padding = '8px 12px'; + notice.style.borderRadius = '20px'; + notice.style.fontSize = '14px'; + notice.style.textDecoration = 'none'; + notice.style.opacity = '0'; + notice.style.transition = 'opacity 2s ease-in-out'; + notice.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; + notice.style.fontFamily = 'Arial, sans-serif'; + notice.title = "Professional low-latency audio input available via ASIO"; + + var textSpan = document.createElement('span'); + textSpan.textContent = noticeText; + + var linkSpan = document.createElement('span'); + linkSpan.textContent = ' - '; + + var link = document.createElement('a'); + link.href = 'https://github.com/steveseguin/electroncapture#asio-audio-capture-windows-only'; + link.target = '_blank'; + link.textContent = 'Low-latency pro audio available'; + link.style.color = '#0ff'; + + notice.appendChild(textSpan); + notice.appendChild(linkSpan); + notice.appendChild(link); + + document.body.appendChild(notice); + setTimeout(() => notice.style.opacity = '1', 100); + + console.log("ASIO devices available:", asioDevices.map(function(d) { return d.name; })); +} function normalizeDeviceLabel(deviceName) { return String(deviceName).replace(/[\W]+/g, "_").toLowerCase(); } @@ -654,9 +734,19 @@ function getPermssions(e=null){ }); listed=true; audioOutputSelect.focus(); - - }).catch(function(){ - document.getElementById("messageDiv").innerHTML = "Failed to list available audio devices\n\nPlease ensure you allowed the microphone permissions."; + + }).catch(function(err){ + var errorMessage = "Failed to list available audio devices\n\n"; + if (err && err.name === "NotFoundError") { + errorMessage += "No microphone detected. Please connect a microphone and refresh."; + } else if (err && err.name === "NotAllowedError") { + errorMessage += "Microphone permission denied. Please allow microphone access."; + } else if (err && err.name === "NotReadableError") { + errorMessage += "Microphone is in use by another application."; + } else { + errorMessage += "Please ensure you have a microphone connected and allowed permissions."; + } + document.getElementById("messageDiv").innerHTML = errorMessage; document.getElementById("messageDiv").style.display="block"; setTimeout(function(){document.getElementById("messageDiv").style.opacity="1.0";},0); }); @@ -687,6 +777,7 @@ if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(naviga document.addEventListener('DOMContentLoaded', () => { getPermssions(); checkForSpecialVideoDevices(); + checkForAsioDevices(); setTimeout(lazyPreloadCSS, 2000); }); } @@ -814,4 +905,4 @@ if (lastUrls != undefined) { getPermssions(); - \ No newline at end of file + diff --git a/index.html b/index.html index 2cc8074..6663c92 100644 --- a/index.html +++ b/index.html @@ -1840,6 +1840,10 @@ Record Remote + - -

- - -
`; - } else if (recording) { - modalTemplate = ` -
`; - } else if (field && field.type === "file") { - modalTemplate = ` -
`; - } else if (hotkey) { - modalTemplate = ` -
`; - } else if (field && field.type === "select") { - modalTemplate = ` -
`; - } else if (field && field.placeholder) { - modalTemplate = ` -
`; - } else { - modalTemplate = ` -
`; - } - - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - - // Only add select listener if we have a select field - if (field && field.type === "select") { - document.getElementById(`select_${promptID}`).addEventListener("change", (e) => { - const input = document.getElementById(`input_${promptID}`); - if (e.target.value === "[Custom]") { - input.style.display = "block"; - input.focus(); - } else { - input.style.display = "none"; - result = e.target.value; - } - }); - } - - document.getElementById("input_" + promptID).focus(); - - if (value !== false) { - if (time) { - document.getElementById("input_" + promptID).value = parseInt(value / 60); - document.getElementById("input_" + promptID + "_sec").value = parseInt(value) % 60; - } else { - document.getElementById("input_" + promptID).value = value; - } - } - - if (time) { - document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { - if (event.key === "Enter") { - document.getElementById("input_" + promptID + "_sec").focus(); - } - }); - document.getElementById("input_" + promptID + "_sec").addEventListener("keyup", function (event) { - if (event.key === "Enter") { - document.getElementById("submit_" + promptID).focus(); - } - }); - document.getElementById("countup_" + promptID).addEventListener("click", function (event) { - if (document.getElementById("countup_" + promptID).checked) { - document.getElementById("input_" + promptID).disabled = true; - document.getElementById("input_" + promptID + "_sec").disabled = true; - } else { - document.getElementById("input_" + promptID).disabled = false; - document.getElementById("input_" + promptID + "_sec").disabled = false; - delete document.getElementById("input_" + promptID).disabled; - delete document.getElementById("input_" + promptID + "_sec").disabled; - } - }); - } else if (field && field.type === "file") { - document.getElementById(`input_${promptID}`).addEventListener("change", async (e) => { - const file = e.target.files[0]; - if (file) { - result = file; - if (file.type.startsWith('image/')) { - const preview = document.getElementById(`preview_${promptID}`); - const reader = new FileReader(); - reader.onload = (e) => { - preview.innerHTML = ``; - }; - reader.readAsDataURL(file); - } - } - }); - } else { - document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { - if (event.key === "Enter") { - var pid = event.target.dataset.pid; - if (field && field.type === "select") { - const selectEl = document.getElementById(`select_${pid}`); - if (selectEl.value === "[Custom]") { - result = document.getElementById(`input_${pid}`).value; - } else { - result = selectEl.value; - } - } else { - result = document.getElementById("input_" + pid).value; - } - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - } - }); - } - - try { - document.getElementById("submit_" + promptID).addEventListener("click", async function (event) { - var pid = event.target.dataset.pid; - if (time) { - result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60; - if (document.getElementById("countup_" + promptID).checked) { - result = 0; - } - } else if (field && field.type === "select") { - const selectEl = document.getElementById(`select_${pid}`); - if (selectEl.value === "[Custom]") { - result = document.getElementById(`input_${pid}`).value; - } else { - result = selectEl.value; - } - } else if (field && field.type === "file") { - if (result) { - await handleImageUpload(result, field); - } - } else { - result = document.getElementById("input_" + pid).value; - } - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - } catch (e) { } - - try { - document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - } catch (e) { } - - try { - document.getElementById("close_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - } catch (e) { } - - getById("modal_" + promptID).addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - miniTranslate(getById("modal_" + promptID)); - return; - }); - return result; -} - -async function promptRecordingOptions(inputText, block = false, defaultOptions = {}) { - var result = null; - // console.log(defaultOptions); - var defaultVideoBitrate = 6000; - var defaultAudioBitrate = 80; - - if (defaultOptions.audioOnly && !defaultOptions.usePCM) { - defaultAudioBitrate = defaultOptions.bitrate || 80; - } else if (!defaultOptions.audioOnly) { - defaultVideoBitrate = defaultOptions.bitrate || 6000; - } - - if (session.beepToNotify) { - playtone(); - } - - // Helper functions for bitrate controls - function updateBitrateControls(promptID) { - const audioOnly = document.getElementById(`audioOnly_${promptID}`).checked; - const usePCM = document.getElementById(`usePCM_${promptID}`).checked; - const pcmContainer = document.getElementById(`audioFormatContainer_${promptID}`); - const slider = document.getElementById(`bitrateSlider_${promptID}`); - const value = document.getElementById(`bitrateValue_${promptID}`); - const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`); - - // PCM option always available - pcmContainer.style.opacity = '1'; - pcmContainer.style.pointerEvents = 'auto'; - bitrateLabel.parentNode.style.opacity = "1"; - bitrateLabel.parentNode.style.pointerEvents = 'auto'; - //document.getElementById(`usePCM_${promptID}`).disabled = false; - - // Update bitrate controls based on mode - if (audioOnly && usePCM) { - // Audio-only PCM mode - bitrateLabel.textContent = "Uncompressed PCM16 audio"; - bitrateLabel.parentNode.style.opacity = "0.5"; - bitrateLabel.parentNode.style.pointerEvents = 'none'; - slider.disabled = true; - value.disabled = true; - slider.value = ""; - value.value = ""; - } else if (audioOnly && !usePCM) { - // Audio-only OPUS mode - bitrateLabel.textContent = "Audio recording bitrate (OPUS):"; - slider.disabled = false; - value.disabled = false; - slider.min = "1"; - slider.max = "128"; - slider.value = defaultAudioBitrate; - slider.step = "1"; - value.value = defaultAudioBitrate; - } else if (!audioOnly && usePCM) { - // Video + PCM audio mode - bitrateLabel.textContent = "Video bitrate (with PCM16 audio):"; - slider.disabled = false; - value.disabled = false; - slider.min = "50"; - slider.max = "10000"; - slider.value = defaultVideoBitrate; - slider.step = "100"; - value.value = defaultVideoBitrate; - } else { - // Video + OPUS audio mode - bitrateLabel.textContent = "Recording Bitrate:"; - slider.disabled = false; - value.disabled = false; - slider.min = "50"; - slider.max = "10000"; - slider.value = defaultVideoBitrate; - slider.step = "100"; - value.value = defaultVideoBitrate; - } - - updateBitrateQuality(promptID); - } - - function updateBitrateValue(promptID) { - const value = document.getElementById(`bitrateSlider_${promptID}`).value; - document.getElementById(`bitrateValue_${promptID}`).value = value; - updateBitrateQuality(promptID); - } - - function updateBitrateSlider(promptID) { - const value = document.getElementById(`bitrateValue_${promptID}`).value; - document.getElementById(`bitrateSlider_${promptID}`).value = value; - updateBitrateQuality(promptID); - } - - function updateBitrateQuality(promptID) { - const audioOnly = document.getElementById(`audioOnly_${promptID}`).checked; - const usePCM = document.getElementById(`usePCM_${promptID}`).checked; - const value = parseInt(document.getElementById(`bitrateValue_${promptID}`).value); - const qualitySpan = document.getElementById(`bitrateQuality_${promptID}`); - - if (audioOnly && usePCM) { - qualitySpan.textContent = "N/A"; - return; - } - - if (audioOnly) { - if (value < 32) qualitySpan.textContent = "Low"; - else if (value >= 80) qualitySpan.textContent = "High"; - else qualitySpan.textContent = "Medium"; - defaultAudioBitrate = value; - } else { - if (value < 2000) qualitySpan.textContent = "Low"; - else if (value >= 6000) qualitySpan.textContent = "High"; - else qualitySpan.textContent = "Medium"; - defaultVideoBitrate = value; - } - } - - await new Promise((resolve, reject) => { - var promptID = "pid_" + Math.random().toString(36).substr(2, 9); - Prompts[promptID] = {}; - Prompts[promptID].resolve = resolve; - Prompts[promptID].reject = reject; - - var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; - var backdropClass = block ? "opaqueBackdrop" : "modalBackdrop"; - - inputText = "

Recording setup


" + inputText.replace(/\n/g, "
"); - - const modalTemplate = `start record -
`; - - document.body.insertAdjacentHTML("beforeend", modalTemplate); - - // Set default values if provided - if (defaultOptions.audioOnly) { - document.getElementById(`audioOnly_${promptID}`).checked = true; - updateBitrateControls(promptID); - } - if (defaultOptions.usePCM) { - document.getElementById(`usePCM_${promptID}`).checked = true; - if (defaultOptions.audioOnly) { - const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`); - bitrateLabel.textContent = "Uncompressed PCM16 audio"; - bitrateLabel.parentNode.style.opacity = "0.5"; - bitrateLabel.parentNode.style.pointerEvents = 'none'; - } else { - const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`); - bitrateLabel.textContent = "Video bitrate (with PCM16 audio):"; - } - - } - - // Add event listeners - document.getElementById(`audioOnly_${promptID}`).addEventListener("change", () => updateBitrateControls(promptID)); - document.getElementById(`usePCM_${promptID}`).addEventListener("change", () => updateBitrateControls(promptID)); - document.getElementById(`bitrateSlider_${promptID}`).addEventListener("input", () => updateBitrateValue(promptID)); - document.getElementById(`bitrateValue_${promptID}`).addEventListener("change", () => updateBitrateSlider(promptID)); - - // Submit handler - document.getElementById(`submit_${promptID}`).addEventListener("click", function (event) { - const pid = event.target.dataset.pid; - result = { - audioOnly: document.getElementById(`audioOnly_${pid}`).checked, - usePCM: document.getElementById(`usePCM_${pid}`).checked, - bitrate: parseInt(document.getElementById(`bitrateValue_${pid}`).value) - }; - document.getElementById(`modal_${pid}`).remove(); - document.getElementById(`modalBackdrop_${pid}`).remove(); - Prompts[pid].resolve(); - }); - - // Cancel handler - document.getElementById(`cancel_${promptID}`).addEventListener("click", function (event) { - const pid = event.target.dataset.pid; - result = null; - document.getElementById(`modal_${pid}`).remove(); - document.getElementById(`modalBackdrop_${pid}`).remove(); - Prompts[pid].resolve(); - }); - - // Close handler - document.getElementById(`close_${promptID}`).addEventListener("click", function (event) { - const pid = event.target.dataset.pid; - result = null; - document.getElementById(`modal_${pid}`).remove(); - document.getElementById(`modalBackdrop_${pid}`).remove(); - Prompts[pid].resolve(); - }); - - // Stop propagation on modal click - document.getElementById(`modal_${promptID}`).addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - - // Translate if needed - miniTranslate(document.getElementById(`modal_${promptID}`)); - }); - - return result; -} - -async function promptAltRecord(inputText, block = false, asterix = false, value = false) { - var result = null; - if (session.beepToNotify) { - playtone(); - } - await new Promise((resolve, reject) => { - var promptID = "pid_" + Math.random().toString(36).substr(2, 9); - Prompts[promptID] = {}; - Prompts[promptID].resolve = resolve; - Prompts[promptID].reject = reject; - - var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; - - if (block) { - var backdropClass = "opaqueBackdrop"; - } else { - var backdropClass = "modalBackdrop"; - } - - inputText = "" + inputText.replace("\n", "
") + ""; - inputText = inputText.replace(/\n/g, "
"); - var type = "text"; - if (asterix) { - type = "password"; - } - - if (time) { - modalTemplate = ` -
`; - } else if (recording) { - modalTemplate = ` -
`; - } else if (hotkey) { - modalTemplate = ` -
`; - } else { - modalTemplate = ` -
`; - } - - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - - document.getElementById("input_" + promptID).focus(); - - if (value !== false) { - if (time) { - document.getElementById("input_" + promptID).value = parseInt(value / 60); - document.getElementById("input_" + promptID + "_sec").value = parseInt(value) % 60; - } else { - document.getElementById("input_" + promptID).value = value; - } - } - - if (time) { - document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { - if (event.key === "Enter") { - document.getElementById("input_" + promptID + "_sec").focus(); - } - }); - document.getElementById("input_" + promptID + "_sec").addEventListener("keyup", function (event) { - if (event.key === "Enter") { - document.getElementById("submit_" + promptID).focus(); - } - }); - document.getElementById("countup_" + promptID).addEventListener("click", function (event) { - if (document.getElementById("countup_" + promptID).checked) { - document.getElementById("input_" + promptID).disabled = true; - document.getElementById("input_" + promptID + "_sec").disabled = true; - } else { - document.getElementById("input_" + promptID).disabled = false; - document.getElementById("input_" + promptID + "_sec").disabled = false; - delete document.getElementById("input_" + promptID).disabled; - delete document.getElementById("input_" + promptID + "_sec").disabled; - } - }); - } else { - document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { - if (event.key === "Enter") { - var pid = event.target.dataset.pid; - result = document.getElementById("input_" + pid).value; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - } - }); - } - - try { - document.getElementById("submit_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - if (time) { - result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60; - - if (document.getElementById("countup_" + promptID).checked) { - result = 0; - } - } else { - result = document.getElementById("input_" + pid).value; - } - - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - - Prompts[pid].resolve(); - }); - } catch (e) { } - - try { - document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - } catch (e) { } - - try { - document.getElementById("close_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - } catch (e) { } - - getById("modal_" + promptID).addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - miniTranslate(getById("modal_" + promptID)); - return; - }); - return result; -} - -async function promptRecord() { - var result = null; - if (session.beepToNotify) { - playtone(); - } - await new Promise((resolve, reject) => { - var promptID = "pid_" + Math.random().toString(36).substr(2, 9); - Prompts[promptID] = {}; - Prompts[promptID].resolve = resolve; - Prompts[promptID].reject = reject; - - var zindex = 1030 + document.querySelectorAll(".promptModal").length; - - var backdropClass = "modalBackdrop"; - - var inputText = "RECORD SETTINGS"; - - inputText = "" + inputText.replace("\n", "
") + ""; - inputText = inputText.replace(/\n/g, "
"); - var type = "text"; - - modalTemplate = ` -
`; - - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - - var gdrive = getById(`input_${promptID}_gdrive`); - gdrive.onchange = async function () { - if (this.checked) { - this.uploadLink = await session.gdrive.startResumableUpload(); - } else { - this.uploadLink = null; - } - }; - - document.getElementById("input_" + promptID).focus(); - - document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { - if (event.key === "Enter") { - var pid = event.target.dataset.pid; - result = document.getElementById("input_" + pid).value; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - } - }); - - try { - document.getElementById("submit_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - if (time) { - result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60; - - if (document.getElementById("countup_" + promptID).checked) { - result = 0; - } - } else { - result = document.getElementById("input_" + pid).value; - } - - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - - Prompts[pid].resolve(); - }); - } catch (e) { } - - try { - document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - } catch (e) { } - - try { - document.getElementById("close_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - } catch (e) { } - - getById("modal_" + promptID).addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - miniTranslate(getById("modal_" + promptID)); - return; - }); - return result; -} - -async function promptTransfer(value = null, bcmode = null, updateurl = null, queueMode = null) { - var result = { roomid: null }; - if (session.beepToNotify) { - playtone(); - } - await new Promise((resolve, reject) => { - var promptID = "pid_" + Math.random().toString(36).substr(2, 9); - Prompts[promptID] = {}; - Prompts[promptID].resolve = resolve; - Prompts[promptID].reject = reject; - - var zindex = 30 + document.querySelectorAll(".promptModal").length; - var backdropClass = "modalBackdrop"; - - var inputText = "" + getTranslation("transfer-guest-to-room").replace("\n", "
") + ""; - inputText = inputText.replace(/\n/g, "
"); - - modalTemplate = ` -
`; - - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - - document.getElementById("input_" + promptID).focus(); - - if (value !== null) { - document.getElementById("input_" + promptID).value = value; - } - - if (bcmode !== null) { - document.getElementById("broadcast_" + promptID).checked = bcmode; - } - if (queueMode !== null) { - document.getElementById("queued_" + promptID).checked = queueMode; - } - - if (updateurl !== null) { - document.getElementById("private_" + promptID).checked = updateurl; - } - - document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { - if (event.key === "Enter") { - var pid = event.target.dataset.pid; - var room = document.getElementById("input_" + pid).value; - var updateurl = document.getElementById("private_" + pid).checked; - var broadcast = document.getElementById("broadcast_" + pid).checked; - var queue = document.getElementById("queued_" + pid).checked; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - result = { roomid: room, updateurl: updateurl, broadcast: broadcast, queue: queue }; - } - }); - - document.getElementById("submit_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - var room = document.getElementById("input_" + pid).value; - var updateurl = document.getElementById("private_" + pid).checked; - var broadcast = document.getElementById("broadcast_" + pid).checked; - var queue = document.getElementById("queued_" + pid).checked; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - result = { roomid: room, updateurl: updateurl, broadcast: broadcast, queue: queue }; - }); - - document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - - document.getElementById("close_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - document.getElementById("modal_" + pid).remove(); - document.getElementById("modalBackdrop_" + pid).remove(); - Prompts[pid].resolve(); - }); - - getById("modal_" + promptID).addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - miniTranslate(getById("modal_" + promptID)); - return; - }); - return result; -} - -function youveBeenTransferred() { - getChatMessage(getTranslation("you-have-been-transferred"), (label = false), (director = false), (overlay = true)); // "you-have-been-transferred" - getById("head2").innerHTML = getTranslation("room-changed"); // not sure this is right?? - - miniTranslate(getById("head2"), "room-changed"); - - if (session.director) { - getById("head4").innerHTML = getTranslation("you-are-no-longer-a-co-director"); //"You are no longer a co-director as you were transferred."; // - } - - if (session.label) { - document.title = session.label + " - " + getTranslation("transferred"); - } else { - document.title = getTranslation("transferred"); - } - - hideHomeCheck(); -} - -function youveBeenActivated() { - if (session.queueType == 3 || session.queueType == 4) { - // For both hold modes, publish to any deferred peers upon activation. - // queueType 3: All peers were deferred (needsPublishing=true) - // queueType 4: Only non-director peers were deferred; directors already receiving - closeModal(false, "123"); - for (var UUID in session.pcs) { - if (session.pcs[UUID].needsPublishing) { - session.initialPublish(UUID); - } - } - } - - getChatMessage(getTranslation("you-have-been-activated"), (label = false), (director = false), (overlay = true)); - hideHomeCheck(); -} - -function youreWaitingToBeActivated() { - // getChatMessage( getTranslation("you-not-yet-activated"), label = false, director = false, overlay = true); - warnUser(getTranslation("you-not-yet-activated"), false, false, 123); - hideHomeCheck(); -} - -async function confirmAlt(inputText, block = false, context = null) { - var result = null; - if (session.beepToNotify) { - playtone(); - } - await new Promise((resolve, reject) => { - var promptID = "pid_" + Math.random().toString(36).substr(2, 9); - Prompts[promptID] = {}; - Prompts[promptID].resolve = resolve; - Prompts[promptID].reject = reject; - Prompts[promptID].context = context; - - var zindex = 33 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; - - if (block) { - var backdropClass = "opaqueBackdrop"; - } else { - var backdropClass = "modalBackdrop"; - } - - inputText = "" + inputText.replace("\n", "
") + ""; - inputText = inputText.replace(/\n/g, "
"); - - modalTemplate = ` -
]/g, "") : ''}" style="z-index:${zindex + 1}">
`; - - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - - document.getElementById("submit_" + promptID).focus(); - - document.getElementById("submit_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - result = true; - getById("modalBackdrop_" + pid).remove(); - getById("modal_" + pid).remove(); - Prompts[pid].resolve(); - }); - - document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - getById("modalBackdrop_" + pid).remove(); - getById("modal_" + pid).remove(); - Prompts[pid].resolve(); - }); - - document.getElementById("close_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - getById("modalBackdrop_" + pid).remove(); - getById("modal_" + pid).remove(); - Prompts[pid].resolve(); - }); - - getById("modal_" + promptID).addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - miniTranslate(getById("modal_" + promptID)); - return; - }); - return result; -} - -async function confirmHangupWithBlock(inputText) { - // Similar to confirmAlt but includes a "block from rejoining" checkbox - // Returns: { confirmed: boolean, block: boolean } - var result = { confirmed: false, block: false }; - if (session.beepToNotify) { - playtone(); - } - await new Promise((resolve, reject) => { - var promptID = "pid_" + Math.random().toString(36).substr(2, 9); - Prompts[promptID] = {}; - Prompts[promptID].resolve = resolve; - Prompts[promptID].reject = reject; - - var zindex = 33 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; - var backdropClass = "modalBackdrop"; - - inputText = "" + inputText.replace("\n", "
") + ""; - inputText = inputText.replace(/\n/g, "
"); - - modalTemplate = ` -
`; - - document.body.insertAdjacentHTML("beforeend", modalTemplate); - - document.getElementById("submit_" + promptID).focus(); - - document.getElementById("submit_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - result.confirmed = true; - result.block = document.getElementById("blockUser_" + pid).checked; - getById("modalBackdrop_" + pid).remove(); - getById("modal_" + pid).remove(); - Prompts[pid].resolve(); - }); - - document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - getById("modalBackdrop_" + pid).remove(); - getById("modal_" + pid).remove(); - Prompts[pid].resolve(); - }); - - document.getElementById("close_" + promptID).addEventListener("click", function (event) { - var pid = event.target.dataset.pid; - getById("modalBackdrop_" + pid).remove(); - getById("modal_" + pid).remove(); - Prompts[pid].resolve(); - }); - - getById("modal_" + promptID).addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - miniTranslate(getById("modal_" + promptID)); - return; - }); - return result; -} - -var modalTimeout = null; -function warnUser(message, timeout = false, sanitize = true, modalID = false) { - // Allows for multiple alerts to stack better. - // Every modal and backdrop has an increasing z-index - // to block the previous modal - if (!message) { - return; - } - - if (document.getElementById("modalBackdrop")) { - getById("alertModal").innerHTML = ""; // Delete modal - getById("alertModal").remove(); - getById("modalBackdrop").innerHTML = ""; // Delete modal - getById("modalBackdrop").remove(); - } - - zindex = 31 + document.querySelectorAll(".alertModal").length + document.querySelectorAll(".promptModal").length; - try { - if (sanitize) { - message = sanitizeChat(message, 2000); - } - message = message.replace(/\n/g, "
"); - } catch (e) { - errorlog(message); - } - - if (!modalID) { - modalID = Math.floor(Math.random() * 999) + 1000; - } - - modalTemplate = `
-
- × - ${message} -
-
-
`; - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - - document.getElementById("modalBackdrop").addEventListener("click", closeModal); - - clearTimeout(modalTimeout); - if (timeout) { - modalTimeout = setTimeout(closeModal, timeout, false, modalID); - } - getById("alertModal").addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - return modalID; -} -function closeModal(ele = false, modalID = false) { - if (modalID && !ele) { - // Check for alertModal with data-modalID (warnUser modals) - var alertModalMatch = document.querySelector("#alertModal[data-modalID='" + modalID + "']"); - // Check for promptModal with data-context (confirmAlt modals like approval popups) - var ctx = ("" + modalID).replace(/["<>]/g, ""); - var promptModalMatch = document.querySelector('.promptModal[data-context="' + ctx + '"]'); - - if (promptModalMatch) { - // Close the confirmAlt modal by context (programmatic dismissal) - try { - var modalIdAttr = promptModalMatch.id; // e.g., "modal_pid_abc123" - if (modalIdAttr && modalIdAttr.startsWith("modal_")) { - var pid = modalIdAttr.substring(6); // Extract "pid_abc123" - // Remove modal and backdrop - promptModalMatch.remove(); - var backdrop = document.getElementById("modalBackdrop_" + pid); - if (backdrop) { backdrop.remove(); } - // Do NOT resolve the promise - let it stay pending - // Resolving would trigger the denial dialog in promptApproval - if (typeof Prompts !== "undefined" && Prompts[pid]) { - delete Prompts[pid]; // Clean up reference, but don't resolve - } - } else { - promptModalMatch.remove(); - } - } catch (e) { warnlog(e); } - return; - } - - if (!alertModalMatch) { - return; - } - } - - clearTimeout(modalTimeout); - try { - getById("modalBackdrop").innerHTML = ""; // Delete modal - getById("modalBackdrop").remove(); - getById("alertModal").innerHTML = ""; // Delete modal - getById("alertModal").remove(); - getById("promptModal").innerHTML = ""; // Delete modal - getById("promptModal").remove(); - - query(".modalBackdrop").innerHTML = ""; // Delete modal - query(".modalBackdrop").remove(); - - if (ele && ele.innerHTML && ele.remove) { - ele.innerHTML = ""; // Delete specific modal - ele.remove(); - } - } catch (e) { - warnlog(e); - } - if (session.timeoutTriggered) { - session.warnUserTriggered = false; - } -} - -var sanitizeStreamID = function (streamID) { - streamID = streamID.trim(); - - if (streamID.length < 1) { - streamID = session.generateStreamID(8); - if (!session.cleanOutput) { - warnUser(getTranslation("no-streamID-provided") + streamID, false, false); - } - } - var streamID_sanitized = streamID.replace(/[\W]+/g, "_"); - if (streamID !== streamID_sanitized) { - if (!session.cleanOutput) { - warnUser(getTranslation("alphanumeric-only"), false, false); - } - } - if (streamID_sanitized.length > 64) { - streamID_sanitized = streamID_sanitized.substring(0, 70); // leave room for salting - if (!session.cleanOutput) { - warnUser(getTranslation("stream-id-too-long"), false, false); - } - } - return streamID_sanitized; -}; - -var checkStrength = function (string) { - var matcher = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{7,30}$/; - if (string.match(matcher)) { - return true; - } else if (string.length > 20) { - return true; - } else { - return false; - } -}; - -var checkStrengthRoom = function () { - var result1 = checkStrength(getById("videoname1").value); - var result2 = getById("passwordRoom").value.length; - var target = getById("securityLevelRoom"); - target.style.display = "block"; - if (result1) { - if (result2) { - target.innerHTML = "" + getTranslation("share-with-trusted") + ""; - } else { - target.innerHTML = "" + getTranslation("pass-recommended") + ""; - } - } else { - target.innerHTML = "" + getTranslation("insecure-room-name") + " " + getTranslation("allowed-chars") + ": A-Z, a-z, 0-9, _"; - } -}; - -var emojiShortCodes = { - ":joy:": "😂", - ":heart:": "❤️", - ":heart_eyes:": "😍", - ":sob:": "😭", - ":blush:": "😊", - ":unamused:": "😒", - ":two_hearts:": "💕", - ":weary:": "😩", - ":ok_hand:": "👌", - ":pensive:": "😔", - ":smirk:": "😏", - ":grin:": "😁", - ":wink:": "😉", - ":thumbsup:": "👍", - ":pray:": "🙏", - ":relieved:": "😌", - ":notes:": "🎶", - ":flushed:": "😳", - ":raised_hands:": "🙌", - ":see_no_evil:": "🙈", - ":cry:": "😢", - ":sunglasses:": "😎", - ":v:": "✌️", - ":eyes:": "👀", - ":sweat_smile:": "😅", - ":sparkles:": "✨", - ":sleeping:": "😴", - ":smile:": "😄", - ":purple_heart:": "💜", - ":broken_heart:": "💔", - ":blue_heart:": "💙", - ":confused:": "😕", - ":disappointed:": "😞", - ":yum:": "😋", - ":neutral_face:": "😐", - ":sleepy:": "😪", - ":clap:": "👏", - ":cupid:": "💘", - ":heartpulse:": "💗", - ":kiss:": "💋", - ":point_right:": "👉", - ":scream:": "😱", - ":fire:": "🔥", - ":rage:": "😡", - ":smiley:": "😃", - ":tada:": "🎉", - ":tired_face:": "😫", - ":camera:": "📷", - ":rose:": "🌹", - ":muscle:": "💪", - ":skull:": "💀", - ":sunny:": "☀️", - ":yellow_heart:": "💛", - ":triumph:": "😤", - ":laughing:": "😆", - ":sweat:": "😓", - ":point_left:": "👈", - ":grinning:": "😀", - ":mask:": "😷", - ":green_heart:": "💚", - ":wave:": "👋", - ":persevere:": "😣", - ":heartbeat:": "💓", - ":crown:": "👑", - ":innocent:": "😇", - ":headphones:": "🎧", - ":confounded:": "😖", - ":angry:": "😠", - ":grimacing:": "😬", - ":star2:": "🌟", - ":gun:": "🔫", - ":raising_hand:": "🙋", - ":thumbsdown:": "👎", - ":dancer:": "💃", - ":musical_note:": "🎵", - ":no_mouth:": "😶", - ":dizzy:": "💫", - ":fist:": "✊", - ":point_down:": "👇", - ":no_good:": "🙅", - ":boom:": "💥", - ":tongue:": "👅", - ":poop:": "💩", - ":cold_sweat:": "😰", - ":gem:": "💎", - ":ok_woman:": "🙆", - ":pizza:": "🍕", - ":joy_cat:": "😹", - ":leaves:": "🍃", - ":sweat_drops:": "💦", - ":penguin:": "🐧", - ":zzz:": "💤", - ":walking:": "🚶", - ":airplane:": "✈️", - ":balloon:": "🎈", - ":star:": "⭐", - ":ribbon:": "🎀", - ":worried:": "😟", - ":underage:": "🔞", - ":fearful:": "😨", - ":hibiscus:": "🌺", - ":microphone:": "🎤", - ":open_hands:": "👐", - ":ghost:": "👻", - ":palm_tree:": "🌴", - ":nail_care:": "💅", - ":alien:": "👽", - ":bow:": "🙇", - ":cloud:": "☁", - ":soccer:": "⚽", - ":angel:": "👼", - ":dancers:": "👯", - ":snowflake:": "❄️", - ":point_up:": "☝️", - ":rainbow:": "🌈", - ":gift_heart:": "💝", - ":gift:": "🎁", - ":beers:": "🍻", - ":anguished:": "😧", - ":earth_africa:": "🌍", - ":movie_camera:": "🎥", - ":anchor:": "⚓", - ":zap:": "⚡", - ":runner:": "🏃", - ":sunflower:": "🌻", - ":bouquet:": "💐", - ":dog:": "🐶", - ":moneybag:": "💰", - ":herb:": "🌿", - ":couple:": "👫", - ":fallen_leaf:": "🍂", - ":tulip:": "🌷", - ":birthday:": "🎂", - ":cat:": "🐱", - ":coffee:": "☕", - ":dizzy_face:": "😵", - ":point_up_2:": "👆", - ":open_mouth:": "😮", - ":hushed:": "😯", - ":basketball:": "🏀", - ":ring:": "💍", - ":astonished:": "😲", - ":hear_no_evil:": "🙉", - ":dash:": "💨", - ":cactus:": "🌵", - ":hotsprings:": "♨️", - ":telephone:": "☎️", - ":maple_leaf:": "🍁", - ":princess:": "👸", - ":massage:": "💆", - ":love_letter:": "💌", - ":trophy:": "🏆", - ":blossom:": "🌼", - ":lips:": "👄", - ":fries:": "🍟", - ":doughnut:": "🍩", - ":frowning:": "😦", - ":ocean:": "🌊", - ":bomb:": "💣", - ":cyclone:": "🌀", - ":rocket:": "🚀", - ":umbrella:": "☔", - ":couplekiss:": "💏", - ":lollipop:": "🍭", - ":clapper:": "🎬", - ":pig:": "🐷", - ":smiling_imp:": "😈", - ":imp:": "👿", - ":bee:": "🐝", - ":kissing_cat:": "😽", - ":anger:": "💢", - ":santa:": "🎅", - ":earth_asia:": "🌏", - ":football:": "🏈", - ":guitar:": "🎸", - ":panda_face:": "🐼", - ":strawberry:": "🍓", - ":smirk_cat:": "😼", - ":banana:": "🍌", - ":watermelon:": "🍉", - ":snowman:": "⛄", - ":smile_cat:": "😸", - ":eggplant:": "🍆", - ":crystal_ball:": "🔮", - ":calling:": "📲", - ":iphone:": "📱", - ":partly_sunny:": "⛅", - ":warning:": "⚠️", - ":scream_cat:": "🙀", - ":baby:": "👶", - ":feet:": "🐾", - ":footprints:": "👣", - ":beer:": "🍺", - ":wine_glass:": "🍷", - ":video_camera:": "📹", - ":rabbit:": "🐰", - ":smoking:": "🚬", - ":peach:": "🍑", - ":snake:": "🐍", - ":turtle:": "🐢", - ":cherries:": "🍒", - ":kissing:": "😗", - ":frog:": "🐸", - ":milky_way:": "🌌", - ":closed_book:": "📕", - ":candy:": "🍬", - ":hamburger:": "🍔", - ":bear:": "🐻", - ":tiger:": "🐯", - ":icecream:": "🍦", - ":pineapple:": "🍍", - ":ear_of_rice:": "🌾", - ":syringe:": "💉", - ":tv:": "📺", - ":pill:": "💊", - ":octopus:": "🐙", - ":grapes:": "🍇", - ":smiley_cat:": "😺", - ":cd:": "💿", - ":cocktail:": "🍸", - ":cake:": "🍰", - ":video_game:": "🎮", - ":lipstick:": "💄", - ":whale:": "🐳", - ":cookie:": "🍪", - ":dolphin:": "🐬", - ":loud_sound:": "🔊", - ":man:": "👨", - ":monkey:": "🐒", - ":books:": "📚", - ":guardsman:": "💂", - ":loudspeaker:": "📢", - ":scissors:": "✂️", - ":girl:": "👧", - ":mortar_board:": "🎓", - ":baseball:": "⚾️", - ":woman:": "👩", - ":fireworks:": "🎆", - ":stars:": "🌠", - ":mushroom:": "🍄", - ":pouting_cat:": "😾", - ":left_luggage:": "🛅", - ":high_heel:": "👠", - ":dart:": "🎯", - ":swimmer:": "🏊", - ":key:": "🔑", - ":bikini:": "👙", - ":family:": "👪", - ":pencil2:": "✏", - ":elephant:": "🐘", - ":droplet:": "💧", - ":seedling:": "🌱", - ":apple:": "🍎", - ":dollar:": "💵", - ":book:": "📖", - ":haircut:": "💇", - ":computer:": "💻", - ":bulb:": "💡", - ":boy:": "👦", - ":tangerine:": "🍊", - ":sunrise:": "🌅", - ":poultry_leg:": "🍗", - ":shaved_ice:": "🍧", - ":bird:": "🐦", - ":eyeglasses:": "👓", - ":goat:": "🐐", - ":older_woman:": "👵", - ":new_moon:": "🌑", - ":customs:": "🛃", - ":house:": "🏠", - ":full_moon:": "🌕", - ":lemon:": "🍋", - ":baby_bottle:": "🍼", - ":spaghetti:": "🍝", - ":wind_chime:": "🎐", - ":fish_cake:": "🍥", - ":nose:": "👃", - ":pig_nose:": "🐽", - ":fish:": "🐟", - ":koala:": "🐨", - ":ear:": "👂", - ":shower:": "🚿", - ":bug:": "🐛", - ":ramen:": "🍜", - ":tophat:": "🎩", - ":fuelpump:": "⛽", - ":horse:": "🐴", - ":watch:": "⌚", - ":monkey_face:": "🐵", - ":baby_symbol:": "🚼", - ":sparkler:": "🎇", - ":corn:": "🌽", - ":tennis:": "🎾", - ":battery:": "🔋", - ":wolf:": "🐺", - ":moyai:": "🗿", - ":cow:": "🐮", - ":mega:": "📣", - ":older_man:": "👴", - ":dress:": "👗", - ":link:": "🔗", - ":chicken:": "🐔", - ":whale2:": "🐋", - ":bento:": "🍱", - ":pushpin:": "📌", - ":dragon:": "🐉", - ":hamster:": "🐹", - ":golf:": "⛳", - ":surfer:": "🏄", - ":mouse:": "🐭", - ":blue_car:": "🚙", - ":bread:": "🍞", - ":cop:": "👮", - ":tea:": "🍵", - ":bike:": "🚲", - ":rice:": "🍚", - ":radio:": "📻", - ":baby_chick:": "🐤", - ":sheep:": "🐑", - ":lock:": "🔒", - ":green_apple:": "🍏", - ":racehorse:": "🐎", - ":fried_shrimp:": "🍤", - ":volcano:": "🌋", - ":rooster:": "🐓", - ":inbox_tray:": "📥", - ":wedding:": "💒", - ":sushi:": "🍣", - ":ice_cream:": "🍨", - ":tomato:": "🍅", - ":rabbit2:": "🐇", - ":beetle:": "🐞", - ":bath:": "🛀", - ":no_entry:": "⛔", - ":crocodile:": "🐊", - ":dog2:": "🐕", - ":cat2:": "🐈", - ":hammer:": "🔨", - ":meat_on_bone:": "🍖", - ":shell:": "🐚", - ":poodle:": "🐩", - ":stew:": "🍲", - ":jeans:": "👖", - ":honey_pot:": "🍯", - ":unlock:": "🔓", - ":black_nib:": "✒", - ":snowboarder:": "🏂", - ":white_flower:": "💮", - ":necktie:": "👔", - ":womens:": "🚺", - ":ant:": "🐜", - ":city_sunset:": "🌇", - ":dragon_face:": "🐲", - ":snail:": "🐌", - ":dvd:": "📀", - ":shirt:": "👕", - ":game_die:": "🎲", - ":dolls:": "🎎", - ":8ball:": "🎱", - ":bus:": "🚌", - ":custard:": "🍮", - ":camel:": "🐫", - ":curry:": "🍛", - ":hospital:": "🏥", - ":bell:": "🔔", - ":pear:": "🍐", - ":door:": "🚪", - ":saxophone:": "🎷", - ":church:": "⛪", - ":bicyclist:": "🚴", - ":dango:": "🍡", - ":office:": "🏢", - ":rowboat:": "🚣", - ":womans_hat:": "👒", - ":mans_shoe:": "👞", - ":love_hotel:": "🏩", - ":mount_fuji:": "🗻", - ":handbag:": "👜", - ":hourglass:": "⌛", - ":trumpet:": "🎺", - ":school:": "🏫", - ":cow2:": "🐄", - ":toilet:": "🚽", - ":pig2:": "🐖", - ":violin:": "🎻", - ":credit_card:": "💳", - ":ferris_wheel:": "🎡", - ":bowling:": "🎳", - ":barber:": "💈", - ":purse:": "👛", - ":rat:": "🐀", - ":date:": "📅", - ":ram:": "🐏", - ":tokyo_tower:": "🗼", - ":kimono:": "👘", - ":ship:": "🚢", - ":mag_right:": "🔎", - ":mag:": "🔍", - ":fire_engine:": "🚒", - ":police_car:": "🚓", - ":black_joker:": "🃏", - ":package:": "📦", - ":calendar:": "📆", - ":horse_racing:": "🏇", - ":tiger2:": "🐅", - ":boot:": "👢", - ":ambulance:": "🚑", - ":boar:": "🐗", - ":pound:": "💷", - ":ox:": "🐂", - ":rice_ball:": "🍙", - ":sandal:": "👡", - ":tent:": "⛺", - ":seat:": "💺", - ":taxi:": "🚕", - ":briefcase:": "💼", - ":newspaper:": "📰", - ":circus_tent:": "🎪", - ":mens:": "🚹", - ":flashlight:": "🔦", - ":foggy:": "🌁", - ":bamboo:": "🎍", - ":ticket:": "🎫", - ":helicopter:": "🚁", - ":minidisc:": "💽", - ":oncoming_bus:": "🚍", - ":melon:": "🍈", - ":notebook:": "📓", - ":no_bell:": "🔕", - ":oden:": "🍢", - ":flags:": "🎏", - ":blowfish:": "🐡", - ":sweet_potato:": "🍠", - ":ski:": "🎿", - ":construction:": "🚧", - ":satellite:": "📡", - ":euro:": "💶", - ":ledger:": "📒", - ":leopard:": "🐆", - ":truck:": "🚚", - ":sake:": "🍶", - ":railway_car:": "🚃", - ":speedboat:": "🚤", - ":vhs:": "📼", - ":yen:": "💴", - ":mute:": "🔇", - ":wheelchair:": "♿", - ":paperclip:": "📎", - ":atm:": "🏧", - ":telescope:": "🔭", - ":rice_scene:": "🎑", - ":blue_book:": "📘", - ":postbox:": "📮", - ":e-mail:": "📧", - ":mouse2:": "🐁", - ":nut_and_bolt:": "🔩", - ":hotel:": "🏨", - ":wc:": "🚾", - ":green_book:": "📗", - ":tractor:": "🚜", - ":fountain:": "⛲", - ":metro:": "🚇", - ":clipboard:": "📋", - ":no_smoking:": "🚭", - ":slot_machine:": "🎰", - ":bathtub:": "🛁", - ":scroll:": "📜", - ":station:": "🚉", - ":rice_cracker:": "🍘", - ":bank:": "🏦", - ":wrench:": "🔧", - ":bar_chart:": "📊", - ":minibus:": "🚐", - ":tram:": "🚊", - ":microscope:": "🔬", - ":bookmark:": "🔖", - ":pouch:": "👝", - ":fax:": "📠", - ":sound:": "🔉", - ":chart:": "💹", - ":floppy_disk:": "💾", - ":post_office:": "🏣", - ":speaker:": "🔈", - ":japan:": "🗾", - ":mahjong:": "🀄", - ":orange_book:": "📙", - ":restroom:": "🚻", - ":train:": "🚋", - ":trolleybus:": "🚎", - ":postal_horn:": "📯", - ":factory:": "🏭", - ":train2:": "🚆", - ":pager:": "📟", - ":outbox_tray:": "📤", - ":mailbox:": "📫", - ":light_rail:": "🚈", - ":busstop:": "🚏", - ":file_folder:": "📁", - ":card_index:": "📇", - ":monorail:": "🚝", - ":no_bicycles:": "🚳", - ":hugging:": "🤗", - ":thinking:": "🤔", - ":nerd:": "🤓", - ":zipper_mouth:": "🤐", - ":rolling_eyes:": "🙄", - ":upside_down:": "🙃", - ":slight_smile:": "🙂", - ":writing_hand:": "✍", - ":eye:": "👁", - ":man_in_suit:": "🕴", - ":golfer:": "🏌", - ":golfer_woman:": "🏌‍♀", - ":anger_right:": "🗯", - ":coffin:": "⚰", - ":gear:": "⚙", - ":alembic:": "⚗", - ":scales:": "⚖", - ":keyboard:": "⌨", - ":shield:": "🛡", - ":bed:": "🛏", - ":ballot_box:": "🗳", - ":compression:": "🗜", - ":wastebasket:": "🗑", - ":file_cabinet:": "🗄", - ":trackball:": "🖲", - ":printer:": "🖨", - ":joystick:": "🕹", - ":hole:": "🕳", - ":candle:": "🕯", - ":prayer_beads:": "📿", - ":amphora:": "🏺", - ":label:": "🏷", - ":film_frames:": "🎞", - ":level_slider:": "🎚", - ":thermometer:": "🌡", - ":motorway:": "🛣", - ":synagogue:": "🕍", - ":mosque:": "🕌", - ":kaaba:": "🕋", - ":stadium:": "🏟", - ":desert:": "🏜", - ":cityscape:": "🏙", - ":camping:": "🏕", - ":rosette:": "🏵", - ":volleyball:": "🏐", - ":medal:": "🏅", - ":popcorn:": "🍿", - ":champagne:": "🍾", - ":hot_pepper:": "🌶", - ":burrito:": "🌯", - ":taco:": "🌮", - ":hotdog:": "🌭", - ":shamrock:": "☘", - ":comet:": "☄", - ":turkey:": "🦃", - ":scorpion:": "🦂", - ":lion_face:": "🦁", - ":crab:": "🦀", - ":spider_web:": "🕸", - ":spider:": "🕷", - ":chipmunk:": "🐿", - ":fog:": "🌫", - ":chains:": "⛓", - ":pick:": "⛏", - ":stopwatch:": "⏱", - ":ferry:": "⛴", - ":mountain:": "⛰", - ":ice_skate:": "⛸", - ":skier:": "⛷", - ":sad:": "😥", - ":egg:": "🥚", - ":drum:": "🥁" -}; - -function convertShortcodes(string) { - if (string.split(":").length > 2) { - for (var i in emojiShortCodes) { - if (string.includes(i)) { - string = string.replaceAll(i, emojiShortCodes[i]); - } - } - } - return string; -} - -function escapeHtml(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -var sanitizeChat = function (string, maxlength = 500) { - var temp = document.createElement("div"); - temp.innerText = string; - temp.innerText = temp.innerHTML; - temp = temp.textContent || temp.innerText || ""; - temp = temp.substring(0, Math.min(temp.length, maxlength)); - return temp.trim(); -}; - -var sanitizeString = function (str) { - str = str.replace(/[^a-z0-9áéíóúñü \.,_-]/gim, ""); - return str.trim(); -}; - -var sanitizeLabel = function (string) { - let temp = document.createElement("div"); - temp.innerText = string; - temp.innerText = temp.innerHTML; - temp = temp.textContent || temp.innerText || ""; - temp = temp.substring(0, Math.min(temp.length, 100)); - return temp.trim(); -}; - -var sanitizeRoomName = function (roomid) { - roomid = roomid.trim(); - if (roomid === "") { - return roomid; - } else if (roomid === false) { - return roomid; - } - - var sanitized = roomid.replace(/[\W]+/g, "_"); - if (roomid.replace(/ /g, "_") !== sanitized) { - if (!session.cleanOutput) { - warnUser("Info: Only AlphaNumeric characters should be used for the room name.\n\nThe offending characters have been replaced by an underscore"); - } - } - if (sanitized.length > 30) { - sanitized = sanitized.substring(0, 30); - if (!session.cleanOutput) { - warnUser("The Room name should be less than 31 alPhaNuMeric characters long.\n\nWe will trim it to length."); - } - } - return sanitized; -}; - -var sanitizePassword = function (passwrd) { - if (passwrd === "") { - return passwrd; - } else if (passwrd === false) { - return passwrd; - } else if (passwrd === null) { - return passwrd; - } - passwrd = passwrd.trim(); - if (passwrd.length < 1) { - if (!session.cleanOutput) { - warnUser("The password provided was blank."); - } - } - var sanitized = encodeURIComponent(passwrd); //.replace(/[\W]+/g, "_"); - //if (sanitized !== passwrd) { - // if (!(session.cleanOutput)) { - // warnUser("Info: Only AlphaNumeric characters should be used in the password.\n\nThe offending characters have been replaced by an underscore"); - // } - //} - return sanitized; -}; - -function checkConnection() { - if (session.ws === null) { - return; - } - if (!session.cleanOutput) { - if (document.getElementById("qos")) { - // true or false; null might cause problems? - getById("logoname").style.display = "unset"; - if (session.ws && session.ws.readyState === WebSocket.OPEN) { - getById("qos").style.color = "#FFF7"; - } else { - getById("qos").style.color = "red"; - } - } - } -} - -session.obsSceneSync = function () { - if (session.layouts && session.obsSceneTriggers && session.obsState && session.obsState.details && session.obsState.details.currentScene.name && session.obsSceneTriggers.includes(session.obsState.details.currentScene.name)) { - var idx = session.obsSceneTriggers.indexOf(session.obsState.details.currentScene.name); - if (idx >= 0) { - if (session.layouts[idx]) { - var layout = combinedLayout(session.layouts[idx]); - if (layout) { - session.layout = layout; - updateMixer(); - } - } - } - return true; - } - return false; -}; - -session.sceneSync = function (UUID) { - if (!session.rpcs[UUID]) { - return; - } else if (!session.rpcs[UUID].videoElement) { - return; - } // i'll want to consider other things, such as canvas at some point. - - var msg = {}; - msg.sceneDisplay = session.rpcs[UUID].videoElement.style.display != "none"; - msg.sceneMute = session.rpcs[UUID].mutedState; - - if (session.optimize !== false) { - // if not visible in the scene anymore, lets lets optimize. This is outside the scope of OBS - var bandwidth = parseInt(session.rpcs[UUID].targetBandwidth); // wtf is goign on here? - if (msg.sceneDisplay === false) { - if (bandwidth > session.optimize || bandwidth < 0) { - // limit to optimized bitrate - bandwidth = session.optimize; - } - } - if (session.rpcs[UUID].bandwidth !== bandwidth) { - // bandwidth already set correctly. don't resend. - msg.bitrate = bandwidth; - if (session.sendRequest(msg, UUID)) { - session.rpcs[UUID].bandwidth = bandwidth; // this is letting the system know what the actual bandwidth is, even if it isn't the real target. - } else { - errorlog("Unable to set update OBS Visibility"); - } - } else { - session.sendRequest(msg, UUID); - } - } else { - session.sendRequest(msg, UUID); - } -}; - -var TriggerOnNewDetails = false; -session.obsStateSync = function (data2send = false, uid = false) { - if (session.disableOBS) { - return; - } - if (!window.obsstudio) { - return; - } // this isn't OBS - // they can disable remote control via OBS brower source drop-down itself. - - log(data2send); - - if (data2send && data2send == "sourceActive" && session.obsState.sourceActive) { - TriggerOnNewDetails = true; - } else if (data2send && data2send == "details" && session.obsState.sourceActive && TriggerOnNewDetails) { - if (session.obsState.details && session.obsState.details.currentScene && session.obsState.details.currentScene.name) { - session.obsState.details.thisScene = session.obsState.details.currentScene.name; - TriggerOnNewDetails = false; - } - } - - var needOptimize = false; - if (session.obsState.visibility !== null) { - if (session.obsState.visibility === false) { - /////////////////// I need to change tis to .state or whatever, anc catch/handle these events to update the buttons in the pop up menu - needOptimize = true; - } - } - - session.obsSceneSync(); - - for (var UUID in session.rpcs) { - if (uid && uid !== UUID) { - continue; - } // target just a single connection. - - var msg = {}; - if (!data2send) { - msg.obsState = Object.assign({}, session.obsState); // shallow copy to avoid mutating global state - if (session.rpcs[UUID].obsControl === false) { - msg.obsState.details = null; // we don't want to send needless data - } - } else if (data2send in session.obsState) { - if (data2send == "details") { - if (session.rpcs[UUID].obsControl === false) { - continue; // we don't want to send needless data; this isn't a visibility update, so skip. - } - msg.obsState = {}; - msg.obsState[data2send] = session.obsState[data2send]; - } else { - msg.obsState = {}; - msg.obsState[data2send] = session.obsState[data2send]; - } - } - - if (session.filterOBSscenes && msg.obsState && msg.obsState.details && msg.obsState.details.scenes && msg.obsState.details.scenes.length) { - var scenes = []; - msg.obsState.details.scenes.forEach(scene => { - if (session.filterOBSscenes && session.filterOBSscenes.length) { - if (session.filterOBSscenes.includes(scene)) { - scenes.push(scene); - } - } - }); - msg.obsState.details.scenes = scenes; - } - - if (session.optimize !== false) { - var bandwidth = parseInt(session.rpcs[UUID].targetBandwidth); - if (needOptimize) { - if (bandwidth > session.optimize || bandwidth < 0) { - // limit to optimized bitrate - bandwidth = session.optimize; - } - } - if (session.rpcs[UUID].bandwidth !== bandwidth) { - // bandwidth already set correctly. don't resend. - msg.bitrate = bandwidth; - warnlog("Message to be sent: "); - warnlog(msg); - if (session.sendRequest(msg, UUID)) { - session.rpcs[UUID].bandwidth = bandwidth; // this is letting the system know what the actual bandwidth is, even if it isn't the real target. - } else { - errorlog("Unable to set update OBS Visibility"); - } - } else { - warnlog("Message to be sent: "); - warnlog(msg); - session.sendRequest(msg, UUID); - } - } else { - warnlog("Message to be sent: "); - warnlog(msg); - session.sendRequest(msg, UUID); - } - } -}; - -session.getOBSOptimization = function (msg, UUID) { - if (session.obsState) { - msg.obsState = {}; - var needOptimize = false; - if (session.obsState.visibility !== null) { - msg.obsState.visibility = session.obsState.visibility; - if (session.obsState.visibility === false) { - needOptimize = true; - } - } - if (session.obsState.sourceActive !== null) { - msg.obsState.sourceActive = session.obsState.sourceActive; - //if (session.obsState.sourceActive===false){ - // needOptimize=true; - //} - } - if (session.obsState.recording !== null) { - msg.obsState.recording = session.obsState.recording; - } - if (session.obsState.streaming !== null) { - msg.obsState.streaming = session.obsState.streaming; - } - if (session.obsState.virtualcam !== null) { - msg.obsState.virtualcam = session.obsState.virtualcam; - } - } - if (session.optimize !== false) { - msg.optimizedBitrate = parseInt(session.optimize) || 0; // not setting a bitrate; just letting them know what the optimized bitrate is. - if (needOptimize) { - session.rpcs[UUID].bandwidth = msg.optimizedBitrate; - } - } - return msg; -}; - -function getOBSDetails(callbackname = "details") { - if (session.disableOBS) { - return false; - } - if (!window.obsstudio) { - return; - } - - if (!("details" in session.obsState)) { - session.obsState.details = {}; - } - - var readOnlyFuncs = [ - "getControlLevel", - //"getStatus", - "getCurrentScene", - "getScenes" - //"getTransitions", - //"getCurrentTransition", - //"pluginVersion" - ]; - - var promises = {}; - promises.main = true; - - Object.keys(window.obsstudio).forEach(async key => { - try { - if (typeof window.obsstudio[key] === "function") { - if (readOnlyFuncs.includes(key)) { - try { - promises[key] = true; - window.obsstudio[key](function (out) { - var shortkey = key.replace("get", ""); - shortkey = shortkey[0].toLowerCase() + shortkey.slice(1); - session.obsState.details[shortkey] = out; - delete promises[key]; - if (!Object.keys(promises).length) { - session.obsStateSync(callbackname); - } - }); - } catch (e) { - delete promises[key]; - } - } - /* } else if (typeof window.obsstudio[key] === 'object'){ // none of these values I really need right now. - var shortkey = key.replace("get",""); - shortkey = shortkey[0].toLowerCase() + shortkey.slice(1); - session.obsState.details[shortkey] = window.obsstudio[key]; - } else { - var shortkey = key.replace("get",""); - shortkey = shortkey[0].toLowerCase() + shortkey.slice(1); - session.obsState.details[shortkey] = window.obsstudio[key]; */ - } - } catch (e) { - errorlog(e); - } - }); - delete promises.main; - if (!Object.keys(promises).length) { - session.obsStateSync(callbackname); - } -} - -function toggleOBSControls() { - toggle(getById("remoteOBSControl")); - if (getById("remoteOBSControl").style.display == "none") { - getById("modalBackdrop").innerHTML = ""; // Delete modal - getById("modalBackdrop").remove(); - } else { - getById("modalBackdrop").innerHTML = ""; // Delete modal - getById("modalBackdrop").remove(); - var modalTemplate = `
`; - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - document.getElementById("modalBackdrop").addEventListener("click", toggleOBSControls); - } -} - -function toggleOBSControlsLock(ele, disable = null) { - const element = getById("remoteOBSControlContents"); - - // If disable is not specified, toggle based on current state - // Otherwise, use the provided value - const shouldDisable = disable !== null ? disable : - element.style.pointerEvents !== 'none'; - - element.style.pointerEvents = shouldDisable ? 'none' : 'auto'; - element.style.opacity = shouldDisable ? '0.6' : '1'; - ele.textContent = shouldDisable ? '🔒' : '🔓'; - - return shouldDisable; // returns the new state -} -function requestOBSAction(ele) { - if (session.disableOBS) { - return false; - } -} -function obsSceneChanged(event) { - log(event.detail.name); - getOBSDetails(); // contains obsStateSync -} -function obsVirtualcamStarted(event) { - session.obsState.virtualcam = true; - session.obsStateSync("virtualcam"); -} -function obsVirtualcamStopped(event) { - session.obsState.virtualcam = false; - session.obsStateSync("virtualcam"); -} -function obsStreamingStarted(event) { - session.obsState.streaming = true; - session.obsStateSync("streaming"); -} -function obsStreamingStopped(event) { - session.obsState.streaming = false; - session.obsStateSync("streaming"); -} -function obsRecordingStarted(event) { - session.obsState.recording = true; - session.obsStateSync("recording"); -} -function obsRecordingStopped(event) { - session.obsState.recording = false; - session.obsStateSync("recording"); -} -function obsSourceActiveChanged(event) { - warnlog("obsSourceActiveChanged"); - warnlog(event.detail); - - try { - if (typeof event === "boolean") { - var sourceActive = event; - } else if (typeof event.detail === "boolean") { - var sourceActive = event.detail; - } else if (typeof event.detail.active === "boolean") { - var sourceActive = event.detail.active; - } else { - var sourceActive = event.detail.active; - } - - if (typeof sourceActive === "undefined") { - return; - } // Just fail. - - if (session.obsState.sourceActive !== sourceActive) { - // only move forward if there is a change; the event likes to double fire you see. - session.obsState.sourceActive = sourceActive; - session.obsStateSync("sourceActive"); - } - } catch (e) { - errorlog(e); - } -} - -function obsSourceVisibleChanged(event) { - // accounts for visible in VDO.Ninja scene AND visible in OBS scene - warnlog("obsSourceVisibleChanged"); - warnlog(event.detail); - try { - if (typeof event === "boolean") { - var visibility = event; - } else if (typeof event.detail === "boolean") { - var visibility = event.detail; - } else if (typeof event.detail.visible === "boolean") { - var visibility = event.detail.visible; - } else { - var visibility = event.detail.visible; - } - - if (typeof visibility === "undefined") { - // fall back - if (typeof document.visibilityState !== "undefined") { - visibility = document.visibilityState === "visible"; // modern - } else if (typeof document.hidden !== "undefined") { - visibility = !document.hidden; // legacy - } else { - return; // ... unknown input? fail. - } - } - - if (session.obsState.visibility !== visibility) { - // only move forward if there is a change; the event likes to double fire you see. - session.obsState.visibility = visibility; - session.obsStateSync("visibility"); - } - } catch (e) { - errorlog(e); - } -} - -function manageSceneState(data, UUID) { - // incoming obs details - if (session.disableOBS) { - return; - } - var processNeeded = false; - try { - if ("sceneDisplay" in data) { - processNeeded = true; - session.pcs[UUID].sceneDisplay = data.sceneDisplay; - } - if ("sceneMute" in data) { - processNeeded = true; - session.pcs[UUID].sceneMute = data.sceneMute; - } - - if (data.obsState) { - if ("sourceActive" in data.obsState) { - processNeeded = true; - session.pcs[UUID].obsState.sourceActive = data.obsState.sourceActive; - } - if ("visibility" in data.obsState) { - processNeeded = true; - session.pcs[UUID].obsState.visibility = data.obsState.visibility; - session.optimizeBitrate(UUID); // &optimize flag; sets video bitrate to target value if this flag == HIDDEN (if optimize=0, disables both audio and video) - } - if ("details" in data.obsState) { - //if (Object.keys(data.obsState.details).length){ - processNeeded = true; - session.pcs[UUID].obsState.details = data.obsState.details; - //} - } - if ("streaming" in data.obsState) { - processNeeded = true; - session.pcs[UUID].obsState.streaming = data.obsState.streaming; - } - if ("recording" in data.obsState) { - processNeeded = true; - session.pcs[UUID].obsState.recording = data.obsState.recording; - } - if ("virtualcam" in data.obsState) { - processNeeded = true; - session.pcs[UUID].obsState.virtualcam = data.obsState.virtualcam; - } - } - } catch (e) { - errorlog(e); - } - - if (processNeeded) { - log(data); - applySceneState(); - } else { - return; - } - - if (isIFrame) { - pokeIframeAPI("obs-state", data.obsState, UUID); - } - - if (session.obsControls === false) { - return; - } - - try { - var control = 0; - if (session.pcs[UUID].obsState && session.pcs[UUID].obsState.details) { - control = parseInt(session.pcs[UUID].obsState.details.controlLevel) || 0; //0 for NONE, 1 for READ_OBS (OBS data), 2 for READ_USER (User data), 3 for BASIC, 4 for ADVANCED and 5 for ALL - } - - if (control >= 4) { - if (session.director || !session.roomid) { - if (session.pcs[UUID].remote) { - if (session.obsControls !== false) { - getById("obscontrolbutton").classList.remove("hidden"); // so they get a tip. - } - } - } - } - - var multi = false; - getById("obsControlButtons") - .querySelectorAll("[data-system]") - .forEach(ele => { - if (ele.dataset.system in session.pcs) { - if (ele.dataset.system !== UUID) { - multi = true; - } - } else { - // delete, since no longer active. - ele.remove(); - } - }); - - getById("obsSceneNames") - .querySelectorAll("[data-system]") - .forEach(ele => { - if (ele.dataset.system in session.pcs) { - if (ele.dataset.system !== UUID) { - multi = true; - } - } else { - // delete, since no longer active. - ele.remove(); - } - }); - - if (control == 0) { - var obsControlButtonsBox = getById("obsControlButtons").querySelector("[data-system='" + UUID + "']"); - if (obsControlButtonsBox) { - obsControlButtonsBox.remove(); - } - var obsSceneNamesBox = getById("obsSceneNames").querySelector("[data-system='" + UUID + "']"); // this hides if less than 2, so hide it now. - if (obsSceneNamesBox) { - obsSceneNamesBox.remove(); - } - if (!multi) { - getById("obsControlHelp").classList.remove("hidden"); - } - return; - } - - getById("obsControlHelp").classList.add("hidden"); - - var obsControlButtonsBox = getById("obsControlButtons").querySelector("[data-system='" + UUID + "']"); - if (!obsControlButtonsBox) { - obsControlButtonsBox = document.createElement("div"); - obsControlButtonsBox.dataset.system = UUID; - getById("obsControlButtons").appendChild(obsControlButtonsBox); - } else { - obsControlButtonsBox.innerHTML = ""; - } - - if (multi) { - var h3 = document.createElement("h3"); - h3.innerText = "OBS instance: " + (session.pcs[UUID].label || session.pcs[UUID].scene || UUID); - obsControlButtonsBox.appendChild(h3); - } - - if (session.pcs[UUID].obsState && "streaming" in session.pcs[UUID].obsState) { - var controlButton = document.createElement("button"); - controlButton.dataset.UUID = UUID; - - if (session.pcs[UUID].obsState.streaming) { - controlButton.classList.add("pressed"); - controlButton.ariaPressed = "true"; - controlButton.dataset.obsAction = "stopStreaming"; - controlButton.innerText = "📡 stop streaming"; - controlButton.classList.remove("hidden"); - } else if (session.pcs[UUID].obsState.streaming === false) { - controlButton.classList.remove("hidden"); - controlButton.dataset.obsAction = "startStreaming"; - controlButton.innerText = "📡 start streaming"; - } else { - controlButton.dataset.obsAction = "startStreaming"; - controlButton.innerText = "📡 start streaming"; - controlButton.classList.remove("hidden"); - } - - if (control < 5) { - controlButton.disabled = true; - controlButton.style.cursor = "not-allowed"; - controlButton.title = "Source is lacking required permissions."; - } else { - controlButton.onclick = async function () { - var msg = {}; - msg.obsCommand = {}; - msg.obsCommand.action = this.dataset.obsAction; - msg.UUID = this.dataset.UUID; - if (document.querySelector("#obsRemotePassword>input") && document.querySelector("#obsRemotePassword>input").value) { - msg.remote = document.querySelector("#obsRemotePassword>input").value; - } else { - msg.remote = session.remote; - } - msg = await session.encodeRemote(msg); - session.anysend(msg); // this is neat, but doesn't work with websocket. I need to add - log("action request: " + this.dataset.obsAction); - }; - } - obsControlButtonsBox.appendChild(controlButton); - } - if (session.pcs[UUID].obsState && "recording" in session.pcs[UUID].obsState) { - var controlButton = document.createElement("button"); - controlButton.dataset.UUID = UUID; - - if (session.pcs[UUID].obsState.recording) { - controlButton.classList.add("pressed"); - controlButton.ariaPressed = "true"; - controlButton.dataset.obsAction = "stopRecording"; - controlButton.innerText = "📽 stop recording"; - controlButton.classList.remove("hidden"); - } else if (session.pcs[UUID].obsState.recording === false) { - controlButton.classList.remove("hidden"); - controlButton.dataset.obsAction = "startRecording"; - controlButton.innerText = "📽 start recording"; - } else { - controlButton.classList.remove("hidden"); - controlButton.dataset.obsAction = "startRecording"; - controlButton.innerText = "📽 start recording"; - } - - if (control < 5) { - controlButton.disabled = true; - controlButton.style.cursor = "not-allowed"; - controlButton.title = "Source is lacking required permissions."; - } else { - controlButton.onclick = async function () { - var msg = {}; - msg.obsCommand = {}; - msg.obsCommand.action = this.dataset.obsAction; - msg.UUID = this.dataset.UUID; - if (document.querySelector("#obsRemotePassword>input").value) { - msg.remote = document.querySelector("#obsRemotePassword>input").value; - } else { - msg.remote = session.remote; - } - msg = await session.encodeRemote(msg); - session.anysend(msg); - log("action request: " + this.dataset.obsAction); - }; - } - obsControlButtonsBox.appendChild(controlButton); - } - if (session.pcs[UUID].obsState && "virtualcam" in session.pcs[UUID].obsState) { - var controlButton = document.createElement("button"); - - controlButton.dataset.UUID = UUID; - - if (session.pcs[UUID].obsState.virtualcam) { - controlButton.classList.add("pressed"); - controlButton.ariaPressed = "true"; - controlButton.dataset.obsAction = "stopVirtualcam"; - controlButton.innerText = "💻 stop virtualcam"; - controlButton.classList.remove("hidden"); - } else if (session.pcs[UUID].obsState.virtualcam === false) { - controlButton.classList.remove("hidden"); - controlButton.dataset.obsAction = "startVirtualcam"; - controlButton.innerText = "💻 start virtualcam"; - } else { - controlButton.classList.remove("hidden"); - controlButton.dataset.obsAction = "startVirtualcam"; - controlButton.innerText = "💻 start virtualcam"; - } - - if (control < 5) { - controlButton.disabled = true; - controlButton.style.cursor = "not-allowed"; - controlButton.title = "Source is lacking required permissions."; - } else { - controlButton.onclick = async function () { - var msg = {}; - msg.obsCommand = {}; - msg.obsCommand.action = this.dataset.obsAction; - msg.UUID = this.dataset.UUID; - if (document.querySelector("#obsRemotePassword>input").value) { - msg.remote = document.querySelector("#obsRemotePassword>input").value; - } else { - msg.remote = session.remote; - } - msg = await session.encodeRemote(msg); - session.anysend(msg); - log("action request: " + this.dataset.obsAction); - }; - } - obsControlButtonsBox.appendChild(controlButton); - } - } catch (e) { - errorlog(e); - } // just in case the client has disconnected. - - if (control < 2) { - var obsSceneNamesBox = getById("obsSceneNames").querySelector("[data-system='" + UUID + "']"); - if (obsSceneNamesBox) { - obsSceneNamesBox.remove(); - } - return; - } - - var obsSceneNamesBox = getById("obsSceneNames").querySelectorAll("div[data-system='" + UUID + "']"); - if (!obsSceneNamesBox.length) { - obsSceneNamesBox = document.createElement("div"); - obsSceneNamesBox.dataset.system = UUID; - getById("obsSceneNames").appendChild(obsSceneNamesBox); - } else { - obsSceneNamesBox = obsSceneNamesBox[0]; - obsSceneNamesBox.innerHTML = ""; - } - - if (multi) { - var h3 = document.createElement("h3"); - h3.innerText = "OBS instance: " + (session.pcs[UUID].label || session.pcs[UUID].scene || UUID); - obsSceneNamesBox.appendChild(h3); - } - - if (session.pcs[UUID].obsState.details) { - var details = session.pcs[UUID].obsState.details; - if (details.scenes) { - details.scenes.forEach(scene => { - var sceneButton = document.createElement("button"); - sceneButton.dataset.obsScene = scene; - sceneButton.dataset.UUID = UUID; - sceneButton.innerText = scene; - if (details.currentScene && details.currentScene.name && details.currentScene.name === scene) { - sceneButton.classList.add("pressed"); - sceneButton.ariaPressed = "true"; - } - obsSceneNamesBox.appendChild(sceneButton); - if (control < 4) { - sceneButton.disabled = true; - sceneButton.style.cursor = "not-allowed"; - sceneButton.title = "Source is lacking required permissions."; - } else { - sceneButton.onclick = async function () { - var msg = {}; - msg.obsCommand = { action: "setCurrentScene", value: this.dataset.obsScene }; - msg.UUID = this.dataset.UUID; - if (document.querySelector("#obsRemotePassword>input").value) { - msg.remote = document.querySelector("#obsRemotePassword>input").value; - } else { - msg.remote = session.remote; - } - msg = await session.encodeRemote(msg); - session.anysend(msg); - log("scene change request: " + this.dataset.obsScene); - }; - } - }); - } - } - getById("debugRemoteOBSControl").innerText = JSON.stringify(session.pcs[UUID].obsState); -} - -function processOBSCommand(msg) { - if (session.disableOBS) { - return false; - } else if (!window.obsstudio) { - return false; - } else if (typeof msg.obsCommand !== "object") { - return false; - } else if ("remote" in msg) { - if ((msg.remote === session.remote && session.remote) || session.remote === true) { - // approved - } else { - if (msg.UUID && msg.obsCommand.action) { - var data = {}; - data.rejected = "obsCommand"; - //data.debug = msg.remote; - session.sendRequest(data, msg.UUID); // this skips the server - } - warnlog("Denied access; remote does not match"); - return false; - } - } else { - if (msg.UUID && msg.obsCommand.action) { - var data = {}; - data.rejected = "obsCommand"; - //data.debug = "no remote code provided"; - session.sendRequest(data, msg.UUID); // this skips the server - } - return false; - } - - try { - // {changeScene: this.dataset.obsScene} - if (msg.obsCommand.action && typeof msg.obsCommand.action == "string") { - - if (msg.obsCommand.action == "stopVirtualcam" || msg.obsCommand.action == "startVirtualcam") { - if (session.obsState.virtualcam === false) { - if (msg.UUID) { - var data = {}; - data.rejected = msg.obsCommand.action; - session.sendRequest(data, msg.UUID); // this skips the server - } - return false; - } - } - if (msg.obsCommand.action == "stopRecording" || msg.obsCommand.action == "startRecording") { - if (session.obsState.recording === false) { - if (msg.UUID) { - var data = {}; - data.rejected = msg.obsCommand.action; - session.sendRequest(data, msg.UUID); // this skips the server - } - return false; - } - } - if (msg.obsCommand.action == "stopStreaming" || msg.obsCommand.action == "startStreaming") { - if (session.obsState.streaming === false) { - if (msg.UUID) { - var data = {}; - data.rejected = msg.obsCommand.action; - session.sendRequest(data, msg.UUID); // this skips the server - } - return false; - } - } - - if (msg.obsCommand.value && typeof msg.obsCommand.value == "string") { - if (msg.obsCommand.action == "setCurrentScene" && session.filterOBSscenes && session.filterOBSscenes.length) { - try { - if (!session.filterOBSscenes.includes(msg.obsCommand.value)) { - return false; - } - } catch (e) { - errorlog(e); - return false; - } - } - window.obsstudio[msg.obsCommand.action](msg.obsCommand.value); - } else { - window.obsstudio[msg.obsCommand.action](); - } - } - } catch (e) { - errorlog(e); - return false; - } - return true; -} - -function applySceneState() { - // guest side; tally light, etc. - - if (document.getElementById("videosource")) { - var visibility = false; - var ondeck = false; - var recording = false; - var tallyStyle = session.tallyStyle; - if (!tallyStyle && session.tallyStyleDefault) { - tallyStyle = session.tallyStyleDefault; - } - - if (session.tallyOverride !== false) { - if (session.tallyOverride == 1) { - recording = true; - } else if (session.tallyOverride == 2) { - ondeck = true; - visibility = false; - recording = false; - } else if (session.tallyOverride == 3) { - visibility = true; - recording = false; - } else if (session.tallyOverride == 0) { - ondeck = false; - visibility = false; - recording = false; - } else { - // maybe its a custom message or default? - } - if (!session.cleanOutput) { - getById("obsState").classList.remove("hidden"); - } - } else if (!session.disableOBS) { - for (var uid in session.pcs) { - if (session.pcs[uid].obsState.sourceActive !== false && session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) { - visibility = true; - } else if (session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) { - ondeck = true; - } - if ((session.pcs[uid].obsState.recording || session.pcs[uid].obsState.streaming) && session.pcs[uid].obsState.sourceActive !== false && session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) { - // the scene that is recording must be visible also. - recording = true; - } - } - if (!session.cleanOutput) { - getById("obsState").classList.remove("hidden"); - } - } else { - return; - } - - if (recording) { - getById("obsState").classList.remove("ondeck"); - getById("obsState").classList.add("recording"); // TODO: this needs to check all peers to make sure it's valid - getById("obsState").innerHTML = "ON AIR"; - - if (tallyStyle) { - getById("main").classList.remove("ondeck"); - getById("main").classList.add("recording"); - } - } else if (visibility) { - getById("obsState").classList.remove("recording"); - getById("obsState").classList.remove("ondeck"); - getById("obsState").innerHTML = "ACTIVE"; - - if (tallyStyle) { - // only show active if its tally is enabled manually - getById("main").classList.remove("recording"); - getById("main").classList.remove("ondeck"); - } else { - getById("obsState").classList.add("hidden"); // most people don't care about being active - } - } else if (ondeck) { - getById("obsState").classList.remove("recording"); - getById("obsState").classList.add("ondeck"); // TODO: this needs to check all peers to make sure it's valid - getById("obsState").innerHTML = "STAND BY"; - - if (tallyStyle) { - getById("main").classList.remove("recording"); - getById("main").classList.add("ondeck"); - } - } else { - getById("obsState").classList.remove("recording"); - getById("obsState").classList.remove("ondeck"); - getById("obsState").innerHTML = "INACTIVE"; - getById("obsState").classList.add("hidden"); // I don't think most people care to see inactive. - - if (tallyStyle) { - getById("main").classList.remove("recording"); - getById("main").classList.remove("ondeck"); - } - } - - //miniTranslate(getById("obsState")); - - if (visibility) { - // BASIC TALLY LIGHT (on deck disabled) - getById("obsState").classList.add("onair"); // LIVE - if (tallyStyle) { - getById("main").classList.add("onair"); - } - } else { - getById("obsState").classList.remove("onair"); - if (tallyStyle) { - getById("main").classList.remove("onair"); - } - } - - if (session.automute) { - if (!visibility) { - session.micIsolatedAutoMute = []; - if (session.automute !== "2") { - for (var uid in session.pcs) { - if (session.directorList.indexOf(uid) >= 0) { - // allow validated directors to hear the guest - session.micIsolatedAutoMute.push(uid); - } - } - } - } else { - session.micIsolatedAutoMute = false; - } - session.applyIsolatedChat(); - } - } -} - -function compare_vids(a, b) { - var aa = a.order || 0; - var bb = b.order || 0; - if (aa < bb) { - return 1; - } - if (aa > bb) { - return -1; - } - return 0; -} - -function compare_vids_sid(a, b) { - var aa = a.dataset.sid || 0; - var bb = b.dataset.sid || 0; - if (aa > bb) { - return 1; - } - if (aa < bb) { - return -1; - } - return 0; -} -function compare_vids_label(a, b) { - if (a.dataset.UUID && session.rpcs[a.dataset.UUID] && session.rpcs[a.dataset.UUID].label) { - var aa = session.rpcs[a.dataset.UUID].label.toLowerCase(); - } else { - var aa = 0; - } - - if (b.dataset.UUID && session.rpcs[b.dataset.UUID] && session.rpcs[b.dataset.UUID].label) { - var bb = session.rpcs[b.dataset.UUID].label.toLowerCase(); - } else { - var bb = 0; - } - - if (aa > bb) { - return 1; - } - if (aa < bb) { - return -1; - } - return 0; -} - -function sortByZ(mediaPool, layout) { - function sortABZ(a, b) { - if (layout[a.dataset.sid]) { - var aa = layout[a.dataset.sid].zIndex || layout[a.dataset.sid].z || 0; - } else { - var aa = 0; - } - if (layout[b.dataset.sid]) { - var bb = layout[b.dataset.sid].zIndex || layout[b.dataset.sid].z || 0; - } else { - var bb = 0; - } - if (aa < bb) { - return -1; - } - if (aa > bb) { - return 1; - } - return 0; - } - mediaPool.sort(sortABZ); - return mediaPool; -} - -window.onpopstate = function () { - if (session.firstPlayTriggered) { - window.location.reload(true); // deprecated, but it seems to work, so w/e - } -}; - -var miniPerformerX = null; -var miniPerformerY = null; -function makeMiniDraggableElement(elmnt) { - if (session.disableMouseEvents) { - return; - } - - try { - elmnt.dragElement = false; - // elmnt.style.bottom = "auto"; - elmnt.style.cursor = "grab"; - - elmnt.stashonmouseup = null; - elmnt.stashonmousemove = null; - } catch (e) { - errorlog(e); - return; - } - - var pos1 = 0; - var pos2 = 0; - var pos3 = 0; - var pos4 = 0; - - var timestamp = false; - - function elementDrag(e) { - // ON DRAG - timestamp = false; - if (session.infocus) { - return; - } - try { - e = e || window.event; - - if (e.type !== "touchmove") { - if ("buttons" in e && e.buttons !== 1) { - closeDragElement(e); - return; - } - e.preventDefault(); - } - e.stopPropagation(); - - elmnt.dragElement = true; - - if (e.type === "touchmove") { - pos1 = pos3 - e.touches[0].clientX; - pos2 = pos4 - e.touches[0].clientY; - pos3 = e.touches[0].clientX; - pos4 = e.touches[0].clientY; - } else { - pos1 = pos3 - e.clientX; - pos2 = pos4 - e.clientY; - pos3 = e.clientX; - pos4 = e.clientY; - } - - var topDrag = elmnt.offsetTop - pos2; - if (topDrag > -3 + (window.innerHeight - elmnt.clientHeight)) { - topDrag = -3 + (window.innerHeight - elmnt.clientHeight); - } - - miniPerformerY = topDrag; - miniPerformerX = elmnt.offsetLeft - pos1; - - if (miniPerformerY > window.innerHeight - elmnt.clientHeight) { - miniPerformerY = window.innerHeight - elmnt.clientHeight; - } - if (miniPerformerX > window.innerWidth - elmnt.clientWidth) { - miniPerformerX = window.innerWidth - elmnt.clientWidth; - } - - miniPerformerX = (100 * miniPerformerX) / window.innerWidth; - miniPerformerY = (100 * miniPerformerY) / window.innerHeight; - - if (session.widget && !session.leftMiniPreview) { - if (miniPerformerX > 74) { - miniPerformerX = 74; - } - } - - if (miniPerformerY < 0) { - miniPerformerY = 0; - } else if (miniPerformerY > 100) { - miniPerformerY = 100; - } - if (miniPerformerX < 0) { - miniPerformerX = 0; - } else if (miniPerformerX > 100) { - miniPerformerX = 100; - } - - elmnt.style.right = "unset"; - elmnt.style.top = miniPerformerY + "%"; - elmnt.style.left = miniPerformerX + "%"; - } catch (e) { - errorlog(e); - } - } - - function closeDragElement(e) { - // TOUCH END - e = e || window.event; - - if (e.type !== "touchend") { - if (e.button !== 0) { - return; - } - document.onmouseup = elmnt.stashonmouseup; - document.onmousemove = elmnt.stashonmousemove; - elmnt.onmouseleave = null; - } - - if (session.infocus) { - return; - } - e.preventDefault(); - - if (timestamp && Date.now() - timestamp > 500) { - // long hold, so this is a drag - e.stopPropagation(); - if (e.type === "touchend") { - if (session.infocus === true) { - session.infocus = false; - } else { - session.infocus = true; - log("session: myself"); - } - setTimeout(() => updateMixer(), 10); - } - } else if (timestamp && e.type !== "touchend") { - if (session.infocus === true) { - session.infocus = false; - } else { - session.infocus = true; - log("session: myself"); - } - setTimeout(() => updateMixer(), 10); - } - } - - function dragMouseDown(e) { - ////// TOUCH START - - if (event.ctrlKey || event.metaKey) { - return; - } - - timestamp = Date.now(); - - e = e || window.event; - if (session.infocus) { - return; - } - - e.preventDefault(); - if (e.type === "touchstart") { - pos3 = e.touches[0].clientX; - pos4 = e.touches[0].clientY; - - elmnt.ontouchend = closeDragElement; - elmnt.ontouchmove = elementDrag; - } else { - if (e.button !== 0) { - return; - } - pos3 = e.clientX; - pos4 = e.clientY; - elmnt.stashonmouseup = document.onmouseup; // I don't want to interfere with other drag events. - elmnt.stashonmousemove = document.onmousemove; - - document.onmouseup = closeDragElement; - document.onmousemove = elementDrag; - elmnt.onmouseleave = function (event) { - closeDragElement(event); - }; - } - } - - elmnt.onmousedown = dragMouseDown; - elmnt.ontouchstart = dragMouseDown; -} - -function makeDraggableElement(element) { - if (session.disableMouseEvents) { - return; - } // this is here for a reason. :P - if (!element) { - return; - } - element.initialX; - element.initialY; - element.currentX; - element.xOffset = 0; - element.currentY; - element.yOffset = 0; - element.isDragging = false; - element.dragElement = true; - - element.addEventListener("mousedown", dragStart); - - function dragStart(e) { - element.initialX = e.clientX - element.xOffset; - element.initialY = e.clientY - element.yOffset; - - document.addEventListener("mousemove", drag); - document.addEventListener("mouseup", dragEnd); - document.addEventListener("onmouseleave", dragEnd); - document.addEventListener("onmouseenter", dragEnd); - - element.isDragging = true; - } - - function dragEnd(e) { - element.initialX = element.currentX; - element.initialY = element.currentY; - - document.removeEventListener("mousemove", drag); - document.removeEventListener("mouseup", dragEnd); - document.removeEventListener("onmouseleave", dragEnd); - document.removeEventListener("onmouseenter", dragEnd); - - element.isDragging = false; - } - - function drag(e) { - if (element.isDragging) { - element.currentX = e.clientX - element.initialX; - element.currentY = e.clientY - element.initialY; - - // Get the dimensions of the viewport - let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); - let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); - - // Get the dimensions of the object - let elementWidth = element.offsetWidth; - let elementHeight = element.offsetHeight; - // console.log('elementWidth:\n',elementWidth) - // console.log('elementHeight:\n',elementHeight) - - // Calculate the boundaries - let maxX = vw - elementWidth; - let maxY = vh - elementHeight; - let minX = 0; - let minY = 0; - - // Calculate real boundaries (parent position: fixed issues) - let topOffset = 0; - let leftOffset = 0; - let elementOffset = element; - while (elementOffset) { - topOffset += elementOffset.offsetTop; - leftOffset += elementOffset.offsetLeft; - elementOffset = elementOffset.offsetParent; - } - - // Adjust the position if it's going beyond the boundaries - let realX = element.currentX + leftOffset; - let realY = element.currentY + topOffset; - - if (realX > maxX) { - element.currentX = maxX - leftOffset; - } else if (realX < minX) { - element.currentX = minX - leftOffset; - } - - if (realY > maxY) { - element.currentY = maxY - topOffset; - } else if (realY < minY) { - element.currentY = minY - topOffset; - } - // Update the position and offset - element.xOffset = element.currentX; - element.yOffset = element.currentY; - - element.style.transform = `translate(${element.currentX}px, ${element.currentY}px)`; - } - } -} - -function clearCacheForCurrentSite() { - if ('caches' in window) { - try { - caches.keys().then(function (cacheNames) { - cacheNames.forEach(function (cacheName) { - caches.delete(cacheName); - }); - }); - log("Cache cleared for current site"); - } catch (e) { } - } else { - warnlog("Cache API not supported"); - } -} - -function removeStorage(cname) { - localStorage.removeItem(cname); -} - -function clearStorage() { - localStorage.clear(); - //clearCacheForCurrentSite(); // cache as well. - if (!session.cleanOutput) { - warnUser("The local storage and saved settings have been cleared", 1000); - } -} - -function setStorage(cname, cvalue, hours = 9999) { - // not actually a cookie - var now = new Date(); - var item = { - value: cvalue, - expiry: now.getTime() + hours * 60 * 60 * 1000 - }; - try { - localStorage.setItem(cname, JSON.stringify(item)); - } catch (e) { - errorlog(e); - } -} - -function getStorage(cname) { - try { - var itemStr = localStorage.getItem(cname); - } catch (e) { - errorlog(e); - return; - } - if (!itemStr) { - return ""; - } - var item = JSON.parse(itemStr); - var now = new Date(); - if (now.getTime() > item.expiry) { - localStorage.removeItem(cname); - return ""; - } - return item.value; -} - -function play(streamid = null, UUID = false) { - // play whatever is in the URL params; or filter by a streamID option - log("play stream: " + session.view + " " + streamid); - - if (session.viewDirectorOnly) { - if (!(UUID || streamid)) { - warnlog("No UUID and StreamID"); - return; - } else if (session.directorList.indexOf(UUID) == -1) { - warnlog("Not a director"); - return; - } - } - - if (session.view_set) { - var played = false; - for (var j in session.view_set) { - if (streamid === null) { - // play what is in the view list ; not a group room probably - session.watchStream(session.view_set[j]); - played = true; - } else if (streamid === session.view_set[j]) { - // plays if the group room list matches the explicit list - session.watchStream(session.view_set[j]); - played = true; - } - } - if (session.include) { - session.include.forEach(sid => { - if (session.view_set.includes(sid)) { - // already played - } else if (streamid === null) { - // play what is in the view list ; not a group room probably - session.watchStream(sid); - } else if (streamid === sid) { - // plays if the group room list matches the explicit list - session.watchStream(sid); - played = true; - } - }); - } - - if (!played && streamid) { - if (session.scene !== false) { - if (!session.permaid) { - if (!session.queue) { - // I don't want to deal with queues. - if (session.exclude === false || !session.exclude.includes(streamid)) { - if (UUID) { - if (session.directorList.indexOf(UUID) >= 0) { - warnlog("stream ID added to badStreamList: " + streamid); - session.badStreamList.push(streamid); - // if I uncomment this, the director can mute the solo link. - // session.watchStream(streamid); // changed June 4th 2024. We shouldn't be viewing the stream if on the bad list, no? - } - } - } - } - } - } - } - } else if (streamid && session.exclude !== false) { - if (session.exclude.includes(streamid)) { - // we don't play it at all. (if explicity listed as VIDEO, then OKay.) - } else { - session.watchStream(streamid); // I suppose we do play it. - } - } else if (streamid) { - if (session.optimize === 0) { - log("Running special optimize===logic logic for loading rtc connections"); - try { - // must be a scene and not an auto scene - if (session.scene && session.activatedStreams.size) { - if (session.activatedStreams.has(streamid)) { - session.watchStream(streamid); - return; - } - } - if (UUID && streamid) { - if (session.directorList.indexOf(UUID) >= 0) { - session.watchStream(streamid); - } - } - } catch (e) { - errorlog(e); - } - } else { - session.watchStream(streamid); - } - } else if (session.include.length) { - session.include.forEach(sid => { - session.watchStream(sid); - }); - } -} - -function nextQueue() { - if (!session.queue) { - return; - } - if (!session.director) { - return; - } - if (session.queueList.length == 0) { - getById("queuebutton").classList.add("red"); - setTimeout(function () { - getById("queuebutton").classList.remove("red"); - }, 50); - return; - } - var nextStream = session.queueList.shift(); - - getById("queuebutton").classList.add("red"); - setTimeout(function () { - getById("queuebutton").classList.remove("red"); - }, 200); - - updateQueue(); - - session.watchStream(nextStream); - log("next stream loading: " + nextStream); -} - -function updateQueue(adding = false) { - if (!session.queue) { - return; - } - if (!session.director) { - return; - } - if (session.queueList.length) { - if (session.queueList.length > 10) { - getById("queueNotification").innerHTML = "‼"; - } else { - getById("queueNotification").innerHTML = session.queueList.length; - } - getById("queueNotification").classList.add("queueNotification"); - } else { - getById("queueNotification").innerHTML = ""; - getById("queueNotification").classList.remove("queueNotification"); - } - - // Keep the toolbar button visually hot while guests are waiting - var queueButton = getById("queuebutton"); - if (queueButton) { - if (session.queueList.length) { - queueButton.classList.add("queueAttention"); - } else { - queueButton.classList.remove("queueAttention"); - } - } - var queueBadge = getById("queueNotification"); - if (queueBadge) { - if (session.queueList.length) { - queueBadge.classList.add("queueNotificationPulse"); - } else { - queueBadge.classList.remove("queueNotificationPulse"); - } - } - - if (adding) { - if (session.beepToNotify) { - // Favor the louder knock tone when approvals are enabled - playtone(false, session.knockToneEnabled ? "knocktone" : "testtone"); - showNotification("someone joined the queue", "queue length: " + session.queueList.length); - } - getById("queuebutton").classList.remove("shake"); - setTimeout(function () { - getById("queuebutton").classList.add("shake"); - }, 10); - } -} - -function hideStreamLowBandwidth(bandwidth, UUID) { - if (session.lowBitrateCutoff === false) { // allow 0,but I should probably also do this if there is a disconnect. - return; - } - - if (session.directorList.includes(UUID) || session.rpcs[UUID].director) { - if (session.showDirector || session.rpcs[UUID].showDirector) { - // all good - } else { - return; // we don't include the director since not treated as a guest - } - } - - if (bandwidth <= session.lowBitrateCutoff) { - //log("bandwidth <= session.lowBitrateCutoff"); - log("actual bandwidth: " + bandwidth + " < bandwidth cut threshold: " + session.lowBitrateCutoff); - // <= used so 0 can be used as a trigger - if (session.lowBitrateSceneChange) { - changeSceneLowBandwidth(true); - } else if (!session.rpcs[UUID].bandwidthMuted) { - session.rpcs[UUID].bandwidthMuted = true; - updateMixer(); - } - } else if (session.lowBitrateSceneChange) { - log("changeSceneLowBandwidth(false)"); - changeSceneLowBandwidth(false); - } else if (session.rpcs[UUID].bandwidthMuted) { - session.rpcs[UUID].bandwidthMuted = false; - if (session.rpcs[UUID].videoElement) { - session.rpcs[UUID].videoElement.muted = checkMuteState(UUID); - } - updateMixer(); - } -} - -var changeSceneEnabled = false; -var changeSceneLowBandwidthRevert = false; -function changeSceneLowBandwidth(state) { - if (!session.lowBitrateSceneChange) { - return; - } - if (!session.obsState) { - return; - } - try { - if (session.obsState.sourceActive && session.obsState.details && session.obsState.details.currentScene) { - - changeSceneLowBandwidthRevert = session.obsState.details.currentScene.name || false; - } else if ("sourceActive" in session.obsState && !session.obsState.sourceActive && session.obsState.details && session.obsState.details.currentScene) { - if (session.obsState.details.currentScene.name !== session.lowBitrateSceneChange) { - return; // not the FML scene, nor are we visible, so we're not going to switch back. Assume the user has overtaken the setup. - } - } - if (!window.obsstudio || !window.obsstudio["setCurrentScene"]) { - return; - } - if (state && changeSceneLowBandwidthRevert) { - if (changeSceneEnabled) { - // bitrate was higher , so we can now cut off. - //log("2 changeSceneEnabled: "+session.lowBitrateSceneChange+" changeSceneEnabled:"+changeSceneEnabled); - log("Changing to cut scene due to low bandwidth"); - window.obsstudio["setCurrentScene"](session.lowBitrateSceneChange); - } else { - log("Low bandwidth, but not changing scenes because threshold not hit at least once yet"); - } - } else if (changeSceneLowBandwidthRevert) { - changeSceneEnabled = true; - //log("1 setCurrentScene: "+changeSceneLowBandwidthRevert+" changeSceneEnabled:"+changeSceneEnabled); - log("Reverting to original scene due to good bandwidth"); - window.obsstudio["setCurrentScene"](changeSceneLowBandwidthRevert); - } - } catch (e) { - errorlog(e); - } -} - -function setupIncomingScreenTracking(v, UUID) { - // SCREEN element. - - if (session.directorList.indexOf(UUID) >= 0) { - v.muted = false; - } - - v.addEventListener( - "playing", - e => { - try { - var bigPlayButton = document.getElementById("bigPlayButton"); - if (bigPlayButton) { - bigPlayButton.parentNode.removeChild(bigPlayButton); - } - } catch (e) { } - - resetupAudioOut(e.target, true); - - try { - if (session.pip) { - if (v.readyState >= 3) { - if (!v.pip) { - v.pip = true; - toggleSystemPip(v, true); - } - } - } - } catch (e) { } - }, - { once: true } - ); - - v.onpause = event => { - // prevent things from pausing; human or other - - if (v.dataset.UUID && session.rpcs[v.dataset.UUID] && session.rpcs[v.dataset.UUID].manualBandwidth === 0) { - return true; - } - if (!(event.ctrlKey || event.metaKey)) { - warnlog("Video paused; force it to play again"); - //return; - //session.audioCtx.resume(); - //log("ctx resume"); - - event.currentTarget - .play() - .then(_ => { - log("playing 4"); - }) - .catch(error => { - warnlog("didnt play 1"); - }); - if (Firefox) { - unPauseVideo(v); - } - } - return true; - }; - - if (session.pip) { - v.onloadedmetadata = function () { - if (!v.paused) { - if (!v.pip) { - v.pip = true; - toggleSystemPip(v, true); - } - } - }; - } - - v.addEventListener("resize", e => { - // if the aspect ratio changes, then we might want to update the mixer. If audio only, then this doesn't matter. - var v = e.target; - var aspectRatio = parseFloat(v.videoWidth / v.videoHeight) || 0; - log("resize event: " + aspectRatio); - - if (!aspectRatio) { - v.resetAR = true; - return; - } // if Audio only, then we don't want to set or update any aspect ratio. - - if (v.resetAR) { - log("ASPECT RATIO UNMUTED"); - delete v.resetAR; - v.dataset.aspectRatio = aspectRatio; - pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); - setTimeout(function () { - updateMixer(); - }, 1); - } else if (v.dataset.aspectRatio) { - if (aspectRatio != parseFloat(v.dataset.aspectRatio)) { - log("ASPECT RATIO CHANGED"); - v.dataset.aspectRatio = aspectRatio; - pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); - setTimeout(function () { - updateMixer(); - }, 1); // We don't want to run this on the first resize? just subsequent ones. - } - } else { - log("NEW VIDEO ? ASPECT RATIO new"); - v.dataset.aspectRatio = aspectRatio; - pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); - setTimeout(function () { - updateMixer(); - }, 1); - } - }); - if (typeof session.volume == "number") { - v.volume = session.volume; - } else { - v.volume = 1.0; // play audio automatically - } - v.autoplay = true; - v.controls = session.showControls || false; - v.classList.add("tile"); - v.setAttribute("playsinline", ""); - v.controlTimer = null; - - v.dataset.menu = "context-menu-video"; - if (!session.cleanOutput) { - v.classList.add("task"); // this adds the right-click menu - } - - if (document.getElementById("mainmenu")) { - var m = getById("mainmenu"); - m.remove(); - document.querySelectorAll(".hidden2").forEach(ele2 => { - ele2.classList.remove("hidden2"); - }); - } - - if (session.director) { - if (session.showControls !== null) { - v.controls = session.showControls; - } else { - v.controls = true; - } - var container = getById("screenContainer_" + UUID); - v.container = container; - v.disablePictureInPicture = false; - v.setAttribute("controls", "controls"); - container.appendChild(v); - pokeIframeAPI("control-box-video-updated", v.id, UUID); - session.requestRateLimit(session.directorViewBitrate, UUID); /// limit resolution for director - v.title = "Hold CTRL or CMD (⌘) while clicking the video to open detailed stats"; - if (session.beepToNotify) { - playtone(); - } - } else if (session.scene !== false) { - v.controls = session.showControls || false; - - if (session.view) { - // specific video to be played - v.style.display = "block"; - } else if (session.scene === "0") { - // auto plays, right? - v.style.display = "block"; - } else { - // group scene I guess; needs to be added manually - v.style.display = "none"; - v.mutedStateScene = true; - } - - setTimeout(function () { - updateMixer(); - }, 1); - } else if (session.roomid !== false) { - if (session.cleanOutput) { - v.controls = session.showControls || false; - } else if (session.studioSoftware) { - v.controls = session.showControls || false; - } else if (session.showControls !== null) { - v.controls = session.showControls; - } else { - v.controls = true; - } - //if ((session.roomid==="") && (session.bitrate)){ - // let's keep the default bitrates, since this isn't a real room and bitrates are specified. - //} //else if (session.novideo !== false){ - // if (session.novideo.includes(session.rpcs[UUID].streamID)){ // no video will have muted the video already anyways. - // session.requestRateLimit(0,UUID, false);// optimizing audio here doesn't later get turned back on. let the automixer disable audio instead - // } - //} //else { - // session.requestRateLimit(0,UUID, false);//// optimizing audio here doesn't later get turned back on. let the automixer disable audio instead - //} - setTimeout(function () { - updateMixer(); - }, 1); - } else { - v.style.display = "block"; - setTimeout(function () { - updateMixer(); - }, 1); - } - - v.addEventListener("click", function (e) { - // show stats of video if double clicked - log("clicked"); - try { - var uid = e.currentTarget.dataset.UUID; - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - if (session.statsMenu !== false) { - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, uid); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - e.stopPropagation(); - return false; - } else if ("prePausedBandwidth" in session.rpcs[uid]) { - unPauseVideo(e.currentTarget); - } - } catch (e) { - errorlog(e); - } - }); - - if (session.statsMenu) { - if ("stats" in session.rpcs[UUID]) { - if (getById("menuStatsBox")) { - clearInterval(getById("menuStatsBox").interval); - getById("menuStatsBox").remove(); - } - - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, UUID); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID); - } - } - - v.touchTimeOut = null; - v.touchLastTap = 0; - v.touchCount = 0; - v.addEventListener("touchend", function (event) { - if (session.disableMouseEvents) { - return; - } - - log("touched"); - - //document.ontouchup = null; - //document.onmouseup = null; - document.onmousemove = null; - document.ontouchmove = null; - - var currentTime = new Date().getTime(); - var tapLength = currentTime - v.touchLastTap; - clearTimeout(v.touchTimeOut); - if (tapLength < 500 && tapLength > 0) { - /// - log("double touched"); - v.touchCount += 1; - event.preventDefault(); - if (v.touchCount < 5) { - v.touchLastTap = currentTime; - return false; - } - v.touchLastTap = 0; - v.touchCount = 0; - - log("double touched"); - if (session.statsMenu !== false) { - var uid = event.currentTarget.dataset.UUID; - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - - printViewStats(innerMenu, uid); - - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - event.stopPropagation(); - return false; - ////// - } else { - v.touchCount = 1; - v.touchTimeOut = setTimeout( - function (vv) { - clearTimeout(vv.touchTimeOut); - vv.touchLastTap = 0; - vv.touchCount = 0; - }, - 5000, - v - ); - v.touchLastTap = currentTime; - } - }); - - if (v.controls == false) { - v.addEventListener("click", function () { - if (v.paused) { - log("PLAYING MANUALLY?"); - v.play() - .then(_ => { - log("playing 5"); - }) - .catch(warnlog); - } - }); - if (session.nocursor == false) { - // we do not want to show the controls. This is because MacOS + OBS does not work; so electron app needs this. - if (!session.cleanOutput) { - if (session.studioSoftware) { - } else if (session.showControls === false) { - // explicitly disabled; default null. - } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - } else { - if (v.controlTimer) { - clearInterval(v.controlTimer); - } - v.controlTimer = setTimeout(showControlBar.bind(null, v), 1000); - //v.controlTimer = setTimeout(function (){v.controls=true;},3000); // 3 seconds before I enable the controls automatically. This way it doesn't auto appear during loading. 3s enough, right? - } - } - } - } - - //if (session.fadein){ - v.addEventListener("animationend", function (e) { - v.classList.remove("fadein"); // allows the video to fade in. - if (v.holder) { - v.holder.classList.remove("fadein"); - } - }); - // v.classList.add("fadein"); // allows the video to fade in. - // if (v.holder){ - // v.holder.classList.add("fadein"); - // } - //} - - applyMuteState(UUID); // TODO; needs to be specific to screen video - v.usermuted = false; - - v.addEventListener("volumechange", function (e) { - var muteState = checkMuteState(UUID); - if (this.muted && this.muted !== muteState) { - this.usermuted = 1; - } else if (!this.muted && this.muted !== muteState) { - this.usermuted = 2; - } else if (!this.muted) { - this.usermuted = false; - } - }); - - if (session.screenShareStartPaused) { - // we know this is a screen share already - pauseVideo(v, false); - } - - if (session.director) { - /* var wss = ""; - if (session.customWSS || session.wssSetViaUrl){ - if (session.customWSS && (session.customWSS!==true)){ - wss = "&pie="+session.customWSS; - } else if (session.customWSS==true){ - wss = "&wss=" + session.wss; - } else { - wss = "&wss2=" + session.wss; - } - } */ - /* - var codecGroupFlag=""; - if (session.codecGroupFlag){ - codecGroupFlag = session.codecGroupFlag; - } */ - - /* var passAdd2=""; - if (session.password){ - if (session.defaultPassword===false){ - passAdd2="&password="+session.password; - } - } */ - - if (session.customWSS && "isScene" in msg && msg.isScene !== false) { - // this is a scene, so lets not show it. - } else { - var soloLink = soloLinkGenerator(session.rpcs[UUID].streamID); - createControlBoxScreenshare(UUID, soloLink, session.rpcs[UUID].streamID); - } - } - - if (session.autorecord || session.autorecordremote) { - log("AUTO RECORD START"); - setTimeout( - function (UUID, v) { - var videoKbps = session.recordDefault; - if (session.recordLocal !== false) { - videoKbps = session.recordLocal; - } - - if (session.director) { - recordVideo(document.querySelector("[data-action-type='recorder-local'][data--u-u-i-d='" + UUID + "']"), null, videoKbps); - } else if (v.stopWriter || v.recording) { - } else if (v.startWriter) { - v.startWriter(); - } else { - recordLocalVideo(null, videoKbps, v); - } - }, - 2000, - UUID, - v - ); - } - if (session.rpcs[UUID]) { - clearTimeout(session.rpcs[UUID].getStatsTimeout); - session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, 100, UUID); - } -} - -function setupIncomingVideoTracking(v, UUID) { - // video element. - - if (session.directorList.indexOf(UUID) >= 0) { - v.muted = false; - } - - v.onpause = event => { - // prevent things from pausing; human or other - - if (v.dataset.UUID && session.rpcs[v.dataset.UUID] && session.rpcs[v.dataset.UUID].manualBandwidth === 0) { - return true; - } - - if (!CtrlPressed) { - warnlog("Video paused; force it to play again"); - //return; - //session.audioCtx.resume(); - //log("ctx resume"); - - event.currentTarget - .play() - .then(_ => { - log("playing 6"); - }) - .catch(error => { - warnlog("didnt play 1"); - }); - unPauseVideo(v); - } else if (Firefox && CtrlPressed) { - log("CLICK 351"); - if (session.statsMenu !== false) { - var uid = event.currentTarget.dataset.UUID; - event.preventDefault(); - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - - printViewStats(innerMenu, uid); - - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - event.stopPropagation(); - return false; - } - return true; - }; - - /* v.onerror = function(event){ - errorlog(event); - try{ - warnlog("Vidieo element threw an error; going to reconnect it"); - session.rpcs[UUID].videoElement.stop(); - session.rpcs[UUID].videoElement.srcObject = null; - session.rpcs[UUID].videoElement.srcObject = session.rpcs[UUID].streamSrc; // replaecd with updateIncomingVideoElement these days - session.rpcs[UUID].videoElement.play(); - setTimeout(function(){updateMixer();},1); - } catch(e){errorlog(e);} - } */ - - if (session.pip) { - v.onloadedmetadata = function () { - if (!v.paused) { - if (!v.pip) { - v.pip = true; - toggleSystemPip(v, true); - } - } - }; - } - - v.addEventListener("resize", e => { - var v = e.target; - var aspectRatio = parseFloat(v.videoWidth / v.videoHeight) || 0; - log("resize event: " + aspectRatio); - - if (!aspectRatio) { - v.resetAR = true; - return; - } // if Audio only, then we don't want to set or update any aspect ratio. - if (typeof v.manualRotate == "number") { - //v.rotated = v.manualRotate; // ((session.rotate || 0) + 90) % 360; - } else if (session.keepIncomingVideosInLandscape) { - if (aspectRatio < 1) { - // session.keepIncomingVideosInLandscape - v.rotated = session.keepIncomingVideosInLandscape; - } else { - v.rotated = 0; - } - } else if (session.keepIncomingVideosInPortrait) { - if (aspectRatio > 1) { - // session.keepIncomingVideosInLandscape - v.rotated = session.keepIncomingVideosInPortrait; - } else { - v.rotated = 0; - } - } - - if (v.resetAR) { - log("ASPECT RATIO UNMUTED"); - delete v.resetAR; - v.dataset.aspectRatio = aspectRatio; - pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); - setTimeout(function () { - updateMixer(); - }, 1); - } else if (v.dataset.aspectRatio) { - if (aspectRatio != parseFloat(v.dataset.aspectRatio)) { - log("ASPECT RATIO CHANGED"); - v.dataset.aspectRatio = aspectRatio; - pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); - setTimeout(function () { - updateMixer(); - }, 1); // We don't want to run this on the first resize? just subsequent ones. - } - } else { - log("NEW VIDEO ? ASPECT RATIO new"); - v.dataset.aspectRatio = aspectRatio; - pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); - setTimeout(function () { - updateMixer(); - }, 1); - } - }); - - if (typeof session.volume == "number") { - v.volume = session.volume; - } else { - v.volume = 1.0; // play audio automatically - } - v.autoplay = true; - v.controls = session.showControls || false; - v.classList.add("tile"); - v.setAttribute("playsinline", ""); - v.controlTimer = null; - - v.dataset.menu = "context-menu-video"; - if (!session.cleanOutput) { - v.classList.add("task"); // this adds the right-click menu - } - - if (document.getElementById("mainmenu")) { - var m = getById("mainmenu"); - m.remove(); - document.querySelectorAll(".hidden2").forEach(ele2 => { - ele2.classList.remove("hidden2"); - }); - } - - if (session.director) { - if (session.showControls === false) { - v.controls = false; - } else { - v.controls = true; - } - var container = getById("videoContainer_" + UUID); - v.container = container; - v.disablePictureInPicture = false; - v.setAttribute("controls", "controls"); - container.appendChild(v); - pokeIframeAPI("control-box-video-updated", v.id, UUID); - container.classList.add("hasMedia"); - session.requestRateLimit(session.directorViewBitrate, UUID); /// limit resolution for director - v.title = "Hold CTRL or CMD (⌘) while clicking the video to open detailed stats"; - if (session.beepToNotify) { - playtone(); - } - } else if (session.scene !== false) { - v.controls = session.showControls || false; - - if (session.view) { - // specific video to be played - v.style.display = "block"; - } else if (session.scene === "0") { - // auto plays, right? - v.style.display = "block"; - } else if (session.scene !== false && session.autoadd && session.rpcs[UUID].streamID && session.autoadd.includes(session.rpcs[UUID].streamID)) { - /// session.autoadd - v.style.display = "block"; // auto added because manually added. - } else { - // group scene I guess; needs to be added manually - v.style.display = "none"; - session.rpcs[UUID].mutedStateScene = true; - } - } else if (session.roomid !== false) { - if (session.cleanOutput) { - v.controls = session.showControls || false; - } else if (session.studioSoftware) { - v.controls = session.showControls || false; - } else if (session.showControls !== null) { - v.controls = session.showControls; - } else { - v.controls = true; - } - } else { - v.style.display = "block"; - } - - v.addEventListener("click", function (e) { - // show stats of video if double clicked - log("clicked"); - try { - var uid = e.currentTarget.dataset.UUID; - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - if (session.statsMenu !== false) { - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, uid); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - e.stopPropagation(); - return false; - } else if ("prePausedBandwidth" in session.rpcs[uid]) { - unPauseVideo(e.currentTarget); - } - } catch (e) { - errorlog(e); - } - }); - - if (session.statsMenu) { - if ("stats" in session.rpcs[UUID]) { - if (getById("menuStatsBox")) { - clearInterval(getById("menuStatsBox").interval); - getById("menuStatsBox").remove(); - } - - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, UUID); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID); - } - } - - v.touchTimeOut = null; - v.touchLastTap = 0; - v.touchCount = 0; - v.addEventListener("touchend", function (event) { - if (session.disableMouseEvents) { - return; - } - - log("touched"); - - //document.ontouchup = null; - //document.onmouseup = null; - document.onmousemove = null; - document.ontouchmove = null; - - var currentTime = new Date().getTime(); - var tapLength = currentTime - v.touchLastTap; - clearTimeout(v.touchTimeOut); - if (tapLength < 500 && tapLength > 0) { - /// - log("double touched"); - v.touchCount += 1; - event.preventDefault(); - if (v.touchCount < 5) { - v.touchLastTap = currentTime; - return false; - } - v.touchLastTap = 0; - v.touchCount = 0; - - log("double touched"); - if (session.statsMenu !== false) { - var uid = event.currentTarget.dataset.UUID; - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, uid); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - event.stopPropagation(); - return false; - ////// - } else { - v.touchCount = 1; - v.touchTimeOut = setTimeout( - function (vv) { - clearTimeout(vv.touchTimeOut); - vv.touchLastTap = 0; - vv.touchCount = 0; - }, - 5000, - v - ); - v.touchLastTap = currentTime; - } - }); - - if (session.rpcs[UUID].stats.info && "remote" in session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote) { - v.addEventListener("wheel", remotePTZRequest); - // v.addEventListener("wheel", remoteFocusZoomRequest); // just remote focus -- obsolete. - } - - if (session.ptzSlider && (session.director || (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote))) { - - const ptzContainer = document.createElement('div'); - ptzContainer.className = 'video-ptz-controls'; - - // Zoom slider - const zoomSlider = document.createElement('div'); - zoomSlider.className = 'video-zoom-slider'; - - const zoomLabel = document.createElement('label'); - zoomLabel.innerText = "Zoom"; - - const zoomInput = document.createElement('input'); - zoomInput.title = "Camera zoom control"; - zoomInput.type = 'range'; - zoomInput.min = '0'; - zoomInput.max = '100'; - zoomInput.value = '0'; - - let zoomUpdating = false; - zoomInput.addEventListener('input', (e) => { - if (zoomUpdating) return; - const zoomValue = parseInt(e.target.value) / 100; // Normalize to 0-1 - - session.requestZoomChange(zoomValue, UUID, session.remote, true); - }); - - // Pan slider - const panSlider = document.createElement('div'); - panSlider.className = 'video-pan-slider'; - - const panLabel = document.createElement('label'); - panLabel.innerText = "Pan"; - - const panInput = document.createElement('input'); - panInput.title = "Camera pan control (left/right)"; - panInput.type = 'range'; - panInput.min = '-100'; - panInput.max = '100'; - panInput.value = '0'; - - let panUpdating = false; - panInput.addEventListener('input', (e) => { - if (panUpdating) return; - const panValue = parseInt(e.target.value) / 100; // Normalize to -1 to 1 - - session.requestPanChange(panValue, UUID, session.remote, true); - }); - - // Tilt slider - const tiltSlider = document.createElement('div'); - tiltSlider.className = 'video-tilt-slider'; - - const tiltLabel = document.createElement('label'); - tiltLabel.innerText = "Tilt"; - - const tiltInput = document.createElement('input'); - tiltInput.title = "Camera tilt control (up/down)"; - tiltInput.type = 'range'; - tiltInput.min = '-100'; - tiltInput.max = '100'; - tiltInput.value = '0'; - - let tiltUpdating = false; - tiltInput.addEventListener('input', (e) => { - if (tiltUpdating) return; - const tiltValue = parseInt(e.target.value) / 100; // Normalize to -1 to 1 - - session.requestTiltChange(tiltValue, UUID, session.remote, true); - }); - - // Append elements to containers - zoomSlider.appendChild(zoomLabel); - zoomSlider.appendChild(zoomInput); - - panSlider.appendChild(panLabel); - panSlider.appendChild(panInput); - - tiltSlider.appendChild(tiltLabel); - tiltSlider.appendChild(tiltInput); - - ptzContainer.appendChild(zoomSlider); - ptzContainer.appendChild(panSlider); - ptzContainer.appendChild(tiltSlider); - - if (!v.container) { - v.container = getById("videoContainer_" + UUID); - } - - v.container.appendChild(ptzContainer); - - // Store references for external updates - session.rpcs[UUID].zoomSlider = (value) => { - zoomUpdating = true; - zoomInput.value = Math.round(value * 100); // Convert 0-1 to 0-100 - zoomUpdating = false; - }; - - session.rpcs[UUID].panSlider = (value) => { - panUpdating = true; - panInput.value = Math.round(value * 100); // Convert -1 to 1 to -100 to 100 - panUpdating = false; - }; - - session.rpcs[UUID].tiltSlider = (value) => { - tiltUpdating = true; - tiltInput.value = Math.round(value * 100); // Convert -1 to 1 to -100 to 100 - tiltUpdating = false; - }; - } else if (session.zoomSlider && (session.director || (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote))) { - const slider = document.createElement('div'); - slider.className = 'video-zoom-slider0'; - - const input = document.createElement('input'); - input.title = "Hint: The remote camera's browser may needs to be visible for zoom to work in certain browsers"; - input.type = 'range'; - input.min = '0'; - input.max = '255'; - input.value = '0'; - - let updating = false; - input.addEventListener('input', (e) => { - if (updating) return; - const zoomValue = parseInt(e.target.value) / 255; - session.requestZoomChange(zoomValue, UUID, session.remote, true); - }); - - // Update slider if remote changes occur - const updateSlider = (value) => { - updating = true; - input.value = Math.round(value * 255); - updating = false; - input.title = input.value * 100 + ""; - }; - - slider.appendChild(input); - - if (!v.container) { - v.container = getById("videoContainer_" + UUID); - } - - v.container.appendChild(slider); - - // Store reference for external updates - session.rpcs[UUID].zoomSlider = updateSlider; - } - - if (v.controls == false) { - v.addEventListener("click", function () { - log("click 33"); - if (v.paused) { - log("PLAYING MANUALLY?"); - v.play() - .then(_ => { - log("playing 7"); - }) - .catch(warnlog); - } - }); - if (session.nocursor == false) { - // we do not want to show the controls. This is because MacOS + OBS does not work; so electron app needs this. - if (!session.cleanOutput) { - if (session.studioSoftware) { - } else if (session.showControls === false) { - // explicitly disabled; default null. - } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - } else { - if (v.controlTimer) { - clearInterval(v.controlTimer); - } - v.controlTimer = setTimeout(showControlBar.bind(null, v), 1000); - //v.controlTimer = setTimeout(function (){v.controls=true;},3000); // 3 seconds before I enable the controls automatically. This way it doesn't auto appear during loading. 3s enough, right? - } - } - } - } - - //if (session.fadein){ - v.addEventListener("animationend", function (e) { - v.classList.remove("fadein"); // allows the video to fade in. - if (v.holder) { - v.holder.classList.remove("fadein"); - } - }); - //v.classList.add("fadein"); // allows the video to fade in. - // if (v.holder){ - //// v.holder.classList.add("fadein"); - // } - //} - - applyMuteState(UUID); - v.usermuted = false; - - if (session.screenShareStartPaused && session.rpcs[UUID].screenShareState) { - pauseVideo(v, false); - } - - v.addEventListener("volumechange", function (e) { - var muteState = checkMuteState(UUID); - if (this.muted && this.muted !== muteState) { - this.usermuted = 1; - } else if (!this.muted && this.muted !== muteState) { - this.usermuted = 2; - } else if (!this.muted) { - this.usermuted = false; - } - }); - - if (session.autorecord || session.autorecordremote) { - log("AUTO RECORD START"); - setTimeout( - function (UUID, v) { - var videoKbps = session.recordDefault; - if (session.recordLocal !== false) { - videoKbps = session.recordLocal; - } - - if (session.director) { - recordVideo(document.querySelector("[data-action-type='recorder-local'][data--u-u-i-d='" + UUID + "']"), null, videoKbps); - } else if (v.stopWriter || v.recording) { - } else if (v.startWriter) { - v.startWriter(); - } else { - recordLocalVideo(null, videoKbps, v); - } - }, - 2000, - UUID, - v - ); - } - - if (session.rpcs[UUID]) { - clearTimeout(session.rpcs[UUID].getStatsTimeout); - session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, 100, UUID); - } -} - -session.requestPanChange = async function (pan, UUID, passwd = session.remote, absolute = false) { - // pan is now expected to be a value between -1 and 1 - log("request pan change: " + pan); - var msg = {}; - msg.pan = pan; // Normalized value -1 to 1 - msg.remote = passwd; - msg.abs = absolute; - msg = await session.encodeRemote(msg); - if (session.sendRequest(msg, UUID)) { - log("pan success"); - return true; - } else { - errorlog("failed to send pan change request"); - return false; - } -}; - -session.requestTiltChange = async function (tilt, UUID, passwd = session.remote, absolute = false) { - // tilt is now expected to be a value between -1 and 1 - log("request tilt change: " + tilt); - var msg = {}; - msg.tilt = tilt; // Normalized value -1 to 1 - msg.remote = passwd; - msg.abs = absolute; - msg = await session.encodeRemote(msg); - if (session.sendRequest(msg, UUID)) { - log("tilt success"); - return true; - } else { - errorlog("failed to send tilt change request"); - return false; - } -}; - -session.requestZoomChange = async function (zoom, UUID, passwd = session.remote, absolute = false) { - // zoom is now expected to be a value between 0 and 1 - log("request zoom change: " + zoom); - var msg = {}; - msg.zoom = zoom; // Normalized value 0 to 1 - msg.abs = absolute; - msg.remote = passwd; - msg = await session.encodeRemote(msg); - if (session.sendRequest(msg, UUID)) { - log("zoom success"); - return true; - } else { - errorlog("failed to send zoom change request"); - return false; - } -}; - -session.requestFocusChange = async function (focal, UUID, passwd = session.remote, absolute = false) { - log("request focus change: " + focal); - - var msg = {}; - msg.focus = focal; - msg.abs = absolute; - msg.remote = passwd; - msg = await session.encodeRemote(msg); - - if (session.sendRequest(msg, UUID)) { - log("focus success"); - } else { - errorlog("failed to send focus change request"); - } -}; - -session.requestAutofocusChange = async function (enabled, UUID, passwd = session.remote) { - log("request autofocus change: " + enabled); - - var msg = {}; - msg.autofocus = enabled; - msg.remote = passwd; - msg = await session.encodeRemote(msg); - - if (session.sendRequest(msg, UUID)) { - log("autofocus request success"); - } else { - errorlog("failed to send autofocus change request"); - } -}; - -function remotePTZRequest(event) { - event.preventDefault(); - - var scale = event.deltaY > 0 ? -0.05 : 0.05; // Use larger normalized steps - - if (!event.altKey) { - scale *= 2; // Double the scale when not holding Alt - } - - if (event.ctrlKey || event.metaKey) { - if (event.shiftKey) { - // tilt: -1 to 1 - session.requestTiltChange(scale, event.currentTarget.dataset.UUID); - } else { - // focus: -1 to 1 - session.requestFocusChange(scale, event.currentTarget.dataset.UUID); - } - } else if (event.shiftKey) { - // pan: -1 to 1 - session.requestPanChange(scale, event.currentTarget.dataset.UUID); - } else { - // zoom: 0 to 1 (relative) - session.requestZoomChange(scale, event.currentTarget.dataset.UUID); - } -} - -function remoteFocusZoomRequest(event) { // obsolete. - event.preventDefault(); - - var scale = event.deltaY > 0 ? -0.004 : 0.004; - log(event.currentTarget); - log(event.deltaY); - - if (!event.altKey) { - scale *= 10; - } - - if (event.ctrlKey || event.metaKey) { - // focus - session.requestFocusChange(scale, event.currentTarget.dataset.UUID); - } else { - // zoom - session.requestZoomChange(scale, event.currentTarget.dataset.UUID); - } -} - -function mediaAudioTrackUpdated(UUID, streamID) { - pokeIframeAPI("new-audio-track-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess. -} -function mediaVideoTrackUpdated(UUID, streamID) { - pokeIframeAPI("new-video-track-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess. -} -function mediaSourceUpdated(UUID, streamID) { - pokeIframeAPI("new-stream-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess. - pokeAPI("streamAdded", streamID); -} - -function showControlBar(vel) { - try { - vel.controls = true; - } catch (e) { - errorlog(e); - } -} - -function createRichVideoElement(UUID) { - // this function is used to check and generate a rich video element if needed - if (!session.rpcs[UUID].videoElement) { - log("video element is being created and any media tracks added"); - session.rpcs[UUID].videoElement = createVideoElement(); - session.rpcs[UUID].videoElement.dataset.UUID = UUID; - session.rpcs[UUID].videoElement.id = "videosource_" + UUID; // could be set to UUID in the future - - if (session.rpcs[UUID].streamID) { - session.rpcs[UUID].videoElement.dataset.sid = session.rpcs[UUID].streamID; - } - - if (session.rpcs[UUID].rotate !== false) { - session.rpcs[UUID].videoElement.rotated = session.rpcs[UUID].rotate; - session.rpcs[UUID].videoElement.dataset.rotated = session.rpcs[UUID].rotate; - updateVideoTransform(session.rpcs[UUID].videoElement); - } - - session.rpcs[UUID].videoElement.addEventListener( - "playing", - e => { - try { - var bigPlayButton = document.getElementById("bigPlayButton"); - if (bigPlayButton) { - bigPlayButton.parentNode.removeChild(bigPlayButton); - } - } catch (e) { } - - resetupAudioOut(e.target, true); - - try { - if (session.pip) { - if (v.readyState >= 3) { - if (!v.pip) { - v.pip = true; - toggleSystemPip(v, true); - } - } - } - } catch (e) { } - }, - { once: true } - ); - - if (session.rpcs[UUID].mirrorState !== null || session.rpcs[UUID].flipState !== null) { - applyMirrorGuest( - !!session.rpcs[UUID].mirrorState, - session.rpcs[UUID].videoElement, - session.rpcs[UUID].flipState !== null ? !!session.rpcs[UUID].flipState : undefined - ); - } - - if (session.posterImage) { - session.rpcs[UUID].videoElement.poster = session.posterImage; - } - - setupIncomingVideoTracking(session.rpcs[UUID].videoElement, UUID); - pokeIframeAPI("video-element-created", "videosource_" + UUID, UUID); - } - return session.rpcs[UUID].videoElement; -} - -function updateVolume(update = false) { - if (session.audioGain !== false) { - if (update) { - if (session.roomid) { - var pswd = session.password || ""; - generateHash(session.streamID + session.roomid + pswd + session.salt, 6).then(function (hash) { - setStorage("micVolume_" + hash, session.audioGain, (hours = 6)); - }); - } - } - if (session.audioGain === 0) { - getById("header").classList.add("orange"); - getById("head7").classList.remove("hidden"); - } else { - getById("header").classList.remove("orange"); - getById("head7").classList.add("hidden"); - } - } else { - var pswd = session.password || ""; - generateHash(session.streamID + session.roomid + pswd + session.salt, 6).then(function (hash) { - var volume = getStorage("micVolume_" + hash); - if (volume !== "") { - if (parseInt(volume) === 0) { - getById("header").classList.add("orange"); - getById("head7").classList.remove("hidden"); - } else if (parseInt(volume)) { - getById("header").classList.remove("orange"); - getById("head7").classList.add("hidden"); - } else { - return; - } - session.audioGain = parseInt(volume); - var vol = parseFloat(session.audioGain / 100) || 0; - for (var waid in session.webAudios) { - // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (trackid === wa.id){.. - log("Adjusting Gain; only track 0 in all likely hood, unless more than track 0 support is added."); - session.webAudios[waid].gainNode.gain.setValueAtTime(vol, session.webAudios[waid].audioContext.currentTime); - } - } - }); - } -} - -function hideHomeCheck() { - if (session.hidehome) { - getById("logoname").classList.add("permahide"); - getById("container-1").classList.add("permahide"); - getById("container-4").classList.add("permahide"); - getById("dropButton").classList.add("permahide"); - getById("head1").classList.add("permahide"); - if (session.permaid === false && session.roomid == false && !session.webcamonly && !session.screenshare) { - getById("mainmenu").classList.add("permahide"); - } else { - getById("mainmenu").classList.remove("permahide"); - } - - getById("audioScreenCaptureDocs").classList.add("permahide"); - getById("audioScreenCaptureDocs2").classList.add("permahide"); - getById("translateButton").classList.add("permahide"); - // getById("legal").classList.add("permahide"); - getById("calendarButton").classList.add("permahide"); - getById("info").classList.add("permahide"); - getById("helpbutton").classList.add("permahide"); - } - - if (urlParams.has("headertitle")) { - let pageTitle = urlParams.get("headertitle") || ""; - pageTitle = decodeURIComponent(pageTitle) || ""; - document.title = pageTitle; - getById("metaTitle").content = pageTitle; - } - - if (urlParams.has("favicon")) { - let favicon = ""; - if (urlParams.get("favicon")) { - favicon = decodeURIComponent(urlParams.get("favicon")) || ""; - } - getById("favicon1").href = favicon; - getById("favicon2").href = favicon; - getById("favicon3").href = favicon; - } -} - -function stashRoomSession(broadcastFlag = null) { - try { - let settings = {}; - - settings.roomid = session.roomid; - settings.password = session.password; - settings.label = session.label; - settings.trb = session.totalRoomBitrate; - settings.widget = session.widget; - settings.codecGroupFlag = session.codecGroupFlag; - settings.showDirector = session.showDirector; - - if (broadcastFlag !== null) { - settings.broadcast = broadcastFlag; - } - - setStorage("directorOtherSettings", settings); - } catch (e) { - errorlog(e); - } -} - -// toggleQualityDirector(1200, this.dataset.UUID, this) - -function switchModes(state = null) { - if (state === null) { - session.switchMode = !session.switchMode; - } else { - session.switchMode = state; - } - if (session.switchMode) { - getById("directorlayout").classList.add("hidden"); - getById("gridlayout").classList.remove("hidden"); - updateMixer(); - } else { - getById("directorlayout").classList.remove("hidden"); - getById("gridlayout").classList.add("hidden"); - - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement) { - session.rpcs[UUID].videoElement.style = ""; - session.rpcs[UUID].videoElement.alreadyAdded = false; - var target = document.querySelector("#container_" + UUID + " .controlVideoBox"); - if (target) { - target.prepend(session.rpcs[UUID].videoElement); - if (session.signalMeter) { - if (session.rpcs[UUID].signalMeter) { - target.appendChild(session.rpcs[UUID].signalMeter); - } - } - if (session.batteryMeter) { - if (session.rpcs[UUID].batteryMeter) { - target.appendChild(session.rpcs[UUID].batteryMeter); - } - } - if (session.rpcs[UUID].voiceMeter) { - target.appendChild(session.rpcs[UUID].voiceMeter); - } - if (session.rpcs[UUID].remoteMuteElement) { - target.appendChild(session.rpcs[UUID].remoteMuteElement); - } - } - } - } - - if (session.videoElement) { - session.videoElement.style = ""; - session.videoElement.alreadyAdded = false; - - if (session.showDirector == true) { - var target = document.querySelector("#videoContainer_director"); - if (target && session.videoElement) { - target.prepend(session.videoElement); - } - } else if ((session.videoElement.srcObject && session.videoElement.srcObject.getTracks().length) || getById("press2talk").dataset.enabled == true) { - getById("miniPerformer").prepend(session.videoElement); - } - } - - if (session.screenShareElement) { - session.screenShareElement.style = ""; - session.screenShareElement.alreadyAdded = false; - - if (session.showDirector == true) { - var target = document.querySelector("#videoScreenContainer_director"); - if (target && session.screenShareElement && session.screenShareElement.srcObject && session.screenShareElement.srcObject.getTracks().length) { - target.prepend(session.screenShareElement); - } - } else if ((session.screenShareElement.srcObject && session.screenShareElement.srcObject.getTracks().length) || getById("press2talk").dataset.enabled == true) { - getById("miniPerformer").prepend(session.videoElement); - } - } - - applyQualityDirector(); - } -} - -var updateMixerTimer = null; -var updateMixerActive = false; -function updateMixer(e = false) { - var controlBar = document.getElementById("subControlButtons"); - if (controlBar && controlBar.dragElement && !controlBar.isDragging) { - let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); - let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); - - // Calculate real boundaries (parent position: fixed issues) - let topOffset = 0; - let leftOffset = 0; - let elementOffset = controlBar; - while (elementOffset) { - topOffset += elementOffset.offsetTop; - leftOffset += elementOffset.offsetLeft; - elementOffset = elementOffset.offsetParent; - } - - let realX = controlBar.xOffset + leftOffset; - let realY = controlBar.yOffset + topOffset; - let maxX = vw - controlBar.offsetWidth; - let maxY = vh - controlBar.offsetHeight; - - if (realX > maxX) { - controlBar.xOffset = maxX - leftOffset; - } else if (realX < 0) { - controlBar.xOffset = 0 - leftOffset; - } - - if (realY > maxY) { - controlBar.yOffset = maxY - topOffset; - } else if (realY < 0) { - controlBar.yOffset = 0 - topOffset; - } - - controlBar.style.transform = `translate(${controlBar.xOffset}px, ${controlBar.yOffset}px)`; - } - - if (session.manual === true) { - return; - } else if (!session.switchMode && session.director) { - return; - } else if (session.windowed) { - return; - } - - clearInterval(updateMixerTimer); - if (updateMixerActive) { - if (session.mobile) { - updateMixerTimer = setTimeout(function () { - updateMixer(); - }, 200); - } else { - updateMixerTimer = setTimeout(function () { - updateMixer(); - }, 50); - } - return; - } - updateMixerActive = true; - log("updating mixer"); - - try { - updateMixerRun(e); - } catch (e) { } - - if (session.mobile) { - setTimeout(function () { - updateMixerActive = false; - }, 500); - } else { - setTimeout(function () { - updateMixerActive = false; - }, 100); - } -} - -function updateMixerRun(e = false) { - // this is the main auto-mixing code. It's a giant function that runs when there are changes to screensize, video track statuses, etc. - try { - if (session.switchMode) { - } else if (session.director) { - return; - } else if (session.manual === true) { - return; - } - - var header = getById("header"); - var playarea = getById("gridlayout"); - - if (session.pipWindow) { - var hi = 0; - var h = session.pipWindow.clientHeight || session.pipWindow.innerHeight || session.pipWindow.outerHeight; - var w = session.pipWindow.clientWidth || session.pipWindow.innerWidth || session.pipWindow.outerWidth; - } else if (document.body.dataset.rotated) { - var hi = header.offsetHeight; - var w = document.body.clientHeight; - - if (session.widget && session.iFramesAllowed) { - w *= (100 - session.widgetwidth) / 100; - try { - let widget = document.getElementById("widget"); - if (!widget) { - widget = document.createElement("iframe"); - widget.id = "widget"; - widget = loadIframe(parseURL4Iframe(session.widget), widget); - if (widget) { - document.body.appendChild(widget); - if (session.widgetleft) { - widget.classList.add("left"); - playarea.style.left = session.widgetwidth + "%"; - playarea.style.width = (100 - session.widgetwidth) + "%"; - } else { - playarea.style.left = "0"; - playarea.style.width = (100 - session.widgetwidth) + "%"; - } - } - } - if (widget) { - widget.style.height = "calc(100% - " + hi + "px)"; - widget.style.top = hi; - } - } catch (e) { - errorlog(e); - } - } else if (!session.widget && session.widgetleft) { - playarea.style.left = "0"; - } - - var h = document.body.clientWidth - hi; - if (session.dedicatedControlBarSpace || document.body.clientWidth <= 700) { - // # This needs to be reviewed. - if (session.dedicatedControlBarSpace !== false) { - let controlBar = document.getElementById("subControlButtons"); - if (controlBar && !session.overlayControls) { - if (!controlBar.yOffset || controlBar.yOffset > -10) { - h = document.body.clientWidth - hi - controlBar.offsetHeight; - } - } - } - } - } else { - var hi = header.offsetHeight; - var w = window.innerWidth; - - if (session.widget && session.iFramesAllowed) { - w *= (100 - session.widgetwidth) / 100; - try { - let widget = document.getElementById("widget"); - if (!widget) { - widget = document.createElement("iframe"); - widget.id = "widget"; - widget = loadIframe(parseURL4Iframe(session.widget), widget); - if (widget) { - document.body.appendChild(widget); - if (session.widgetleft) { - widget.classList.add("left"); - playarea.style.left = session.widgetwidth + "%"; - playarea.style.width = (100 - session.widgetwidth) + "%"; - playarea.style.position = "absolute"; - } else { - playarea.style.left = "0"; - playarea.style.width = (100 - session.widgetwidth) + "%"; - } - } - } - if (widget) { - widget.style.height = "calc(100% - " + hi + "px)"; - widget.style.top = hi; - } - } catch (e) { - errorlog(e); - } - } else if (!session.widget && session.widgetleft) { - playarea.style.left = "0"; - } - - var h = window.innerHeight - hi; - if (session.dedicatedControlBarSpace || window.innerHeight <= 700) { - // # This needs to be reviewed. - if (session.dedicatedControlBarSpace !== false) { - let controlBar = document.getElementById("subControlButtons"); - if (controlBar && !session.overlayControls) { - if (!controlBar.yOffset || controlBar.yOffset > -10) { - h = window.innerHeight - hi - controlBar.offsetHeight; - } - } - } - } - } - - if (session.locked) { - var w123 = w; - var h123 = h; - - if (w > h * session.locked) { - w = h * session.locked; - } else if (h > w / session.locked) { - h = w / session.locked; - } - - playarea.style.left = (w123 - w) / 2 + "px"; - playarea.style.top = (h123 - h) / 2 + "px"; - playarea.style.width = w + "px"; - playarea.style.height = h + "px"; - playarea.style.position = "absolute"; - playarea.style.display = "block"; - } - - var arW = 16.0; - var arH = 9.0; - - if (session.aspectRatio) { - if (session.aspectRatio == 1) { - arW = 9.0; - arH = 16.0; - } else if (session.aspectRatio == 2) { - arW = 12.0; // square root; cause why not. - arH = 12.0; - } else if (session.aspectRatio == 3) { - arW = 12.0; // square root; cause why not. - arH = 9.0; - } - } - - var groups = [...session.group]; - if (session.groupView.length) { - groups.push(...session.groupView); - } - var sssid = false; - var soloVideo = false; - - if (session.infocus === true) { - soloVideo = true; - } else if (session.infocus && session.infocus in session.rpcs) { - // if the infocus stream is connected - if (groups.length || session.allowNoGroup) { - try { - if (groups.some(item => session.rpcs[session.infocus].group.includes(item))) { - soloVideo = session.infocus; - } - } catch (e) { - errorlog(e); - } - } else { - soloVideo = session.infocus; - } - } else if (session.infocus2 === true) { - sssid = session.streamID; - } else if (session.infocus2 && session.infocus2 in session.rpcs) { - // if the infocus2 stream is connected - if (groups.length || session.allowNoGroup) { - try { - if (groups.some(item => session.rpcs[session.infocus2].group.includes(item))) { - sssid = session.rpcs[session.infocus2].streamID; - } - } catch (e) { - errorlog(e); - } - } else { - sssid = session.rpcs[session.infocus2].streamID; - } - } - - var ww = w / arW; - var hh = h / arH; - - var mediaPool = []; - var mediaPool_invisible = []; - - var miniPreview = session.minipreview; - - if (miniPreview && (!session.activeSpeaker && session.layout) && session.streamID && session.streamID in session.layout) { - miniPreview = false; - if (session.videoElement.container && session.videoElement.container.id == "minipreview") { - delete session.videoElement.container; - } - if (document.getElementById("minipreview")) { - document.getElementById("minipreview").remove(); - } - } - - if (session.iframeEle && session.iframeEle.style.display !== "none") { - // local feed - if (session.order !== false) { - session.iframeEle.order = session.order; - } else { - session.iframeEle.order = 0; - } - if (session.activeSpeaker && !session.activelySpeaking) { - mediaPool_invisible.push(session.iframeEle); - } else { - mediaPool.push(session.iframeEle); - } - } - - if (session.videoElement && (session.videoElement.src || session.videoElement.srcObject)) { - // I, myself, exist - if (session.videoElement.style.display !== "none") { - // local feed - if (miniPreview && soloVideo !== true) { - - } else { - if (session.order !== false) { - session.videoElement.order = session.order; - } else { - session.videoElement.order = 0; - } - if (session.activeSpeaker && !session.activelySpeaking) { - //mediaPool_invisible.push(session.videoElement); - //} else if (session.videoElement && session.videoElement.srcObject && (session.videoElement.srcObject.getTracks().length === 0)){ - // do not show a video element if its completely empty. - } else if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length === 0) { - // do not show a video element if its completely empty. - } else if (soloVideo && soloVideo !== true) { - // - } else if (session.videoMuted && session.style === 1) { - // i'm too tired to try to get this working. - } else { - mediaPool.push(session.videoElement); - } - } - } - } - - if (session.screenShareState && session.screenShareElement) { - // I, myself, exist - if (!session.screenShareElementHidden) { - if (session.order !== false) { - session.screenShareElement.order = session.order; - } else { - session.screenShareElement.order = 0; - } - - if (soloVideo !== false) { - //session.screenShareElement.style.display="none"; - } else if (session.activeSpeaker && !session.activelySpeaking) { - //session.screenShareElement.style.display="none"; - } else if (!session.noScreenShare) { - mediaPool.push(session.screenShareElement); - } else { - session.screenShareElement.style.display = "none"; - } - } - } - - var delayedRequestList = {}; - - function delayedRequestRate(bandwidth, UUID, optimizeAudio = false, lock = null) { - delayedRequestList[UUID] = [bandwidth, UUID, optimizeAudio, lock]; - } - - if (soloVideo && soloVideo in session.rpcs) { // this technically can be a scene or guest - // remote guest being full screened; infocus == UUID - mediaPool = []; // remove myself from fullscreen - - if (iOS || iPad) { - if (!miniPreview) { - miniPreview = 1; - } - } - - for (var j in session.rpcs) { - if (groups.length || session.allowNoGroup) { - try { - if (!groups.some(item => session.rpcs[j].group.includes(item))) { - continue; - } - } catch (e) { - errorlog(e); - } - } - - if (j != soloVideo) { - // this remote guest is NOT in focus - try { - if (session.rpcs[j].iframeEle) { - mediaPool_invisible.push(session.rpcs[j].iframeEle); - } - if (session.rpcs[j].videoElement && session.rpcs[j].videoElement.style.display !== "none") { - // Add it if not hidden - if (session.scene !== false) { - //delayedRequestRate(session.hiddenSceneViewBitrate, j); - } else { - if (document.pictureInPictureElement && document.pictureInPictureElement.id && document.pictureInPictureElement.id == session.rpcs[j].videoElement.id) { - var bitratePIP = parseInt(session.zoomedBitrate / 4); - //warnUser("GOOD"); - delayedRequestRate(bitratePIP, j); - } else { - delayedRequestRate(0, j); // disable the video of non-fullscreen videos - } - } - } else if (session.rpcs[j].videoElement) { - delayedRequestRate(0, j, true); // disable the video of non-fullscreen videos - } - } catch (e) { - errorlog(e); - } - } else { - // remote guest is in-focus video - //////// - try { - if (session.rpcs[j].iframeEle) { - mediaPool_invisible.push(session.rpcs[j].iframeEle); - } - if (session.rpcs[j].videoElement) { - mediaPool.push(session.rpcs[j].videoElement); // active speaker - session.rpcs[j].videoElement.style.visibility = "visible"; - if (session.rpcs[j].order !== false) { - session.rpcs[j].videoElement.order = session.rpcs[j].order; - } else { - session.rpcs[j].videoElement.order = 0; - } - if (session.scene !== false) { - - } else { - var totalRoomBitrate = session.totalRoomBitrate; - if (session.controlRoomBitrate !== false && session.controlRoomBitrate !== true) { - totalRoomBitrate = Math.min(session.controlRoomBitrate, totalRoomBitrate); - } - var targetBitrate = session.zoomedBitrate; - if (totalRoomBitrate > session.zoomedBitrate) { - targetBitrate = totalRoomBitrate; - } - delayedRequestRate(targetBitrate, j); // 1.2-Mbps is decent, no? in-focus, so higher bitrate - } - } - } catch (e) { - errorlog(e); - } - } - } - } else if (soloVideo && soloVideo === true) { // this cannot be a scene, as you can't have yourself in a scene. - // well, fullscreen myself. "true" represents me. UUID would be for others. - // already added myself to this as fullscreen - for (var j in session.rpcs) { - if (groups.length || session.allowNoGroup) { - try { - if (!groups.some(item => session.rpcs[j].group.includes(item))) { - continue; - } - } catch (e) { - errorlog(e); - } - } - try { - if (session.rpcs[j].videoElement && session.rpcs[j].videoElement.style.display !== "none") { - // Add it if not hidden - if (document.pictureInPictureElement && document.pictureInPictureElement.id && document.pictureInPictureElement.id == session.rpcs[j].videoElement.id) { - var bitratePIP = parseInt(session.zoomedBitrate / 4); - delayedRequestRate(bitratePIP, j); - //warnUser("GOOD"); - } else { - delayedRequestRate(0, j); // disable the video of non-fullscreen videos - } - // mediaPool_invisible.push(session.rpcs[j].videoElement); - } else if (session.rpcs[j].videoElement) { - delayedRequestRate(0, j, true); // other videos are disabled when previewing yourself, but audio retained - } - } catch (e) { - errorlog(e); - } - } - } else { - var roomQuality = 0; - var screenShareTotal = 0; - - for (var i in session.rpcs) { - if (session.rpcs[i] === null) { - continue; - } - if (groups.length || session.allowNoGroup) { - try { - if (!groups.some(item => session.rpcs[i].group.includes(item))) { - continue; - } - } catch (e) { - errorlog(e); - } - } - if (session.rpcs[i].videoElement) { - // remote feeds - if (session.rpcs[i].videoElement.style.display !== "none") { - if (session.rpcs[i].videoElement.srcObject && session.rpcs[i].videoElement.srcObject.getVideoTracks().length) { - // only count videos with actual video tracks; audio-only excluded - if (session.rpcs[i].videoMuted) { - // it's video muted - // mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on - } else if (session.rpcs[i].directorVideoMuted) { - // it's muted by the director, so likely disabled. - // mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on - } else if (session.rpcs[i].virtualHangup) { - } else if (session.rpcs[i].bandwidthMuted) { - } else if (session.rpcs[i].videoElement.style.opacity === "0") { - // mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on - } else { - roomQuality += 1; - if (session.rpcs[i].screenShareState) { - screenShareTotal += 1; - } - } - } - } - } - } - - if (session.broadcast !== false) { - if ((!session.activeSpeaker && session.layout) && session.streamID in session.layout) { - // skip - } else { - if (roomQuality > 0) { - if (session.nopreview !== false) { - for (var i = 0; i < mediaPool.length; i++) { - if (mediaPool[i].nodeName && mediaPool[i].nodeName == "IFRAME") { - mediaPool[i].style.display = "none"; - } - } - mediaPool = []; // we don't want to show our self-preview if in broadcast mode and there is a director. - } - } - } - } - - if (roomQuality === 0) { - roomQuality = 1; - } - - var totalRoomBitrate = session.totalRoomBitrate; - - if (session.controlRoomBitrate !== false && session.controlRoomBitrate !== true) { - totalRoomBitrate = Math.min(session.controlRoomBitrate, totalRoomBitrate); - } - - var roomBitrate = totalRoomBitrate; - var sceneBitrate = false; - var screenShareBitrate = false; - - if (session.bitrate && !roomBitrate && !session.totalSceneBitrate) { - roomBitrate = session.bitrate; - } else if (session.screenShareBitrate !== false) { - screenShareBitrate = session.screenShareBitrate; - if (roomQuality - screenShareTotal > 0) { - roomBitrate = parseInt(totalRoomBitrate / (roomQuality - screenShareTotal)); - if (session.totalSceneBitrate) { - sceneBitrate = parseInt(session.totalSceneBitrate / (roomQuality - screenShareTotal)); - if (session.bitrate !== false) { - sceneBitrate = Math.min(session.bitrate, sceneBitrate); - } - } - } - } else if (screenShareTotal) { - try { - if (session.roomid !== false && session.scene === false) { - if (roomQuality - screenShareTotal <= 0) { - roomBitrate = totalRoomBitrate; - screenShareBitrate = totalRoomBitrate; - } else { - screenShareBitrate = totalRoomBitrate / (1.5 * screenShareTotal); - roomBitrate = parseInt((totalRoomBitrate - screenShareBitrate) / (roomQuality - screenShareTotal)); - } - } else if (session.totalSceneBitrate !== false) { - if (roomQuality - screenShareTotal <= 0) { - sceneBitrate = session.totalSceneBitrate; - if (session.bitrate !== false) { - sceneBitrate = Math.min(session.bitrate, sceneBitrate); - } - screenShareBitrate = sceneBitrate; - } else { - screenShareBitrate = parseInt(totalRoomBitrate / (1.5 * screenShareTotal)); - sceneBitrate = parseInt((totalRoomBitrate - screenShareBitrate) / (roomQuality - screenShareTotal)); - if (session.bitrate !== false) { - sceneBitrate = Math.min(session.bitrate, sceneBitrate); - screenShareBitrate = Math.min(session.bitrate, screenShareBitrate); - } - } - } else { - screenShareBitrate = false; - } - } catch (e) { - errorlog(e); - } - } else { - roomBitrate = parseInt(totalRoomBitrate / roomQuality); - if (session.totalSceneBitrate) { - sceneBitrate = parseInt(session.totalSceneBitrate / roomQuality); - if (session.bitrate !== false) { - sceneBitrate = Math.min(session.bitrate, sceneBitrate); - } - } - } - - if (session.minimumRoomBitrate) { - if (session.totalRoomBitrate && roomBitrate < session.minimumRoomBitrate) { - roomBitrate = session.minimumRoomBitrate; - if (roomBitrate > session.totalRoomBitrate) { - roomBitrate = session.totalRoomBitrate; - } - } - if (session.totalSceneBitrate && sceneBitrate < session.minimumRoomBitrate) { - sceneBitrate = session.minimumRoomBitrate; - if (sceneBitrate > session.totalSceneBitrate) { - sceneBitrate = session.totalSceneBitrate; - } - } - } - - var i = null; - var countOrder = 0; - try { - var RPCSkeys = Object.keys(session.rpcs); // default sorting type: time added; //RPCSkeys.sort(); - } catch (e) { - return; - } - - for (var keyIndex = 0; keyIndex < RPCSkeys.length; keyIndex++) { - i = RPCSkeys[keyIndex]; - if (session.rpcs[i] === null) { - continue; - } - session.rpcs[i].mutedStateMixer = false; - if (groups.length || session.allowNoGroup) { - // The MAIN and LAST group filter. - try { - if (!groups.some(item => session.rpcs[i].group.includes(item))) { - if (session.scene !== false) { - if (session.groupAudio) { - delayedRequestRate(session.hiddenSceneViewBitrate, i, false); - } else { - delayedRequestRate(session.hiddenSceneViewBitrate, i, true); // hidden. I dont want it to be super low, for video quality reasons. - session.rpcs[i].mutedStateMixer = true; - } - if (!session.hiddenSceneViewBitrate) { - session.rpcs[i].videoElement.nogb = 2; - } - } else { - if (session.groupAudio) { - delayedRequestRate(0, i, false); - } else { - delayedRequestRate(0, i, true); // w/e This is not in OBS, so we just set it as low as possible. Shoudln't exist really unless loading? - session.rpcs[i].mutedStateMixer = true; - } - } - applyMuteState(i); - continue; - } - } catch (e) { } - } - applyMuteState(i); - var doNotPush = false; - - var isScreenShareFeed = false; - try { - if ("realUUID" in session.rpcs[i]) { - isScreenShareFeed = true; - } else if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.dataset && session.rpcs[i].videoElement.dataset.sid) { - isScreenShareFeed = session.rpcs[i].videoElement.dataset.sid.endsWith(":s"); - } - } catch (e) { } - - if (session.noScreenShare && isScreenShareFeed) { - doNotPush = true; - if (session.rpcs[i].videoElement) { - session.rpcs[i].videoElement.style.display = "none"; - } - } - - if (session.rpcs[i].iframeEle) { - if (session.rpcs[i].iframeEle.style.display == "none") { - // pass - } else if (session.rpcs[i].iframeEle.style.opacity === "0") { - // pass - } else { - session.rpcs[i].iframeEle.style.visibility = "visible"; - if (session.rpcs[i].order !== false) { - session.rpcs[i].iframeEle.order = session.rpcs[i].order; - } else { - session.rpcs[i].iframeEle.order = 0; - } - try { - if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { - mediaPool_invisible.push(session.rpcs[i].iframeEle); // TODO: this needs validation; will the iframe be maintained if activer speaker is going? do we even want this? - /* } else if (session.rpcs[i].iframeEle.dataset.meshcast){ //////// MESH CAST ONLY LOGIC - if (session.rpcs[i].iframeEle.contentDocument && session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){ - if (session.rpcs[i].iframeVideo){ - mediaPool.push(session.rpcs[i].iframeVideo); - } else if (session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){ - session.rpcs[i].iframeVideo = session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video")[0]; - session.rpcs[i].iframeVideo.id="meshcast_"+i; - //errorlog("THIS IS GOOD"); - mediaPool.push(session.rpcs[i].iframeVideo); - } else { - //errorlog("No video yet"); - } - } else { // this is a problem is not on the same domain. - if (!document.getElementById("iframe_"+i)){ - if (document.getElementById("hiddenElements")){ - document.getElementById("hiddenElements").append(session.rpcs[i].iframeEle); - } else { - document.body.append(session.rpcs[i].iframeEle); - } - if (session.rpcs[i].iframeVideo){ - mediaPool.push(session.rpcs[i].iframeVideo); - } else if (session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){ - session.rpcs[i].iframeVideo = session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video")[0]; - session.rpcs[i].iframeVideo.id="meshcast_"+i; - mediaPool.push(session.rpcs[i].iframeVideo); - } else { - //errorlog("No video yet"); - } - } else { - if (session.rpcs[i].iframeVideo){ - mediaPool.push(session.rpcs[i].iframeVideo); - } else { - //errorlog("Does not support contentDocument or something"); - } - } - } */ - } else { - ///////// MESH CAST LOGIC ENDS HERE - //errorlog("not meshcast"); - mediaPool.push(session.rpcs[i].iframeEle); - } - } catch (e) { - errorlog(e); - } - } - } - - if (session.rpcs[i].imageElement) { - //if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.srcObject.getAudioTracks().length) { - // is there audio? - // mediaPool_invisible.push(session.rpcs[i].videoElement); // include audio as hidden track; - //} - - if (session.rpcs[i].videoMuted || session.rpcs[i].directorVideoMuted || session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted) { - continue; - } - - if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.style.display == "none") { - // currently this is considered the state of scenes. pertty dumb on my part. - continue; - } - - if (session.rpcs[i].order !== false) { - session.rpcs[i].imageElement.order = session.rpcs[i].order; - } else { - session.rpcs[i].imageElement.order = 0; - } - if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { - // mediaPool_invisible.push(session.rpcs[i].imageElement); - } else { - mediaPool.push(session.rpcs[i].imageElement); - } - - doNotPush = true; - } - - if (session.rpcs[i].videoElement) { - // remote feeds - //session.rpcs[i].targetBandwidth = -1; - if (session.rpcs[i].videoElement.style.opacity === "0") { - continue; - } - try { - session.rpcs[i].videoElement.style.visibility = "visible"; - } catch (e) { - errorlog(e); - } - - if (session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted || session.rpcs[i].directorVideoMuted) { - continue; - } - - if (session.style && session.style >= 2) { - if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) { - if (session.rpcs[i].videoElement.style.display == "none") { - // currently this is considered the state of scenes. pertty dumb on my part. - continue; - } - if (createStyleCanvas(i)) { - applyStyleEffect(i); - } - if (session.rpcs[i].order !== false) { - session.rpcs[i].canvas.order = session.rpcs[i].order; - } else { - session.rpcs[i].canvas.order = 0; - } - if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { - // mediaPool_invisible.push(session.rpcs[i].canvas); - } else { - mediaPool.push(session.rpcs[i].canvas); - } - doNotPush = true; - //continue; - } - } else if (session.style == 1) { - if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) { - //if (session.style==1){ // avatars and waveforms might be better done elsewhere? as a canvas effect even? - doNotPush = true; - //} - } - } else if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) { - if (session.rpcs[i].screenShareState) { - doNotPush = true; - } - } - //} else if (!session.directorList.indexOf(i)>=0){ // director is never audio-only. Video if need, yes, but not visualized-audio. - // if (session.rpcs[i].videoElement.srcObject && ((session.rpcs[i].videoElement.srcObject.getVideoTracks().length==0) || (session.rpcs[i].videoMuted)) && !session.rpcs[i].directorVideoMuted){ - // continue; - // } - //} - - session.rpcs[i].opacityMuted = "1"; - if (session.rpcs[i].opacityDisconnect == "1") { - if (session.rpcs[i].videoElement) { - session.rpcs[i].videoElement.style.opacity = "1"; - } - } - if (session.rpcs[i].videoMuted) { - if (session.rpcs[i].videoElement.srcObject.getAudioTracks().length == 0) { - // if no audio track, no point in removing the video track, since it will just stall out then. - continue; // easiest is to just not show anything if no video and no audio track. - } - if (session.rpcs[i].videoElement.srcObject) { - session.rpcs[i].videoElement.srcObject.getVideoTracks().forEach(track => { - log("remove track3"); - session.rpcs[i].videoElement.srcObject.removeTrack(track); - session.rpcs[i].videoElement.load(); - }); - } - //continue; // currently disabling this, since we want to show it. - } else if (session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted || session.rpcs[i].directorVideoMuted) { - continue; - } - - if (session.scene !== false) { - if (session.sceneType === 3) { - // order - countOrder += 1; - if (session.order === false) { - if (countOrder == 1) { - session.rpcs[i].videoElement.style.display = "block"; - } else { - session.rpcs[i].videoElement.style.display = "none"; - } - } else if (session.order === countOrder) { - session.rpcs[i].videoElement.style.display = "block"; - } else { - session.rpcs[i].videoElement.style.display = "none"; - } - } - } - - if (session.rpcs[i].videoElement.style.display == "none") { - // Video is disabled; run at lowest - if (session.scene !== false) { - delayedRequestRate(session.hiddenSceneViewBitrate, i, true); // hidden. I dont want it to be super low, for video quality reasons. - if (!session.hiddenSceneViewBitrate) { - session.rpcs[i].videoElement.nogb = 2; - } - } else { - delayedRequestRate(0, i, true); // w/e This is not in OBS, so we just set it as low as possible. Shoudln't exist really unless loading? - } - } else if (session.scene !== false) { - // max - // - if (sceneBitrate !== false) { - if (screenShareBitrate !== false && session.rpcs[i].screenShareState) { - delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality. - } else { - delayedRequestRate(sceneBitrate, i); // well, screw that. Setting it to room quality. - } - } else { - if (screenShareBitrate !== false && session.rpcs[i].screenShareState) { - delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality. - } else { - delayedRequestRate(-1, i); // unlock. - } - } - if (session.rpcs[i].order !== false) { - session.rpcs[i].videoElement.order = session.rpcs[i].order; - } else { - session.rpcs[i].videoElement.order = 0; - } - if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { - if (!(session.rpcs[i].videoElement in mediaPool_invisible)) { - // mediaPool_invisible.push(session.rpcs[i].videoElement); - } else { - errorlog("THIS SHOULD NOT HAPPEN; 650"); - } - } else if (!doNotPush) { - mediaPool.push(session.rpcs[i].videoElement); - } - } else if (session.roomid !== false) { - // guests should see video at low bitrate, ie: 100kbps (not 35kbps like if disabled) - if (session.rpcs[i].order !== false) { - session.rpcs[i].videoElement.order = session.rpcs[i].order; - } else { - session.rpcs[i].videoElement.order = 0; - } - if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { - if (!(session.rpcs[i].videoElement in mediaPool_invisible)) { - // mediaPool_invisible.push(session.rpcs[i].videoElement); - } else { - errorlog("THIS SHOULD NOT HAPPEN; 665"); - } - } else if (!doNotPush) { - mediaPool.push(session.rpcs[i].videoElement); - } - if (session.roomid === "" && session.bitrate) { - // we will let the URL specified bitrate hold, since this isn't a real room. - delayedRequestRate(-1, i); - } else { - if (screenShareBitrate !== false && session.rpcs[i].screenShareState) { - delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality. - } else { - delayedRequestRate(roomBitrate, i); // well, screw that. Setting it to room quality. - } - } - } else { - // view=xx,yy or whatever. This should be highest quality. - if (session.rpcs[i].order !== false) { - session.rpcs[i].videoElement.order = session.rpcs[i].order; - } else { - session.rpcs[i].videoElement.order = 0; - } - if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { - if (!(session.rpcs[i].videoElement in mediaPool_invisible)) { - // mediaPool_invisible.push(session.rpcs[i].videoElement); - } else { - errorlog("THIS SHOULD NOT HAPPEN; 684"); - } - } else if (!doNotPush) { - mediaPool.push(session.rpcs[i].videoElement); - } - if (sceneBitrate) { - delayedRequestRate(sceneBitrate, i); - } else if (session.screenShareBitrate !== false && session.rpcs[i].screenShareState) { - // session.screenShareBitrate is non-room - delayedRequestRate(session.screenShareBitrate, i); // well, screw that. Setting it to room quality. - } else { - delayedRequestRate(-1, i); - } - } - if (session.rpcs[i].videoElement.nogb == 2) { - session.rpcs[i].videoElement.nogb = 1; - session.rpcs[i].videoElement.classList.add("nogb"); - } else if (session.rpcs[i].videoElement.nogb == 1) { - session.rpcs[i].videoElement.nogb = 0; - session.rpcs[i].videoElement.classList.remove("nogb"); - } - } - } - } - - if (session.broadcastIFrame && session.broadcastIFrame.src) { - // keep alive iframes whennot visible. i think - if (!mediaPool.length) { - mediaPool.push(session.broadcastIFrame); - } - } - - if (document.fullscreenElement) { - try { - if (document.fullscreenElement.tagName === "VIDEO") { - // if its HTML, than we assume its the full canvas - for (var i = 0; i < mediaPool.length; i++) { - // if its your local camera, it shouldn't be a problem, so we can focus on remote cameras only - if (mediaPool[i].id !== document.fullscreenElement.id) { - // if its selected camera, we want to exclude it - if (mediaPool[i].dataset && mediaPool[i].dataset.UUID && mediaPool[i].tagName && mediaPool[i].tagName == "VIDEO") { - delayedRequestRate(session.hiddenSceneViewBitrate, mediaPool[i].dataset.UUID, null); // null implies don't change the current audio setting - mediaPool_invisible.push(mediaPool[i]); // move visible elements to the invisible list, since something is full screen - mediaPool.splice(i, 1); - } - } - } - } - } catch (e) { - errorlog(e); - } - } - - var sscount = 0; - - var skip = false; - - for (var m = 0; m < mediaPool.length; m++) { - mediaPool[m].alreadyAdded = false; - } - - if (!session.layout || session.activeSpeaker) { - if (session.orderby) { - if (session.orderby == "id") { - mediaPool.sort(compare_vids_sid); - } else if (session.orderby == "label") { - mediaPool.sort(compare_vids_label); - } else { - mediaPool.sort(compare_vids_sid); - } - } - mediaPool.sort(compare_vids); - } else if (session.exclusiveLayoutAudio) { - [...mediaPool].forEach(ele => { - if (ele.dataset.sid) { - if (session.layout[ele.dataset.sid]) { - return; - } else if (session.layout[""]) { - let matched = false; - session.layout[""].forEach(i => { - if (i.defaultStreamID && i.defaultStreamID == ele.dataset.sid) { - matched = true; - } - }); - if (matched) { - return; - } - } - const index = mediaPool.indexOf(ele); - if (index > -1) { - if (ele.dataset.UUID && session.scene !== false) { - delayedRequestRate(session.hiddenSceneViewBitrate, ele.dataset.UUID, false); // it's added already, so we know it needs sound. But lets d - } - mediaPool.splice(index, 1); // 2nd parameter means remove one item only - } - } - }); - if (RPCSkeys) { - for (var keyIndex = 0; keyIndex < RPCSkeys.length; keyIndex++) { - i = RPCSkeys[keyIndex]; - if (session.rpcs[i] === null) { - continue; - } - if (session.rpcs[i].streamID) { - let matched = false; - mediaPool.forEach(ele => { - if (ele.dataset.sid == session.rpcs[i].streamID) { - matched = true; - } - }); - if (!matched) { - if (session.rpcs[i].mutedStateMixer === false) { - session.rpcs[i].mutedStateMixer = true; - applyMuteState(i); - } - } - } - } - } - } - - if (session.fakeFeeds && session.fakeFeeds.length && mediaPool.length < session.fakeFeeds.length) { - for (let i = 0; i < session.fakeFeeds.length; i++) { - if (mediaPool.length < session.fakeFeeds.length) { - mediaPool.push(session.fakeFeeds[i]); - } else { - try { - session.fakeFeeds[i].remove(); - } catch (e) { - errorlog(e); - } - } - } - } - - if (session.slotsList && session.slotsList.length > 0) { - // Filter mediaPool to only include videos in the slotsList - const filteredMediaPool = []; - for (let i = 0; i < mediaPool.length; i++) { - if (session.slotsList.includes(i + 1)) { // +1 for 1-indexed slotsList - filteredMediaPool.push(mediaPool[i]); - } - } - mediaPool = filteredMediaPool; - - } - - var mpl = session.slots || mediaPool.length; - - if (!sssid) { - if (mpl > 1) { - var BB = 0; - var rw = 1; - var rh = 1; - var NW; - var NH; - var current; - for (NW = 1; NW <= mpl; NW++) { - NH = Math.ceil(mpl / NW); - var www = ww / NW; - var hhh = hh / NH; - if (www > hhh) { - current = Math.round(hhh * hhh * (mpl / (NW * NH))); - } else { - current = Math.round(www * www * (mpl / (NW * NH))); - } - - if (current >= BB) { - BB = current; - rw = NW; - rh = NH; - } - - if (mediaPool[NW - 1]) { - //if (mediaPool[NW-1].tagName == "VIDEO"){ - if (mediaPool[NW - 1].dataset.UUID) { - if (mediaPool[NW - 1].dataset.UUID in session.rpcs) { - const rpc = session.rpcs[mediaPool[NW - 1].dataset.UUID]; - const currentUUID = mediaPool[NW - 1].dataset.UUID; - - // Skip parent UUID if a _screen counterpart exists (avoid double-counting) - if (!currentUUID.endsWith("_screen") && session.rpcs[currentUUID + "_screen"]) { - // This is a parent with a _screen entry; let the _screen entry be counted instead - continue; - } - - // Check for screen share indicators: flag, UUID suffix, or stream ID suffix - const isScreen = rpc.screenShareState || - currentUUID.endsWith("_screen") || - (mediaPool[NW - 1].dataset.sid && mediaPool[NW - 1].dataset.sid.endsWith(":s")); - - if (isScreen && !rpc.smallScreen) { - let hasLiveScreenVideo = false; - try { - // Check streamSrc first (most reliable for local/direct) - if (rpc.streamSrc && rpc.streamSrc.getVideoTracks) { - hasLiveScreenVideo = rpc.streamSrc.getVideoTracks().some(trk => trk.readyState === "live"); - } - // Fallback: check the video element's srcObject if streamSrc is missing/empty - if (!hasLiveScreenVideo && mediaPool[NW - 1].srcObject && mediaPool[NW - 1].srcObject.getVideoTracks) { - hasLiveScreenVideo = mediaPool[NW - 1].srcObject.getVideoTracks().some(trk => trk.readyState === "live"); - } - } catch (e) { } - - console.log("updateMixer check:", { - uuid: mediaPool[NW - 1].dataset.UUID, - isScreen, - hasLiveScreenVideo, - screenShareState: rpc.screenShareState - }); - - if (hasLiveScreenVideo) { - sscount += 1; - sssid = mediaPool[NW - 1].dataset.sid; - } - } - } - } else if ("id" in mediaPool[NW - 1] && mediaPool[NW - 1].id == "screensharesource" && session.notifyScreenShare) { - sscount += 1; - sssid = mediaPool[NW - 1].dataset.sid; - } - } - } - } else { - var rw = 1; - var rh = 1; - } - - if (sscount > 1) { - sssid = false; // lets not maximize if more than one screen share. - } - } - } catch (e) { - errorlog(e); - sssid = false; - } - - // Add screen share status classes to the gridlayout element - if (sscount > 0) { - playarea.classList.add("has-screenshare"); - playarea.classList.remove("no-screenshare"); - } else { - playarea.classList.add("no-screenshare"); - playarea.classList.remove("has-screenshare"); - } - - var customLayout = false; - - var allowScreenshareAutoLayout = !session.layout || session.activeSpeaker || session.alignRight; - var effectiveScreenshareStyle = session.screenshareStyle; - if (!effectiveScreenshareStyle && session.alignRight) { - effectiveScreenshareStyle = 2; - } - - if (!session.notifyScreenShare && session.scene !== false) { - // this is a scene, so lets assume &smallshare will disable larger screen shares since there is no one to screen share. - } else if (sssid && effectiveScreenshareStyle && allowScreenshareAutoLayout) { - customLayout = {}; - let spotlightLayout; - if (mediaPool.length >= 12) { - spotlightLayout = { x: 10, y: 10, w: 90, h: 90, c: false }; - } else if (mediaPool.length >= 10) { - spotlightLayout = { x: 0, y: 10, w: 100, h: 90, c: false }; - } else if (mediaPool.length >= 8) { - spotlightLayout = { x: 20, y: 20, w: 80, h: 80, c: false }; - } else if (mediaPool.length == 7) { - spotlightLayout = { x: 16.66667, y: 0, w: 83.33333, h: 100, c: false }; - } else if (mediaPool.length == 5 || mediaPool.length == 6) { - spotlightLayout = { x: 20, y: 0, w: 80, h: 100, c: false }; - } else { - spotlightLayout = { x: 20, y: 0, w: 80, h: 100, c: false }; - } - - if (effectiveScreenshareStyle === 2) { - if (spotlightLayout.x < 20) { - spotlightLayout.x = 20; - } - if (spotlightLayout.w > 80) { - spotlightLayout.w = 80; - } - } - - customLayout[sssid] = spotlightLayout; - - if (effectiveScreenshareStyle === 2 && mediaPool.length > 6) { - var columnSlot = 0; - var totalOthers = mediaPool.length - 1; - var slotHeight = totalOthers > 0 ? 100 / totalOthers : 100; - for (var i = 0; i < mediaPool.length; i++) { - if (mediaPool[i].dataset.sid === sssid) { - continue; - } - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: slotHeight * columnSlot, w: 20, h: slotHeight, c: true }; - columnSlot += 1; - } - } else { - var posCount = 0; - for (var i = 0; i < mediaPool.length; i++) { - if (mediaPool[i].dataset.sid === sssid) { - continue; - } - if (mediaPool.length == 2) { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: 0, w: 20, h: 100, c: session.cover }; - } else if (mediaPool.length == 3) { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 41 + 9, w: 20, h: 41, c: session.cover }; - } else if (mediaPool.length == 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 33.3333, w: 20, h: 33.3333, c: session.cover }; - } else if (mediaPool.length == 5) { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: session.cover }; - } else if (mediaPool.length == 6) { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 20, w: 20, h: 20, c: session.cover }; - } else if (mediaPool.length == 7) { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 16.66667, w: 16.66667, h: 16.66667, c: session.cover }; - } else if (mediaPool.length == 8) { - if (posCount == 0 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 70, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 1 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 50, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 2 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 30, y: 0, w: 20, h: 20, c: true }; - } else { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: true }; - } - } else if (mediaPool.length == 9) { - if (posCount == 0 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 1 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 60, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 2 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 40, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 3 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 20, y: 0, w: 20, h: 20, c: true }; - } else { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: true }; - } - } else if (mediaPool.length >= 10) { - if (posCount < 10) { - customLayout[mediaPool[i].dataset.sid] = { x: 90 - 10 * posCount, y: 0, w: 10, h: 10, c: true }; - } else { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: (posCount - 9) * 10, w: 10, h: 10, c: true }; - } - } else { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 20, w: 20, h: 20, c: true }; - } - posCount += 1; - } - } - } else if (sssid && allowScreenshareAutoLayout) { - customLayout = {}; - - if (mediaPool.length >= 12) { - customLayout[sssid] = { x: 0, y: 10, w: 90, h: 90, c: false }; - } else if (mediaPool.length >= 10) { - customLayout[sssid] = { x: 0, y: 10, w: 100, h: 90, c: false }; - } else if (mediaPool.length >= 8) { - customLayout[sssid] = { x: 0, y: 20, w: 80, h: 80, c: false }; - } else if (mediaPool.length == 7) { - customLayout[sssid] = { x: 0, y: 0, w: 83.33333, h: 100, c: false }; - } else if (mediaPool.length == 5) { - customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false }; - } else if (mediaPool.length == 6) { - customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false }; - } else if (mediaPool.length == 1) { - customLayout[sssid] = { x: 0, y: 0, w: 100, h: 100, c: false }; - } else { - customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false }; - } - var posCount = 0; - for (var i = 0; i < mediaPool.length; i++) { - if (mediaPool[i].dataset.sid === sssid) { - continue; - } - if (mediaPool.length == 2) { - // - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: 0, w: 20, h: 100, c: session.cover }; - } else if (mediaPool.length == 3) { - // - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 41 + 9, w: 20, h: 41, c: session.cover }; - } else if (mediaPool.length == 4) { - // - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 33.3333, w: 20, h: 33.3333, c: session.cover }; - } else if (mediaPool.length == 5) { - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: session.cover }; - } else if (mediaPool.length == 6) { - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 20, w: 20, h: 20, c: session.cover }; - } else if (mediaPool.length == 7) { - customLayout[mediaPool[i].dataset.sid] = { x: 83.33333, y: posCount * 16.66667, w: 16.66667, h: 16.66667, c: session.cover }; - } else if (mediaPool.length == 8) { - if (posCount == 0 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 10, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 1 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 30, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 2 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 50, y: 0, w: 20, h: 20, c: true }; - } else { - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: true }; - } - } else if (mediaPool.length == 9) { - if (posCount == 0 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 0, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 1 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 20, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 2 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 40, y: 0, w: 20, h: 20, c: true }; - } else if (posCount == 3 + 4) { - customLayout[mediaPool[i].dataset.sid] = { x: 60, y: 0, w: 20, h: 20, c: true }; - } else { - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: true }; - } - } else if (mediaPool.length >= 10) { - if (posCount < 10) { - customLayout[mediaPool[i].dataset.sid] = { x: 10 * posCount, y: 0, w: 10, h: 10, c: true }; - } else { - customLayout[mediaPool[i].dataset.sid] = { x: 90, y: (posCount - 9) * 10, w: 10, h: 10, c: true }; - } - } else { - customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 20, w: 20, h: 20, c: true }; // - } - posCount += 1; - } - - } else if (session.rows && mediaPool.length) { - try { - customLayout = {}; - let n = mediaPool.length; - - if (session.slots && n < session.slots) { - n = session.slots; // If we have fewer videos than slots, still use the full slots count - } - - let rows = 1; - if (session.rows.length >= n) { - rows = parseInt(session.rows[n - 1]) || 1; - } else { - rows = parseInt(session.rows[session.rows.length - 1]) || 1; - } - - if (rows < 0) { - rows = Math.abs(rows); - let cols = Math.ceil(n / rows) || 1; - for (var i = 0; i < n; i++) { - let col = i % cols; - let row = parseInt(i / cols) % rows; - - if (row === Math.floor((n - 1) / cols) && n % cols !== 0) { // Last row logic - let itemsInLastRow = n % cols || cols; - let offset = (cols - itemsInLastRow) * (100 / cols) / 2; - customLayout[mediaPool[i].dataset.sid] = { - y: (100 / rows) * row, - h: 100 / rows, - x: offset + (100 / cols) * col, - w: 100 / cols, - c: session.cover - }; - } else { - customLayout[mediaPool[i].dataset.sid] = { - y: (100 / rows) * row, - h: 100 / rows, - x: (100 / cols) * col, - w: 100 / cols, - c: session.cover - }; - } - } - } else { - - let cols; - - if (session.slots) { - cols = session.slots / rows; - } else { - cols = Math.ceil(n / rows) || 1; - } - - for (var i = 0; i < n; i++) { - let col = i % cols; - let row = parseInt(i / cols) % rows; - //console.log(row,col, rows,cols,i,n) - if (cols >= 2 && rows >= 2 && n < (row + 1) * cols) { - let delta = (row + 1) * cols - n; - customLayout[mediaPool[i].dataset.sid] = { y: (100 / rows) * row, h: 100 / rows, x: (100 / cols) * (col + delta), w: 100 / cols, c: session.cover }; - } else { - customLayout[mediaPool[i].dataset.sid] = { y: (100 / rows) * row, h: 100 / rows, x: (100 / cols) * col, w: 100 / cols, c: session.cover }; - } - } - } - } catch (e) { - errorlog(e); - } - } - - try { - if (!skip) { - var childNodes = playarea.childNodes; - - for (var n = 0; n < childNodes.length; n++) { - if (childNodes[n].querySelector("video")) { - var vidtemp = childNodes[n].querySelector("video"); - var matched = false; - for (var m = 0; m < mediaPool.length; m++) { - if (vidtemp.id === mediaPool[m].id) { - vidtemp.alreadyAdded = true; - mediaPool[m] = vidtemp; - matched = true; - childNodes[n].matched = true; - break; - } - } - if (!matched && vidtemp.isInvisible) { - vidtemp.isInvisible = false; - if (session.pauseInvisible && vidtemp.dataset.UUID) { - applyMuteState(vidtemp.dataset.UUID); - } - } - } else if (childNodes[n].querySelector("iframe")) { - var iftemp = childNodes[n].querySelector("iframe"); - if (iftemp.framesource) { - if ((!session.activeSpeaker && session.layout) && session.layout[""]) { - for (var i = 0; i < session.layout[""].length; i++) { - if (session.layout[""][i].iframeSrc && session.layout[""][i].iframeSrc == iftemp.framesource) { - childNodes[n].matched = true; - if (iftemp.isConnected) { - iftemp.alreadyAdded = true; - } - } - } - } - continue; - } - for (var m = 0; m < mediaPool.length; m++) { - if (mediaPool[m].nodeName === "IFRAME" && mediaPool[m].src && iftemp.src === mediaPool[m].src) { - iftemp.alreadyAdded = true; - iftemp.id = mediaPool[m].id; - if (session.directorList.indexOf(iftemp.dataset.UUID) == -1) { - iftemp.dataset.UUID = mediaPool[m].dataset.UUID; - iftemp.dataset.sid = mediaPool[m].dataset.sid; - } - mediaPool[m] = iftemp; - childNodes[n].matched = true; - break; - } - } - for (var m = 0; m < mediaPool_invisible.length; m++) { - if (mediaPool_invisible[m].nodeName === "IFRAME" && mediaPool_invisible[m].src && iftemp.src === mediaPool_invisible[m].src) { - iftemp.alreadyAdded = true; - iftemp.id = mediaPool_invisible[m].id; - if (session.directorList.indexOf(iftemp.dataset.UUID) == -1) { - iftemp.dataset.UUID = mediaPool_invisible[m].dataset.UUID; - iftemp.dataset.sid = mediaPool_invisible[m].dataset.sid; - } - mediaPool_invisible[m] = iftemp; - childNodes[n].matched = true; - break; - } - } - } - } - - for (var n = 0; n < childNodes.length; n++) { - if (!childNodes[n].matched) { - playarea.removeChild(childNodes[n]); - n--; - } else { - childNodes[n].matched = null; - } - } - } - } catch (e) { - errorlog(e); - } - - if (session.videoElement && (session.videoElement.src || session.videoElement.srcObject)) { - // fileshare or stream - if ("playlist" in session.videoElement) { - playarea.appendChild(session.videoElement); // fileshare. - } else if (session.videoElement.style.display !== "none") { - if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length) { - if (miniPreview) { - var container = null; - if (mpl === 0 && miniPreview === 2) { - if (soloVideo !== true) { - // since miniPreview==2, we want to full screen our preview. Deleting the old mini preview container will ensure it loads right. - if (!session.layout || session.activeSpeaker) { - if (session.videoElement.container && session.videoElement.container.id == "minipreview") { - delete session.videoElement.container; - } - if (document.getElementById("minipreview")) { - document.getElementById("minipreview").remove(); - } - } - // - mediaPool.push(session.videoElement); - mpl = 1; - } - } else if (miniPreview === 3) { - if (soloVideo !== true) { - container = document.createElement("div"); - session.videoElement.container = container; - container.style.top = "-500px"; - container.style.left = "-500px"; - container.style.width = "1px"; - container.style.height = "1px"; - //container.style.display = "flex"; - container.style.zIndex = "0"; - container.style.margin = "0"; - container.style.position = "absolute"; - container.style.cursor = "pointer"; - container.style.border = "0"; - container.appendChild(session.videoElement); - playarea.appendChild(container); - } - } else if (soloVideo !== true) { - if (document.getElementById("minipreview")) { - container = document.getElementById("minipreview"); - } else { - container = document.createElement("div"); - var togglePreview = document.createElement("div"); - togglePreview.className = "togglePreview"; - try { - container.style.top = "calc(" + hi + "px + 2vh)"; - container.style.maxHeight = parseInt(playarea.offsetHeight) + "px"; - togglePreview.style.top = "calc(" + hi + "px + 2vh)"; - togglePreview.style.maxHeight = parseInt(playarea.offsetHeight) + "px"; - } catch (e) { - errorlog(e); - container.style.top = hi + "px"; - togglePreview.style.top = hi + "px"; - } - // - if (miniPerformerY !== null) { - container.style.top = miniPerformerY + "%"; - } - if (miniPerformerX !== null) { - if (session.widget && !session.leftMiniPreview) { - if (miniPerformerX > (99 - session.widgetwidth)) { - miniPerformerX = 99 - session.widgetwidth; - } - } - container.style.left = miniPerformerX + "%"; - } else if (session.leftMiniPreview !== false) { - container.style.left = session.leftMiniPreview + "%"; - togglePreview.style.left = session.leftMiniPreview + "%"; - } else if (session.widget) { - container.style.right = session.widgetwidth + "%"; - togglePreview.style.right = session.widgetwidth + "%"; - } else { - container.style.right = "2vw"; - togglePreview.style.right = "2vw"; - } - container.appendChild(session.videoElement); - session.videoElement.container = container; - playarea.appendChild(container); - togglePreview.innerHTML = ''; - if (!session.previewToggleState) { - container.classList.toggle("hidden"); - togglePreview.classList.toggle("blinded"); - } - if (!(iOS || iPad)) { - playarea.appendChild(togglePreview); - } - togglePreview.onclick = function (event) { - event.preventDefault(); - event.stopPropagation(); - getById("minipreview").classList.toggle("hidden"); - this.classList.toggle("blinded"); - session.previewToggleState = !session.previewToggleState; - return false; - }; - makeMiniDraggableElement(container); - container.id = "minipreview"; - } - container.style.width = "18%"; - //container.style.display = "flex"; - container.style.zIndex = "3"; - container.style.margin = "0"; - container.style.position = "absolute"; - container.style.cursor = "pointer"; - container.style.border = "2px #BBB solid"; - container.style.height = "block"; - applyMirror(session.mirrorExclude); - } else if (soloVideo === true) { - if (document.getElementById("minipreview")) { - container = document.getElementById("minipreview"); - container.style.height = "100%"; - //container.style.transform = "block"; - //container.style.transformOrigin = "unset"; - } - } - if (session.ruleOfThirds) { - if (container && container.id == "minipreview" && !container.svg) { - var svg = document.createElement("img"); - svg.src = session.ruleOfThirds; - svg.style.width = "100%"; - svg.style.height = "100%"; - svg.style.position = "absolute"; - svg.style.left = "0"; - svg.style.top = "0"; - container.svg = svg; - container.appendChild(svg); - } - } - container = null; // clear reference - } - } else if (session.streamSrc && !session.videoElement.srcObject) { - warnlog("THIS SHOULD NOT HAPPEN; 2067"); - } - } - } - - try { - if (session.slots) { - var slotArray = []; - mediaPool.forEach(vid => { - if (vid.slotBlank) { - vid.slotBlank = false; - vid.slot = 0; - } - - if ("slot" in vid && vid.slot) { - if (!slotArray.includes(parseInt(vid.slot))) { - slotArray.push(parseInt(vid.slot)); - } else { - vid.slot = 0; - //mediaPool_invisible.push(vid); - //var index = mediaPool.indexOf(vid); - //if (index > -1) { - // mediaPool.splice(index, 1); - //} - } - } - }); - var slotCounter = 1; - mediaPool.reverse(); - var j = mediaPool.length; - while (j--) { - if (!("slot" in mediaPool[j]) || mediaPool[j].slot == "0" || !mediaPool[j].slot) { - while (slotArray.includes(slotCounter)) { - slotCounter += 1; - } - slotArray.push(slotCounter); - mediaPool[j].slot = slotCounter; - mediaPool[j].slotBlank = true; - } - if (!("slot" in mediaPool[j]) || !parseInt(mediaPool[j].slot) || mediaPool[j].slot == "0" || !mediaPool[j].slot || session.slots < parseInt(mediaPool[j].slot)) { - //if ((!("slot" in mediaPool[j]) || !parseInt(mediaPool[j].slot) || mediaPool[j].slot == "0" || !mediaPool[j].slot || session.slots < parseInt(mediaPool[j].slot)) && !customLayout) { - mediaPool_invisible.push(mediaPool[j]); - mediaPool.splice(j, 1); - } - } - mediaPool.reverse(); - } - - if (session.pauseInvisible) { - mediaPool_invisible.forEach(vid => { // This is an experimental version, with improvements? Maybe its better? - if (vid) { - try { - vid.style.width = ""; - vid.style.height = ""; - if (!vid.isInvisible) { - vid.isInvisible = true; - if (session.pauseInvisible && vid.dataset.UUID) { - session.requestRateLimit(session.hiddenSceneViewBitrate, vid.dataset.UUID, true); - vid.muted = true; - } - } - if (vid.dataset.doNotMove) { - return; - } - vid.style.top = "0px"; - vid.style.left = "0px"; - if (vid.isConnected) { - console.warn("Video is invisible, yet connected to the DOM?"); - } - } catch (e) { - errorlog(e); - } - } - }); - } else { // this is the old version; a fail safe. - mediaPool_invisible.forEach(vid => { - if (vid) { - try { - vid.style.width = "0px"; - vid.style.height = "0px"; - vid.style.top = "0px"; - vid.style.left = "0px"; - vid.isInvisible = true; - if (vid.alreadyAdded && vid.alreadyAdded == true) { - vid.alreadyAdded = false; - return; - } else if (vid.dataset.doNotMove) { - return; - } - if (!(vid.nodeName == "IFRAME" && vid.isConnected)) { - playarea.appendChild(vid); - } - } catch (e) { - errorlog(e); - } - } - }); - } - - } catch (e) { - errorlog(e); - } - - var layout = false; - if (customLayout || (!session.activeSpeaker && session.layout)) { - layout = session.layout || customLayout; - layout = { ...layout }; - if (layout[""]) { - for (var i = 0; i < layout[""].length; i++) { - if (layout[""][i].defaultStreamID && !layout[layout[""][i].defaultStreamID]) { - var found = false; - for (var ell in mediaPool) { - if (mediaPool[ell].dataset.sid && mediaPool[ell].dataset.sid === layout[""][i].defaultStreamID) { - layout[layout[""][i].defaultStreamID] = layout[""][i]; - found = true; - } - } - if (found) { - continue; - } - } - layout["#" + i] = layout[""][i]; - if (layout["#" + i].iframeSrc) { - if (session.iframeSrcs[layout["#" + i].iframeSrc]) { - var ele = session.iframeSrcs[layout["#" + i].iframeSrc]; - ele.id = "#" + i; - } else { - var ele = loadIframe(parseURL4Iframe(layout["#" + i].iframeSrc), "#" + i); - ele.framesource = layout["#" + i].iframeSrc; - session.iframeSrcs[layout["#" + i].iframeSrc] = ele; - } - //ele.alreadyAdded = true; - //ele.matched = true; - } else if (layout["#" + i].backgroundMedia || layout["#" + i].text || layout["#" + i].foregroundMedia) { - var ele = document.createElement("div"); - ele.dataset.sid = "#" + i; - } else { - continue; - } - ele.dataset.sid = "#" + i; - mediaPool.push(ele); - } - } - - try { - mediaPool = sortByZ(mediaPool, layout); - } catch (e) { - // layout = false; - errorlog(e); - } - } - var i = 0; - var offset = 0; - - if (session.waitImage && !(mediaPool_invisible.length || mediaPool.length)) { - if (!session.waitImageTimeoutObject) { - session.waitImageTimeoutObject = setTimeout(function () { - session.waitImageTimeoutObject = true; - if (!document.getElementById("retryimage")) { - playarea.innerHTML += ''; - getById("retryimage").src = decodeURIComponent(session.waitImage); - getById("retryimage").onerror = function () { - this.style.display = "none"; - }; - - if (session.cover) { - getById("retryimage").style.objectFit = "cover"; - } - } - getById("retryimage").style.display = "block"; - }, session.waitImageTimeout); - } - } else if (session.waitImage) { - try { - clearTimeout(session.waitImageTimeoutObject); - session.waitImageTimeoutObject = false; - getById("retryimage").style.display = "none"; - } catch (e) { } - } - mediaPool.forEach(vid => { - try { - if (!vid || !("id" in vid)) { - errorlog(vid); - return; - } - - if (vid.needsLoading) { - try { - vid.load(); - } catch (e) { - errorlog(e); - } - } - - if (session.slots) { - if ("slot" in vid && parseInt(vid.slot)) { - i = parseInt(vid.slot) - 1; - if (i < 0) { - return; - } - } else { - return; - } - } - - var offsetx = 0; - if (i !== 0) { - if (Math.ceil((i + 0.01) / rw) == rh) { - if (mpl % rw) { - offsetx = Math.max(((rw - (mpl % rw)) * (w / rw)) / 2, 0); - } - } - } - - var cover = session.cover; - var borderOffset = session.border || 0; - var videoMargin = session.videoMargin || 0; - var borderRadius = session.borderRadius || 0; - var borderColor = session.borderColor || "#000"; - var fadein = session.fadein || false; - var backgroundMedia = session.defaultMedia || false; - var foregroundMedia = session.defaultOverlayMedia || false; - var animated = session.animatedMoves || 0; - var textOverlay = false; - if (!borderOffset) { - borderColor = "#0000"; - } - - if (layout) { - if (!(vid.dataset.sid && vid.dataset.sid in layout)) { - - if (vid.container) { - vid.container.style.display = "none"; - } - - vid.isInvisible = true; - if (session.pauseInvisible && vid.dataset.UUID) { - session.requestRateLimit(session.hiddenSceneViewBitrate, vid.dataset.UUID, true); - vid.muted = true; - return; - } - - if (vid.dataset.UUID) { - delayedRequestRate(session.hiddenSceneViewBitrate, vid.dataset.UUID, false); // it's added already, so we know it needs sound. But lets d - } - return; - } - if ("borderThickness" in layout[vid.dataset.sid]) { - borderOffset = layout[vid.dataset.sid].borderThickness || 0; - } - if ("animated" in layout[vid.dataset.sid]) { - animated = layout[vid.dataset.sid].animated || 0; - if (animated === true) { - animated = session.animatedMoves || 50; - } - } - if ("margin" in layout[vid.dataset.sid]) { - videoMargin = layout[vid.dataset.sid].margin || 0; - } - if ("rounded" in layout[vid.dataset.sid]) { - borderRadius = layout[vid.dataset.sid].rounded || 0; - } - if (layout[vid.dataset.sid].borderColor) { - borderColor = layout[vid.dataset.sid].borderColor; - } - if (layout[vid.dataset.sid].fadeIn) { - fadein = layout[vid.dataset.sid].fadeIn; - } - if ("backgroundMedia" in layout[vid.dataset.sid]) { - backgroundMedia = layout[vid.dataset.sid].backgroundMedia || false; - } - if ("foregroundMedia" in layout[vid.dataset.sid]) { - foregroundMedia = layout[vid.dataset.sid].foregroundMedia || false; - } - - if (layout[vid.dataset.sid].text) { - - if (!vid.container || !vid.container.textOverlay) { - textOverlay = document.createElement("div"); - textOverlay.className = "textOverlay"; - //vid.container.appendChild(vid.container.textOverlay); - } else { - textOverlay = vid.container.textOverlay; - } - - textOverlay.innerText = layout[vid.dataset.sid].text; - textOverlay.style.color = layout[vid.dataset.sid].textColor || "#ffffff"; - textOverlay.style.fontSize = layout[vid.dataset.sid].fontSize || "24px"; - textOverlay.style.fontFamily = layout[vid.dataset.sid].fontFamily || "Arial, sans-serif"; - textOverlay.style.position = "absolute"; - textOverlay.style.width = "100%"; - textOverlay.style.textAlign = "center"; - textOverlay.style.zIndex = "10"; - - // Position the text - const textPosition = layout[vid.dataset.sid].textPosition || "50%"; - textOverlay.style.top = textPosition; - textOverlay.style.transform = "translateY(-50%)"; - - // Add background if specified - if (layout[vid.dataset.sid].textBackground) { - textOverlay.style.backgroundColor = layout[vid.dataset.sid].textBackground; - textOverlay.style.padding = "10px"; - } else { - textOverlay.style.backgroundColor = "transparent"; - textOverlay.style.textShadow = "1px 1px 2px rgba(0,0,0,0.8)"; - } - } else if (vid.container && vid.container.textOverlay) { - vid.container.textOverlay.remove(); - delete vid.container.textOverlay; - } - - if (vid.container) { - if (!(vid.nodeName == "IFRAME" && vid.isConnected)) { - // moving an iframe will break it. - if (!vid.alreadyAdded || vid.nodeName == "IFRAME") { - playarea.appendChild(vid.container); - } - } - } - } - - var skipAnimation = false; - if (vid.isInvisible) { - vid.isInvisible = false; - if (session.pauseInvisible && vid.dataset.UUID) { - applyMuteState(vid.dataset.UUID); - } - skipAnimation = true; - if (fadein) { - vid.classList.add("fadein"); - if (vid.holder) { - vid.holder.classList.add("fadein"); - } - } - - } - - offsety = Math.max((h - Math.ceil(mpl / rw) * Math.ceil(h / rh)) / 2, 0); - - if (vid.container) { - var container = vid.container; - if (container.move) { - clearInterval(container.move); - container.move = null; - } - } else { - var container = document.createElement("div"); - vid.container = container; - } - container.style.position = "absolute"; - container.style.display = "block"; - container.classList.add("container_holder_video"); - - // Add screen share class to individual containers - var isScreenShare = false; - var vidUUID = vid.dataset.UUID; - var vidSid = vid.dataset.sid; - if (vidUUID && session.rpcs[vidUUID] && !vidSid && session.rpcs[vidUUID].streamID) { - vidSid = session.rpcs[vidUUID].streamID; - } - - if (vid.id === "screensharesource" || (vidUUID && vidUUID.endsWith("_screen")) || (vidSid && vidSid.endsWith(":s"))) { - isScreenShare = true; - } else if (vidUUID && session.rpcs[vidUUID] && session.rpcs[vidUUID].screenShareState) { - if (!session.rpcs[vidUUID + "_screen"]) { - isScreenShare = true; - } - } - - if (isScreenShare) { - container.classList.add("is-screenshare"); - container.classList.remove("is-not-screenshare"); - } else { - container.classList.add("is-not-screenshare"); - container.classList.remove("is-screenshare"); - } - - // ANIMATED - CONTAINER ; width/height/z-index/cover/////////////// - if (layout) { - try { - var left = (w / 100) * layout[vid.dataset.sid].x || layout[vid.dataset.sid].xp || 0; - var top = (h / 100) * layout[vid.dataset.sid].y || layout[vid.dataset.sid].yp || 0; - top += hi; - var width = (w / 100) * layout[vid.dataset.sid].w || layout[vid.dataset.sid].wp || 0; - var height = (h / 100) * layout[vid.dataset.sid].h || layout[vid.dataset.sid].hp || 0; - - if (layout[vid.dataset.sid].cover || layout[vid.dataset.sid].c) { - // this should be true/false - //vid.style.objectFit = "cover"; - cover = layout[vid.dataset.sid].cover || layout[vid.dataset.sid].c; - } else { - //vid.style.objectFit = "contain"; // this should fall back to sessio.cover if no layout supplied - cover = false; - } - //container.style.zindex = 0; - container.style.zIndex = layout[vid.dataset.sid].zIndex || layout[vid.dataset.sid].z || 0; - } catch (e) { - errorlog(e); - } - } else { - var left = Math.max(offsetx + Math.floor((((i % rw) + 0) * w) / rw), 0); - var top = Math.max(offsety + Math.floor(((Math.floor(i / rw) + 0) * h) / rh + hi), 0); - var width = Math.ceil(w / rw); - var height = Math.ceil(h / rh); - //container.style.zIndex = 0; - } - - var computed = getComputedStyle(vid); - - if (animated && !skipAnimation) { - container.style.transition = "width " + animated + "ms ease-in-out 0s, height " + animated + "ms ease-in-out 0s, background-color " + animated + "ms ease-in-out 0s, transform " + animated + "ms ease-in-out 0s, top " + animated + "ms ease-in-out 0s, left " + animated + "ms ease-in-out 0s"; - } else { - container.style.transition = ""; - } - - if (layout) { - ////////////////// NOT ANIMATED - CONTAINER ; width/height/z-index/cover/////////////// - container.style.left = left + "px"; - container.style.top = top + "px"; - container.style.width = width + "px"; - container.style.height = height + "px"; - container.twidth = width; - container.theight = height; - } else { - container.style.left = offsetx + Math.floor((((i % rw) + 0) * w) / rw) + "px"; - container.style.top = offsety + Math.floor(((Math.floor(i / rw) + 0) * h) / rh + hi) + "px"; - container.twidth = Math.ceil(w / rw); - container.theight = Math.ceil(h / rh); - container.style.width = container.twidth + "px"; - container.style.height = container.theight + "px"; - } - - var maxWidth = 0; - if (parseInt(computed.width) > parseInt(container.style.width)) { - maxWidth = computed.width; - } else { - maxWidth = container.style.width; - } - - var maxHeight = 0; - if (parseInt(computed.height) > parseInt(container.style.height)) { - maxHeight = computed.height; - } else { - maxHeight = container.style.height; - } - if (cover === true) { - vid.style.maxWidth = maxWidth; - vid.style.maxHeight = maxHeight; - vid.style.objectFit = "cover"; - } else if (cover == 2) { - // For session.cover == 2, determine whether to use cover or contain - // based on aspect ratio comparison - vid.style.maxWidth = maxWidth; - vid.style.maxHeight = maxHeight; - - const vw = vid.naturalWidth || vid.videoWidth || 0; - const vh = vid.naturalHeight || vid.videoHeight || 0; - - if (vw && vh) { - // Calculate aspect ratios - const videoAspect = vw / vh; - - // Use container dimensions for comparison - const containerWidth = parseFloat(maxWidth); - const containerHeight = parseFloat(maxHeight); - const containerAspect = containerWidth / containerHeight; - - // If video is wider than container proportionally (width being squished), - // use cover. Otherwise use contain. - if (videoAspect > containerAspect) { - vid.style.objectFit = "cover"; - } else { - vid.style.objectFit = "contain"; - } - } else { - // Default to contain if we can't determine dimensions - vid.style.objectFit = "contain"; - } - } else { - vid.style.objectFit = "contain"; - vid.style.maxWidth = maxWidth; - vid.style.maxHeight = maxHeight; - } - - //try { - if (vid.alreadyAdded && vid.alreadyAdded == true) { - if (!container.holder) { - var holder = document.createElement("div"); - container.holder = holder; - holder.className = "holder"; - holder.dataset.holder = true; - container.appendChild(holder); - holder.appendChild(vid); - } else { - var holder = container.holder; - } - } else if (vid.dataset.doNotMove) { - vid.style.position = "absolute"; - vid.style.left = left + "px"; - vid.style.top = top + "px"; - vid.style.width = width + "px"; - vid.style.height = height + "px"; - vid.style.display = "flex"; - i += 1; - return; - } else { - if (!container.holder) { - var holder = document.createElement("div"); - container.holder = holder; - holder.className = "holder"; - holder.dataset.holder = true; - holder.appendChild(vid); - container.appendChild(holder); - } else { - var holder = container.holder; - holder.prepend(vid); - } - playarea.appendChild(container); - vid.style.maxWidth = "100%"; - vid.style.maxHeight = "100%"; - } - - if (layout) { - var wrw = (w / 100) * layout[vid.dataset.sid].w || 0; - var hrh = (h / 100) * layout[vid.dataset.sid].h || 0; - } else { - var wrw = w / rw; - var hrh = h / rh; - } - - if (backgroundMedia) { - container.style.backgroundImage = "url(" + backgroundMedia + ")"; - if (cover) { - container.style.backgroundSize = "cover"; - } else { - container.style.backgroundSize = "contain"; - } - container.style.backgroundPosition = "center"; - container.style.backgroundRepeat = "no-repeat"; - } else if (container.style.backgroundImage) { - container.style.backgroundImage = "unset"; - } - - if (foregroundMedia) { - if (!container.foregroundMedia) { - container.foregroundMedia = document.createElement("img"); - container.foregroundMedia.className = "foregroundMedia"; - container.appendChild(container.foregroundMedia); - } - container.foregroundMedia.src = foregroundMedia; - } else if (container.foregroundMedia) { - try { - container.foregroundMedia.remove(); - delete container.foregroundMedia; - } catch (e) { - errorlog(e); - } - } - - if (textOverlay && !container.textOverlay) { - container.appendChild(textOverlay); - } - - if ("rotated" in vid && vid.rotated !== false) { - if (vid.dataset) { - vid.dataset.rotated = vid.rotated ? vid.rotated : "0"; - } - updateVideoTransform(vid); - } else if (vid.dataset && vid.dataset.rotated) { - vid.dataset.rotated = "0"; - updateVideoTransform(vid); - } - - vid.style.width = "100%"; - vid.style.height = "100%"; - holder.style.position = "absolute"; - - if (vid.classList.contains("paused")) { - if (holder.paused) { - holder.paused.className = "playButton"; - } else { - var paused = document.createElement("span"); - paused.id = "paused_" + vid.dataset.UUID; - paused.className = "playButton"; - paused.dataset.UUID = vid.dataset.UUID; - paused.onclick = function () { - unPauseVideo(vid); - }; - holder.paused = paused; - holder.appendChild(paused); - } - } else if (holder.paused) { - holder.paused.className = "hidden"; - } - - var vw = vid.naturalWidth || vid.videoWidth; // naturalWidth is for images I guess - var vh = vid.naturalHeight || vid.videoHeight; - - // log(vw + " : "+vh); - - if (cover && !session.structure) { - ////// - if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) { - holder.style.left = borderOffset + "px"; - holder.style.top = borderOffset + "px"; - holder.style.height = "calc(100% - " + videoMargin * 2 + "px)"; - holder.style.width = "calc(100% - " + videoMargin * 2 + "px)"; - - vid.style.width = height - (borderOffset + videoMargin) * 2 + "px"; - vid.style.height = width - (borderOffset + videoMargin) * 2 + "px"; - vid.style.left = 0; - vid.style.top = 0; - } else { - holder.style.left = videoMargin + "px"; - holder.style.top = videoMargin + "px"; - holder.style.height = "calc(100% - " + videoMargin * 2 + "px)"; - holder.style.width = "calc(100% - " + videoMargin * 2 + "px)"; - - vid.style.width = "100%"; - vid.style.height = "100%"; - vid.style.left = 0; - vid.style.top = 0; - } - - ////////// COVER VERSION - if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) { - // do not dynamically scale the screen share feed. - } else if (session.dynamicScale) { - if (vid.dataset.UUID) { - let targetWidth = wrw; - let targetHeight = hrh; - targetWidth -= (borderOffset + videoMargin) * 2; - targetHeight -= (borderOffset + videoMargin) * 2; - if (targetWidth < 0) { - targetWidth = 0; - } - if (targetHeight < 0) { - targetHeight = 0; - } - if (session.devicePixelRatio) { - session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only - } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) { - session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover); - } else { - session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover); - } - } - } - - vid.style.borderColor = borderColor; - vid.style.borderWidth = borderOffset + "px"; - vid.style.borderRadius = borderRadius + "px"; - holder.style.borderColor = borderColor; - holder.style.borderWidth = "0px"; - holder.style.borderRadius = borderRadius + "px"; - } else if ((vw && vh) || (vid.width && vid.height) || vid.dataset.aspectRatio) { - if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) { - if (vw && vh) { - var vvw = parseInt(vh); - var vvh = parseInt(vw); - } else if (vid.width && vid.height) { - var vvw = parseInt(vid.height); - var vvh = parseInt(vid.width); - } else { - // video disabled; fall back to aspect Ratio - var vvw = 1; - var vvh = vid.dataset.aspectRatio; - } - - vid.style.objectFit = "cover"; //contain; - vid.style.overflow = "unset"; //contain; - //vid.style.maxWidth = "unset"; - //vid.style.maxHeight = "unset"; - } else { - if (vw && vh) { - var vvw = parseInt(vw); - var vvh = parseInt(vh); - } else if (vid.width && vid.height) { - var vvw = parseInt(vid.width); - var vvh = parseInt(vid.height); - } else { - var vvw = vid.dataset.aspectRatio; - var vvh = 1; - } - } - - var asw = (wrw - videoMargin * 2 - borderOffset * 2) / vvw; // (window.innerWidth/ N) / vid.videoHeight; - var ash = (hrh - videoMargin * 2 - borderOffset * 2) / vvh; - - if (session.structure) { - // wrw x hrh - var arx = (wrw - videoMargin * 2 - borderOffset * 2) / (hrh - videoMargin * 2 - borderOffset * 2); - var tarx = arW / arH; - //var arW = 16.0; - //var arH = 9.0; - if (arx > tarx) { - // width is too long - var hsw = hrh * tarx - videoMargin * 2 * tarx - borderOffset * 2; - var hsl = (wrw - hsw) / 2; - var hst = videoMargin; - var hsh = hrh - videoMargin * 2; - } else { - var hsh = (wrw - videoMargin * 2 + borderOffset * 2) / tarx; - var hst = (hrh - hsh) / 2; - var hsl = videoMargin; - var hsw = wrw - videoMargin * 2; - } - } else if (asw > ash) { - var hsh = hrh - videoMargin * 2; - var hst = videoMargin; - var hsw = (hsh - borderOffset * 2) * (vvw / vvh) + borderOffset * 2; - var hsl = (wrw - hsw) / 2; - } else { - var hsw = wrw - videoMargin * 2; - var hsl = videoMargin; - var hsh = (hsw - borderOffset * 2) / (vvw / vvh) + borderOffset * 2; - var hst = (hrh - hsh) / 2; - } - - holder.style.left = Math.floor(hsl) + "px"; // this needs to be replaced with padding. This means testing with rotation = 90 - holder.style.top = Math.floor(hst) + "px"; - holder.style.width = Math.ceil(hsw) + "px"; - holder.style.height = Math.ceil(hsh) + "px"; - //holder.style.padding = videoMargin + "px"; - - holder.style.borderColor = borderColor; - holder.style.borderWidth = borderOffset + "px"; - holder.style.borderRadius = borderRadius + "px"; - vid.style.borderWidth = "0px"; - - if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) { - vid.style.width = Math.ceil(wrw - borderOffset * 2) + "px"; - vid.style.height = Math.ceil(hsw - borderOffset * 2) + "px"; - vid.style.left = 0; - - vid.style.maxWidth = "100vh"; - vid.style.maxHeight = "100vw"; - - if (ChromiumVersion && ChromiumVersion < 77) { - if (!animated && parseInt(container.style.width) > parseInt(holder.style.height)) { - vid.style.position = "relative"; - vid.style.objectFit = "contain"; //contain; - } else if (animated && container.twidth && parseInt(container.twidth) > parseInt(holder.style.height)) { - vid.style.position = "relative"; - vid.style.objectFit = "contain"; //contain; - } - } else { - vid.style.position = "relative"; - } - } else if (session.blurBackground !== false && vid.nodeName == "VIDEO" && !container.blurred && vid.srcObject && ((asw > 1 && ash > 1) || asw >= 1 || ash >= 1)) { - vid.srcObject.getVideoTracks().forEach(trk => { - if (!container.blurred) { - container.blurred = document.createElement("video"); - container.blurred.controls = false; - container.blurred.style = "z-index:-10000;position:absolute;left:0;width:100%;height:100%;top:0;object-fit:fill;-webkit-filter: blur(" + session.blurBackground + "px)"; - container.blurred.srcObject = createMediaStream(); - container.blurred.srcObject.addTrack(trk); - container.blurred.play(); - holder.appendChild(container.blurred); - } else { - if (container.blurred.paused) { - container.blurred.play(); - } - } - }); - } else if (session.blurBackground !== false && vid.nodeName == "VIDEO" && vid.srcObject && ((asw > 1 && ash > 1) || asw >= 1 || ash >= 1)) { - if (container.blurred.paused) { - container.blurred.play(); - } - //container.blurred.style = "z-index:-10000;position:absolute;left:0;width:100%;height:100%;top:0;object-fit:fill;-webkit-filter: blur("+session.blurBackground+"px)"; - } else if (container.blurred) { - try { - container.blurred.remove(); - } catch (e) { } - try { - delete container.blurred; - } catch (e) { } - } - - ////////// NON-COVER VERSION (based on holder) - if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) { - // do not dynamically scale the screen share feed. - } else if (session.dynamicScale) { - if (vid.dataset.UUID) { - let targetWidth = wrw; - let targetHeight = hrh; - targetWidth -= (borderOffset + videoMargin) * 2; - targetHeight -= (borderOffset + videoMargin) * 2; - if (targetWidth < 0) { - targetWidth = 0; - } - if (targetHeight < 0) { - targetHeight = 0; - } - if (session.devicePixelRatio) { - session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only - } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) { - session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover); - } else { - session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover); - } - } - } - } else { - holder.style.left = borderOffset + videoMargin + "px"; - holder.style.top = borderOffset + videoMargin + "px"; - holder.style.height = "calc(100% - " + (borderOffset + videoMargin * 2) + "px)"; - holder.style.width = "calc(100% - " + (borderOffset + videoMargin * 2) + "px)"; - - ////////// UNKNOWN VERSION - if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) { - // do not dynamically scale the screen share feed. - } else if (session.dynamicScale) { - if (vid.dataset.UUID) { - let targetWidth = wrw; - let targetHeight = hrh; - targetWidth -= (borderOffset + videoMargin) * 2; - targetHeight -= (borderOffset + videoMargin) * 2; - if (targetWidth < 0) { - targetWidth = 0; - } - if (targetHeight < 0) { - targetHeight = 0; - } - if (session.devicePixelRatio) { - session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only - } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) { - session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover); - } else { - session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover); - } - } - } - /////////////// - holder.style.borderColor = borderColor; - holder.style.borderWidth = borderOffset + "px"; - holder.style.borderRadius = borderRadius + "px"; - vid.style.borderWidth = "0px"; - } - - if (session.colorVideosBackground) { - vid.style.backgroundColor = session.colorVideosBackground; - } else { - vid.style.backgroundColor = "unset"; - } - - if ((vid.dataset.UUID && session.rpcs && session.rpcs[vid.dataset.UUID] && - ( - ("label" in session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].label !== false && session.showlabels === true) || - (session.showmeta === true && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].meta) - )) - || - ((session.showlabels === true || session.showmeta === true) && - (((vid.id === "videosource") && session.label) || vid.labelText || session.meta) - )) { - - if (animated && container.twidth && container.theight) { - var vidwidth = container.twidth; - var vidheight = container.theight; - } else { - var vidwidth = vid.offsetWidth; - var vidheight = vid.offsetHeight; - } - - var fontsize = (vidwidth + vidheight) * 0.03; - if (vidwidth / 16 >= vidheight / 9) { - var voar = vidwidth / 16 / (vidheight / 9); - } else { - var voar = vidheight / 9 / (vidwidth / 16); - } - voar = Math.pow(voar, 0.5); - fontsize = fontsize / voar; - - if (holder.label) { - var label = holder.label; - } else { - var label = document.createElement("span"); - holder.label = label; - if (session.labelstyle) { - label.className = "video-label " + session.labelstyle; - } else { - label.className = "video-label"; - } - holder.appendChild(label); - } - - if (fontsize) { - if (session.labelsize) { - fontsize = (fontsize * session.labelsize) / 100; - } - label.style.fontSize = parseInt(fontsize) + "px"; - } - - if (( - vid.dataset.UUID && - session.rpcs && - session.rpcs[vid.dataset.UUID] && - ( - // Label check - ( - "label" in session.rpcs[vid.dataset.UUID] && - session.rpcs[vid.dataset.UUID].label !== false && - session.showlabels === true - ) - || - // Meta check with explicit undefined checks - ( - session.showmeta === true && - session.rpcs[vid.dataset.UUID] && - session.rpcs[vid.dataset.UUID].meta - ) - ) - ) - || - ( - (session.showlabels === true || session.showmeta === true) && - ( - ((vid.id === "videosource") && session.label) || - vid.labelText || - session.meta - ) - ) - ) { - - let labelContent = []; - let imageElements = []; - - if (session.showlabels === true) { - if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].label) { - session.rpcs[vid.dataset.UUID].label.split("\\n").forEach(label => { - labelContent.push({ type: 'text', value: label }); - }); - } else if ((vid.id === "videosource") && session.label || vid.labelText) { - (vid.labelText || session.label).split("\\n").forEach(label => { - labelContent.push({ type: 'text', value: label }); - }); - } - } - - if (session.showmeta === true) { - let metaData = []; - if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].meta) { - metaData = Object.entries(session.rpcs[vid.dataset.UUID].meta); - } else if (session.meta) { - metaData = Object.entries(session.meta); - } - metaData.forEach(([key, value]) => { - if (value && typeof value === 'object') { - if (value.type && value.type == 'file') { - // console.log(value); - if (value.filetype) { - imageElements.push(value); - } - } else { - labelContent.push({ - type: 'text', - value: value.value || JSON.stringify(value) - }); - } - } else if (value) { - labelContent.push({ type: 'text', value: value }); - } - }); - } - - // Create or get label container - if (!holder.labelContainer) { - holder.labelContainer = document.createElement("div"); - holder.labelContainer.className = "video-label-container"; - holder.appendChild(holder.labelContainer); - } - - // Handle text content - if (labelContent.length > 0) { - if (!holder.label) { - holder.label = document.createElement("div"); - if (session.labelstyle) { - holder.label.className = "video-label " + session.labelstyle; - } else { - holder.label.className = "video-label"; - } - holder.labelContainer.appendChild(holder.label); - } - // Apply font size adjustments as before - if (fontsize) { - if (session.labelsize) { - fontsize = (fontsize * session.labelsize) / 100; - } - holder.label.style.fontSize = parseInt(fontsize) + "px"; - } - holder.label.innerHTML = labelContent - .map(item => `${escapeHtml(String(item.value))}`) - .join(""); - } - - // Handle image content - if (imageElements.length > 0) { - if (!holder.imageContainer) { - holder.imageContainer = document.createElement("div"); - holder.imageContainer.className = "video-image-container"; - holder.imageContainer.style.position = "absolute"; - holder.imageContainer.style.top = "10px"; - holder.imageContainer.style.right = "10px"; - holder.imageContainer.style.display = "flex"; - holder.imageContainer.style.alignItems = "flex-start"; - holder.imageContainer.style.justifyContent = "flex-end"; - holder.appendChild(holder.imageContainer); - } - holder.imageContainer.style.maxWidth = "min(20vw, calc(" + holder.style.width + " * 0.25))"; - holder.imageContainer.style.maxHeight = "min(20vh, calc(" + holder.style.height + " * 0.25))"; - holder.imageContainer.style.minWidth = "max(7vw, calc(" + holder.style.width + " * 0.22), 30px)"; - holder.imageContainer.style.minHeight = "max(7vw, calc(" + holder.style.height + " * 0.22), 30px)"; - - // Clear existing images - holder.imageContainer.innerHTML = ''; - // Add new images - imageElements.forEach(meta => { - if (meta.filetype.startsWith("image/")) { - const imgElement = document.createElement("img"); - imgElement.src = meta.value; - imgElement.className = `meta-image ${meta.templateName || ''}`; - imgElement.style.width = "auto"; // Fill container width - imgElement.style.height = "auto"; // Fill container height - imgElement.style.objectFit = "contain"; // Maintain aspect ratio - imgElement.style.position = "absolute"; - imgElement.style.maxWidth = "100%"; - imgElement.style.maxHeight = "100%"; - holder.imageContainer.appendChild(imgElement); - } - }); - } - } - } else if (holder.label) { - holder.label.remove(); - delete holder.label; - } - - if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID]) { - if (session.rpcs[vid.dataset.UUID].voiceMeter) { - holder.appendChild(session.rpcs[vid.dataset.UUID].voiceMeter); - } - if (session.rpcs[vid.dataset.UUID].remoteMuteElement) { - holder.appendChild(session.rpcs[vid.dataset.UUID].remoteMuteElement); - } - - if (session.signalMeter) { - if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].signalMeter) { - session.rpcs[vid.dataset.UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true); - session.rpcs[vid.dataset.UUID].signalMeter.classList.remove("hidden"); - session.rpcs[vid.dataset.UUID].signalMeter.id = "signalMeter_" + vid.dataset.UUID; - session.rpcs[vid.dataset.UUID].signalMeter.dataset.level = 0; - session.rpcs[vid.dataset.UUID].signalMeter.title = getTranslation("signal-meter"); - holder.appendChild(session.rpcs[vid.dataset.UUID].signalMeter); - holder.signalMeter = session.rpcs[vid.dataset.UUID].signalMeter; - } else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].signalMeter) { - if (!holder.signalMeter) { - holder.appendChild(session.rpcs[vid.dataset.UUID].signalMeter); - holder.signalMeter = session.rpcs[vid.dataset.UUID].signalMeter; - } - } - } - - if (session.batteryMeter) { - if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].batteryMeter) { - session.rpcs[vid.dataset.UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true); - session.rpcs[vid.dataset.UUID].batteryMeter.classList.remove("hidden"); - session.rpcs[vid.dataset.UUID].batteryMeter.id = "batteryMeter_" + vid.dataset.UUID; - session.rpcs[vid.dataset.UUID].batteryMeter.dataset.level = 0; - session.rpcs[vid.dataset.UUID].batteryMeter.title = getTranslation("battery-meter"); - holder.appendChild(session.rpcs[vid.dataset.UUID].batteryMeter); - holder.batteryMeter = session.rpcs[vid.dataset.UUID].batteryMeter; - } else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].batteryMeter) { - if (!holder.batteryMeter) { - holder.appendChild(session.rpcs[vid.dataset.UUID].batteryMeter); - holder.batteryMeter = session.rpcs[vid.dataset.UUID].batteryMeter; - } - } - } - - if (session.volumeControl && session.rpcs[vid.dataset.UUID].videoElement && vid.tagName != "VIDEO") { - if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].volumeControl) { - session.rpcs[vid.dataset.UUID].volumeControl = getById("volumeControlTemplate").cloneNode(true); - session.rpcs[vid.dataset.UUID].volumeControl.classList.remove("hidden"); - session.rpcs[vid.dataset.UUID].volumeControl.id = "volumeControl_" + vid.dataset.UUID; - session.rpcs[vid.dataset.UUID].volumeControl.value = parseInt(session.rpcs[vid.dataset.UUID].videoElement.volume * 100); - session.rpcs[vid.dataset.UUID].volumeControl.dataset.UUID = vid.dataset.UUID; - session.rpcs[vid.dataset.UUID].volumeControl.title = getTranslation("volume-control"); - session.rpcs[vid.dataset.UUID].volumeControl.oninput = function () { - if (this.dataset.UUID && session.rpcs[this.dataset.UUID] && session.rpcs[this.dataset.UUID].videoElement) { - session.rpcs[this.dataset.UUID].videoElement.volume = parseFloat(this.value / 100); - } - }; - holder.appendChild(session.rpcs[vid.dataset.UUID].volumeControl); - holder.volumeControl = session.rpcs[vid.dataset.UUID].volumeControl; - } else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].volumeControl) { - if (!holder.volumeControl && session.rpcs[vid.dataset.UUID].videoElement) { - holder.appendChild(session.rpcs[vid.dataset.UUID].volumeControl); - holder.volumeControl = session.rpcs[vid.dataset.UUID].volumeControl; - session.rpcs[vid.dataset.UUID].volumeControl.value = parseInt(session.rpcs[vid.dataset.UUID].videoElement.volume * 100); - } - } - } - - if (session.showConnections) { - if (!session.rpcs[vid.dataset.UUID].connectionDetails) { - createConnectionDetailsEle(vid.dataset.UUID); - } - holder.appendChild(session.rpcs[vid.dataset.UUID].connectionDetails); - } - } - - if (session.ruleOfThirds) { - if (vid.id == "videosource") { - if (!holder.svg) { - var svg = document.createElement("img"); - svg.src = session.ruleOfThirds; - svg.style.width = "100%"; - svg.style.height = "100%"; - svg.style.position = "absolute"; - svg.style.left = "0"; - svg.style.top = "0"; - svg.style.pointerEvents = "none"; - holder.svg = svg; - holder.appendChild(svg); - } - } - } - - try { - if (!(session.cleanOutput && session.cleanish == false)) { - if (session.firstPlayTriggered === false) { - // don't play unless needed; might cause clicking or who knows what else. - if (vid.tagName.toLowerCase() == "video") { - // we don't want to try playing an Iframe or Canvas. - if (vid.paused) { - warnlog("VIDEO IS NOT PLAYING"); - var playPromise = vid.play(); - if (playPromise !== undefined) { - playPromise - .then(_ => { - // playing - //session.firstPlayTriggered=true; // global tracking. "user gesture obtained", so no longer needed if playing. - }) - .catch(err => { - var bigPlayButton = document.getElementById("bigPlayButton"); - if (bigPlayButton) { - bigPlayButton.innerHTML = ''; - bigPlayButton.style.display = "block"; - } - }); - } else { - session.firstPlayTriggered = true; // well, I don't know if it's playing, and so whatever. fail gracefully. - } - } - } - } - } - } catch (e) { - errorlog(e); - var bigPlayButton = document.getElementById("bigPlayButton"); - if (bigPlayButton) { - bigPlayButton.parentNode.removeChild(bigPlayButton); - } - } - - if (vid.nodeName == "IFRAME") { - // I need to add this back in at some point. - i += 1; - return; - } - - if (!session.cleanOutput && !session.nocursor && !session.nofullwindowbutton) { - if (session.roomid !== false && session.scene === false) { - if (!(vid.id === "videosource" && miniPreview) && !session.infocusForceMode) { - if (!holder.button) { - var button = document.createElement("div"); - holder.button = button; - holder.appendChild(button); - } else { - var button = holder.button; - } - button.id = "button_" + vid.id; - button.dataset.button = true; - if (soloVideo) { - button.innerHTML = ""; - button.title = "Show all active videos togethers"; - button.style.visibility = "visible"; - } else if (mpl > 1 || session.fullscreenButton) { - // with session.fullscreenButton we hide the actuall full screen button, so this replaces it - button.innerHTML = ""; - button.title = "Enlarge video and increase its clarity"; - button.style.visibility = "visible"; - } else { - button.style.visibility = "hidden"; - } - button.classList.add("fullwindowButton"); - if (vid.id == "videosource") { - button.onclick = function (event) { - if (session.infocus === true) { - session.infocus = false; - } else { - session.infocus = true; - } - setTimeout(() => updateMixer(), 10); - if (session.fullscreenButton) { - if (session.infocus) { - fullscreenPageToggle(true); - } else { - fullscreenPageToggle(false); - } - } - }; - } else { - button.dataset.UUID = vid.dataset.UUID; - button.onclick = function (event) { - var target = event.currentTarget; - if (session.infocus === target.dataset.UUID) { - //target.childNodes[0].className = 'las la-arrows-alt'; - session.infocus = false; - } else { - //target.childNodes[0].className = 'las la-compress'; - session.infocus = target.dataset.UUID; - //log("session:"+target.dataset.UUID); - } - if (session.fullscreenButton) { - if (session.infocus) { - fullscreenPageToggle(true); - } else { - fullscreenPageToggle(false); - } - } - setTimeout(() => updateMixer(), 10); - }; - } - vid.onclick = function (event) { - if (session.disableMouseEvents) { - return; - } - button.style.display = "block"; - container.style.backgroundColor = "#4444"; - button.style.opacity = "100%"; - }; - button.onmouseenter = function (event) { - if (session.disableMouseEvents) { - return; - } - button.style.display = "block"; - container.style.backgroundColor = "#4444"; - setTimeout( - function (button) { - button.style.opacity = "100%"; - }, - 1, - button - ); - }; - button.onmousemove = function (event) { - if (session.disableMouseEvents) { - return; - } - button.style.display = "block"; - container.style.backgroundColor = "#4444"; - button.style.opacity = "100%"; - }; - container.onmousemove = function (event) { - if (session.disableMouseEvents) { - return; - } - button.style.display = "block"; - container.style.backgroundColor = "#4444"; - button.style.opacity = "100%"; - }; - container.onmouseenter = function (event) { - if (session.disableMouseEvents) { - return; - } - button.style.display = "block"; - container.style.backgroundColor = "#4444"; - setTimeout( - function (button) { - button.style.opacity = "100%"; - }, - 1, - button - ); - }; - container.onmouseleave = function (event) { - if (session.disableMouseEvents) { - return; - } - button.style.display = "none"; - container.style.backgroundColor = null; - button.style.opacity = "10%"; - }; - } else if (vid.id === "videosource" && miniPreview && soloVideo == true && !session.infocusForceMode) { - if (!holder.button) { - var button = document.createElement("div"); - holder.button = button; - holder.appendChild(button); - } else { - var button = holder.button; - } - button.id = "button_videosource"; - button.dataset.button = true; - if (soloVideo) { - button.innerHTML = ""; - button.title = "Show all active videos togethers"; - button.style.display = "unset"; - } else { - button.style.visibility = "hidden"; - button.style.display = "none"; - } - button.classList.add("fullwindowButton"); - button.onclick = function (event) { - event.stopPropagation(); - event.preventDefault(); - if (session.infocus === true) { - session.infocus = false; - setTimeout(() => updateMixer(), 10); - } - if (session.fullscreenButton) { - if (session.infocus) { - fullscreenPageToggle(true); - } else { - fullscreenPageToggle(false); - } - } - }; - } else if (session.infocusForceMode && holder.button) { - try { - holder.button.remove(); - } catch (e) { - errorlog(e); - } - } - } - } - i += 1; - } catch (err) { - errorlog(err); - } - }); - for (var uid in delayedRequestList) { - session.requestRateLimit(...delayedRequestList[uid]); - } - updateUserList(); -} - -var translationBacklog = []; - -function miniTranslate(ele, ident = false, direct = false) { - if (!translation) { - translation = {}; - } - - if (ident) { - if (translation.innerHTML && ident in translation.innerHTML) { - if (ele.querySelector("[data-translate]")) { - ele.querySelector("[data-translate]").innerHTML = translation.innerHTML[ident]; - ele.querySelector("[data-translate]").dataset.translate = ident; - } else { - ele.innerHTML = translation.innerHTML[ident]; - ele.dataset.translate = ident; - } - return; - } else if (direct) { - if (ele.querySelector("[data-translate]")) { - ele.querySelector("[data-translate]").innerHTML = direct; - ele.querySelector("[data-translate]").dataset.translate = ident; - } else { - ele.dataset.translate = ident; - ele.innerHTML = direct; - } - return; - } else { - if (!(ident in miscTranslations)) { - var value = ident.replaceAll("-", " "); // lets use the key as the translation - } else { - var value = miscTranslations[ident]; // lets use a miscellaneous translation as backup? - } - - if (ele.querySelector("[data-translate]")) { - ele.querySelector("[data-translate]").innerHTML = value; - ele.querySelector("[data-translate]").dataset.translate = ident; - } else { - ele.innerHTML = value; - ele.dataset.translate = ident; - } - return; - } - } - - var allItems = ele.querySelectorAll("[data-translate]"); - allItems.forEach(function (ele2) { - if (translation.innerHTML && ele2.dataset.translate in translation.innerHTML) { - ele2.innerHTML = translation.innerHTML[ele2.dataset.translate]; - } else if (translation.miscellaneous && ele2.dataset.translate in translation.miscellaneous) { - ele2.innerHTML = translation.miscellaneous[ele2.dataset.translate]; - } - }); - if (ele.dataset) { - if (translation.innerHTML && ele.dataset.translate in translation.innerHTML) { - ele.innerHTML = translation.innerHTML[ele.dataset.translate]; - } else if (translation.miscellaneous && ele.dataset.translate in translation.miscellaneous) { - ele.innerHTML = translation.miscellaneous[ele.dataset.translate]; - } - } - if (translation.titles) { - var allTitles = ele.querySelectorAll("[title]"); - allTitles.forEach(function (ele2) { - if (ele.dataset.key) { - var key = ele2.dataset.key; - } else { - var key = ele2.title.replace(/[\W]+/g, "-").toLowerCase(); - ele2.dataset.key = key; - } - - if (key in translation.titles) { - ele2.title = translation.titles[key]; - } else if (ele2.dataset.translate && ele2.dataset.translate in translation.titles) { - ele2.title = translation.titles[ele2.dataset.translate]; - } - }); - if (ele.title) { - if (ele.dataset.key) { - var key = ele.dataset.key; - } else { - var key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); - ele.dataset.key = key; - } - - if (key in translation.titles) { - ele.title = translation.titles[key]; - } else if (ele.dataset.translate && ele.dataset.translate in translation.titles) { - ele.title = translation.titles[ele.dataset.translate]; - } - } - } - if (translation.placeholders) { - var allPlaceholders = ele.querySelectorAll("[placeholder]"); - allPlaceholders.forEach(function (ele2) { - var key = ele2.placeholder.replace(/[\W]+/g, "-").toLowerCase(); - if (key in translation.placeholders) { - ele2.placeholder = translation.placeholders[key]; - } - }); - - if (ele.placeholder) { - var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); - if (key in translation.placeholders) { - ele.placeholder = translation.placeholders[key]; - } - } - } -} - -async function changeLg(lang, rtl = false, save = false) { - log("changeLg: " + lang); - var retry = false; - if (lang == "auto") { - const navlang = navigator.language || navigator.userLanguage || ""; - const userLang = navlang.toLowerCase(); // Convert to lower case - lang = userLang.replace("_", "-"); // Replace dash with underscore if present - retry = true; - } - - await fetchWithTimeout("./translations/" + lang + ".json", 2000) - .then(async response => { - try { - if (response.status !== 200) { - getById("mainmenu").style.opacity = 1; - if (retry) { - console.warn("Couldn't find the exact language file for '" + lang + "'; trying a more generic option instead"); - lang = lang.split("-")[0]; - if (lang && lang !== "auto") { - await changeLg(lang, rtl); // Retry with a more generic language code. - } - } - return; - } - await response - .json() - .then(async function (data) { - translation = data; // translation.innerHTML[ele.dataset.translate] - document.documentElement.dir = rtl ? "rtl" : "ltr"; - document.documentElement.setAttribute('lang', lang); - document.body.classList.remove('rtl'); - document.body.classList.remove('ltr'); - document.body.classList.add(rtl ? 'rtl' : 'ltr'); - document.documentElement.style.setProperty('--rtl-or-ltr', rtl ? 'right' : 'left'); - if (save) { - try { - localStorage.setItem("vdo_ninja_language", lang); - } catch (e) { - console.warn("Could not save language to localStorage", e); - } - } - if (translation.miscellaneous) { - Object.keys(translation.miscellaneous).forEach(key => { - miscTranslations[key] = translation.miscellaneous[key]; - }); - } - translation.miscellaneous = miscTranslations; - var allItems = document.querySelectorAll("[data-translate]"); - allItems.forEach(function (ele) { - if (ele.dataset.translate in translation.innerHTML) { - ele.innerHTML = translation.innerHTML[ele.dataset.translate]; - } else if (translation.miscellaneous && ele.dataset.translate in translation.miscellaneous) { - ele.innerHTML = translation.miscellaneous[ele.dataset.translate]; // use the misc translation if no main one is found - } - }); - var allTitles = document.querySelectorAll("[title]"); - allTitles.forEach(function (ele) { - if (ele.dataset.key) { - var key = ele.dataset.key; - } else { - var key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); - ele.dataset.key = key; - } - if (key in translation.titles) { - ele.title = translation.titles[key]; - } else if (ele.dataset.translate && translation.titles && ele.dataset.translate in translation.titles) { - ele.title = translation.titles[ele.dataset.translate]; - } - }); - var allPlaceholders = document.querySelectorAll("[placeholder]"); - allPlaceholders.forEach(function (ele) { - var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); - if (key in translation.placeholders) { - ele.placeholder = translation.placeholders[key]; - } - }); - if (translationBacklog.length) { - for (var i = 0; i < translationBacklog.length; i++) { - try { - miniTranslate(translationBacklog[i][0], translationBacklog[i][1]); - } catch (e) { } - } - translationBacklog = []; - } - getById("mainmenu").style.opacity = 1; - }) - .catch(async err2 => { - if (retry) { - console.warn("Couldn't find the exact language file for '" + lang + "'; trying a more generic option instead"); - lang = lang.split("-")[0]; - if (lang && lang !== "auto") { - await changeLg(lang, rtl); // won't retry I'd hope. - } - } else { - errorlog(err2); - } - }); - } catch (e) { - getById("mainmenu").style.opacity = 1; - } - }) - .catch(async err => { - if (retry) { - console.warn("Couldn't find exact language; trying a more generic option instead"); - lang = lang.split("-")[0]; - if (lang && lang !== "auto") { - await changeLg(lang, rtl); // won't retry I'd hope. - } - } else { - errorlog(err); - } - }); -} - -var controlBarTimeout = false; -function showControl(e) { - if (controlBarTimeout) { - clearTimeout(controlBarTimeout); - } - if (session.mobile) { - getById("controlButtons").classList.remove("partialFadeout"); - } else { - getById("controlButtons").classList.remove("fadeout"); - } - controlBarTimeout = setTimeout(function () { - if (session.mobile) { - getById("controlButtons").classList.add("partialFadeout"); - } else { - getById("controlButtons").classList.add("fadeout"); - } - }, 5000); -} - -var loadedQRCode = false; -function loadQR(callback = false, value = false) { - if (loadedQRCode === false) { - loadedQRCode = true; - var script = document.createElement("script"); - if (callback) { - script.onload = function () { - callback(value); - }; - } - script.src = "./thirdparty/qrcode.min.js"; // dynamically load this only if its needed. Keeps loading time down. - document.head.appendChild(script); - } else if (callback) { - callback(value); - } -} - -function showInviteQR() { - var inviteURL = getById("inviteLinkURL").href; - warnUser("Loading QR Code..."); - loadQR(function(url) { - getById("alertModalMessage").innerHTML = ""; - var qrcode = new QRCode(getById("alertModalMessage"), { - width: 300, - height: 300, - colorDark: "#000000", - colorLight: "#FFFFFF", - useSVG: false - }); - qrcode.makeCode(url); - getById("alertModalMessage").title = ""; - setTimeout(function() { - getById("alertModalMessage").title = ""; - if (getById("alertModalMessage").getElementsByTagName("img").length) { - getById("alertModalMessage").getElementsByTagName("img")[0].style.cursor = "none"; - } - }, 100); - }, inviteURL); -} - -if (typeof session.pendingFramegrabAudioSettings === "undefined") { - session.pendingFramegrabAudioSettings = null; -} -if (typeof session.framegrabAudioPending === "undefined") { - session.framegrabAudioPending = false; -} -if (typeof session.framegrabAudioRetryTimer === "undefined") { - session.framegrabAudioRetryTimer = null; -} -if (typeof session.framegrabAudioRetryAttempts === "undefined") { - session.framegrabAudioRetryAttempts = 0; -} -if (typeof session.pendingMicRefreshTimeout === "undefined") { - session.pendingMicRefreshTimeout = null; -} - -session.updateFramegrabAudioUI = function (enable) { - try { - const controls = getById("controlButtons"); - const micButton = getById("mutebutton"); - const speakerButton = getById("mutespeakerbutton"); - if (enable) { - if (controls) { - controls.classList.remove("hidden"); - } - if (micButton) { - micButton.classList.remove("hidden"); - micButton.style.removeProperty("display"); - } - if (speakerButton) { - speakerButton.classList.remove("hidden"); - speakerButton.style.removeProperty("display"); - } - } else if (speakerButton) { - speakerButton.classList.add("hidden"); - } - } catch (err) { - errorlog(err); - } -}; - -session.clearFramegrabAudioTracks = function (stopTracks = true) { - const removeTracks = stream => { - if (!stream) { - return; - } - try { - stream.getAudioTracks().forEach(track => { - try { - stream.removeTrack(track); - } catch (err) { } - if (stopTracks) { - try { - track.stop(); - } catch (stopErr) { } - } - }); - } catch (err) { } - }; - removeTracks(session.streamSrc); - removeTracks(session.streamSrcClone); - if (session.videoElement && session.videoElement.srcObject) { - removeTracks(session.videoElement.srcObject); - } - session.framegrabAudioInitialized = false; - session.framegrabAudioAutoSelectionApplied = false; - session.framegrabAudioEnabling = false; - session.framegrabAudioPending = false; - session.framegrabAudioRetryAttempts = 0; - if (session.framegrabAudioRetryTimer) { - clearTimeout(session.framegrabAudioRetryTimer); - session.framegrabAudioRetryTimer = null; - } -}; - -session.prepareFramegrabAudioPreference = function (settings = {}) { - if (!settings) { - return; - } - if (Object.prototype.hasOwnProperty.call(settings, "deviceId")) { - const deviceId = settings.deviceId; - if (deviceId === null || deviceId === false || deviceId === "") { - session.audioDevice = 0; - } else if (deviceId === 1 || deviceId === "1" || deviceId === "default") { - session.audioDevice = 1; - } else if (deviceId === "communications") { - session.audioDevice = "communications"; - } else if (Array.isArray(deviceId)) { - session.audioDevice = deviceId.filter(Boolean); - } else { - session.audioDevice = String(deviceId); - } - } else if (!session.audioDevice || session.audioDevice === 0) { - session.audioDevice = 1; - } -}; - -session.autoSelectFramegrabAudioDevice = function () { - if (!session.framegrabAudio) { - return false; - } - const audioMenu = getById("audioSource3"); - if (!audioMenu) { - return false; - } - const inputs = Array.from(audioMenu.querySelectorAll("input[type='checkbox']")); - if (!inputs.length) { - return false; - } - const hasActiveMic = inputs.some(input => input.id !== "multiselect1" && input.checked); - if (hasActiveMic) { - session.framegrabAudioAutoSelectionApplied = true; - return true; - } - const noAudioOption = inputs.find(input => input.id === "multiselect1"); - const findByValue = value => inputs.find(input => input.value === value); - let candidate = null; - const preferList = []; - if (Array.isArray(session.audioDevice) && session.audioDevice.length) { - preferList.push(...session.audioDevice); - } else if (session.audioDevice === 0) { - return false; - } else if (session.audioDevice === 1 || session.audioDevice === "1") { - preferList.push("default", "communications"); - } else if (session.audioDevice) { - preferList.push(session.audioDevice); - } - for (let i = 0; i < preferList.length; i++) { - const value = preferList[i]; - if (typeof value !== "string") { - continue; - } - const directMatch = findByValue(value); - if (directMatch) { - candidate = directMatch; - break; - } - const desired = normalizeDeviceLabel(value); - const labelMatch = inputs.find(input => { - const label = input.dataset && input.dataset.label ? input.dataset.label : (input.getAttribute ? input.getAttribute("data-label") : ""); - return label && normalizeDeviceLabel(label) === desired; - }); - if (labelMatch) { - candidate = labelMatch; - break; - } - } - if (!candidate) { - candidate = findByValue("default") || findByValue("communications"); - } - if (!candidate) { - candidate = inputs.find(input => input.id !== "multiselect1"); - } - if (!candidate) { - return false; - } - try { - candidate.checked = true; - candidate.dispatchEvent(new Event("change", { bubbles: true })); - } catch (err) { } - if (typeof SelectedAudioInputDevices !== "undefined") { - if (!Array.isArray(SelectedAudioInputDevices)) { - SelectedAudioInputDevices = []; - } - SelectedAudioInputDevices = SelectedAudioInputDevices.filter(value => value && value !== "ZZZ"); - if (!SelectedAudioInputDevices.includes(candidate.value)) { - SelectedAudioInputDevices.push(candidate.value); - } - } - if (noAudioOption) { - noAudioOption.checked = false; - } - session.framegrabAudioAutoSelectionApplied = true; - return true; -}; - -session.applyFramegrabAudioSettings = async function (settings = {}) { - const enable = !(settings && settings.enable === false); - const clearRetry = () => { - if (session.framegrabAudioRetryTimer) { - clearTimeout(session.framegrabAudioRetryTimer); - session.framegrabAudioRetryTimer = null; - } - session.framegrabAudioRetryAttempts = 0; - }; - const scheduleRetry = () => { - if (!session.framegrabAudio || !session.framegrabAudioRequested) { - return; - } - session.pendingFramegrabAudioSettings = settings || { enable: true }; - session.framegrabAudioPending = true; - const MAX_RETRIES = 6; - const RETRY_DELAY_MS = 600; - if (session.framegrabAudioRetryAttempts >= MAX_RETRIES) { - log('[FRAMEGRAB AUDIO] Retry limit reached; disabling audio'); - session.framegrabAudio = false; - session.framegrabAudioRequested = false; - session.pendingFramegrabAudioSettings = null; - session.updateFramegrabAudioUI(false); - session.clearFramegrabAudioTracks(); - session.framegrabAudioPending = false; - if (!session.cleanOutput) { - warnUser('Unable to attach an audio input for the framegrab. Please confirm microphone access and try again.', 6000); - } - return; - } - session.framegrabAudioRetryAttempts += 1; - if (session.framegrabAudioRetryTimer) { - return; - } - session.framegrabAudioRetryTimer = setTimeout(() => { - session.framegrabAudioRetryTimer = null; - if (!session.framegrabAudio || !session.framegrabAudioRequested) { - return; - } - session.applyFramegrabAudioSettings(session.pendingFramegrabAudioSettings || { enable: true }).catch(errorlog); - }, RETRY_DELAY_MS); - }; - if (!enable) { - session.pendingFramegrabAudioSettings = null; - session.framegrabAudio = false; - session.framegrabAudioRequested = false; - session.framegrabAudioPending = false; - session.updateFramegrabAudioUI(false); - clearRetry(); - session.clearFramegrabAudioTracks(); - return; - } - session.framegrabAudio = true; - session.framegrabAudioRequested = true; - session.framegrabAudioPending = false; - session.pendingFramegrabAudioSettings = settings || { enable: true }; - session.updateFramegrabAudioUI(true); - session.framegrabAudioAutoSelectionApplied = false; - session.prepareFramegrabAudioPreference(settings || {}); - const overrideConstraints = (() => { - if (!settings || typeof settings !== "object") { - return false; - } - if (!Object.prototype.hasOwnProperty.call(settings, "deviceId")) { - return false; - } - let desiredId = settings.deviceId; - if (Array.isArray(desiredId)) { - desiredId = desiredId.find(Boolean) || null; - } - if ( - desiredId === null || - desiredId === false || - desiredId === "" || - desiredId === 0 || - desiredId === "0" || - desiredId === 1 || - desiredId === "1" || - desiredId === "default" || - desiredId === "communications" - ) { - return false; - } - desiredId = String(desiredId); - return { audio: { deviceId: desiredId } }; - })(); - if (!session.streamSrc) { - session.framegrabAudioPending = true; - return; - } - let deviceInfos = null; - try { - deviceInfos = await enumerateDevices(); - gotDevices(deviceInfos); - } catch (err) { - errorlog(err); - } - const audioInputsAvailable = Array.isArray(deviceInfos) && deviceInfos.some(info => info && info.kind === 'audioinput'); - let autoSelectOk = false; - try { - autoSelectOk = session.autoSelectFramegrabAudioDevice() === true; - } catch (err) { - errorlog(err); - } - if (!autoSelectOk) { - if (!audioInputsAvailable) { - log('[FRAMEGRAB AUDIO] No audio inputs detected; retrying'); - } - scheduleRetry(); - return; - } - try { - await grabAudio("#audioSource3", null, overrideConstraints); - session.framegrabAudioInitialized = true; - const trackCount = session.streamSrc && typeof session.streamSrc.getAudioTracks === "function" - ? session.streamSrc.getAudioTracks().length - : 0; - if (!trackCount) { - session.framegrabAudioPending = true; - scheduleRetry(); - return; - } - session.framegrabAudioPending = false; - if (trackCount) { - try { - session.seedStream(); - } catch (err) { - errorlog(err); - } - } - clearRetry(); - session.pendingFramegrabAudioSettings = null; - } catch (err) { - errorlog(err); - scheduleRetry(); - } -}; - -session.startFramegrabAudio = async function (overrideSettings = null) { - if (!session.framegrab) { - return false; - } - const pendingSettings = overrideSettings || session.pendingFramegrabAudioSettings || { enable: true }; - if (pendingSettings && pendingSettings.enable === false) { - return session.applyFramegrabAudioSettings(pendingSettings); - } - session.framegrabAudioRequested = true; - session.framegrabAudio = true; - const settings = Object.assign({ enable: true }, pendingSettings || {}); - session.pendingFramegrabAudioSettings = settings; - session.updateFramegrabAudioUI(true); - if (!session.streamSrc) { - session.framegrabAudioPending = true; - return true; - } - if (session.framegrabAudioEnabling) { - return session.streamSrc.getAudioTracks && session.streamSrc.getAudioTracks().length > 0; - } - session.framegrabAudioEnabling = true; - activatedPreview = false; - let success = false; - try { - await session.applyFramegrabAudioSettings(settings); - const hasAudio = session.streamSrc && session.streamSrc.getAudioTracks && session.streamSrc.getAudioTracks().length > 0; - if (hasAudio) { - session.muted = false; - const muteToggle = getById("mutetoggle"); - if (muteToggle) { - muteToggle.className = "las la-microphone toggleSize"; - } - const muteButton = getById("mutebutton"); - if (muteButton) { - muteButton.classList.remove("red", "pulsate"); - muteButton.ariaPressed = "false"; - } - if (!session.cleanOutput) { - try { - getById("header").classList.remove("red"); - } catch (err) { } - } - success = true; - } - } catch (err) { - errorlog(err); - warnUser("Unable to access the microphone. Please check browser permissions.", 6000); - session.framegrabAudio = false; - session.framegrabAudioRequested = false; - session.updateFramegrabAudioUI(false); - } finally { - session.framegrabAudioEnabling = false; - activatedPreview = false; - } - const result = success || session.framegrabAudioPending; - if (!result) { - session.framegrabAudio = false; - session.framegrabAudioRequested = false; - session.updateFramegrabAudioUI(false); - } - return result; -}; - -var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; -var eventer = window[eventMethod]; -var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message"; -eventer(messageEvent, function (e) { - // this listens for child IFRAMES. - try { - if (e.origin == "https://www.youtube.com") { - processYoutubeEvent(e); - } else if (e.data && typeof e.data == "object" && "action" in e.data) { - if (e.data.action == "screen-share-state" && !e.data.value) { - //pokeIframeAPI("screen-share-state", false); - if (session.screenShareElement && session.screenShareElement.contentWindow) { - if (e.source == session.screenShareElement.contentWindow) { - // reject messages send from other iframes - warnlog(e); - postMessageIframe(session.screenShareElement, { close: true }); - session.screenShareElement.parentNode.removeChild(session.screenShareElement); - session.screenShareElement = false; - updateMixer(); - } - } - } else if (e.data.action === "framegrab-audio-settings") { - if (!session.framegrab || typeof session.startFramegrabAudio !== "function" || typeof session.applyFramegrabAudioSettings !== "function") { - return; - } - const enable = !(typeof e.data.enable !== "undefined" && e.data.enable === false); - const payload = { enable }; - if (enable && typeof e.data.deviceId !== "undefined" && e.data.deviceId !== null) { - payload.deviceId = e.data.deviceId; - } - try { - const handler = enable ? session.startFramegrabAudio : session.applyFramegrabAudioSettings; - const maybePromise = handler(payload); - if (maybePromise && typeof maybePromise.then === "function") { - maybePromise.catch(errorlog); - } - } catch (err) { - errorlog(err); - } - } else if (e.data.action == "video-loaded") { - // TODO: if (e.source == session...iframeEle.contentWindow) { - warnlog(e); - toggleSpeakerMute(true); - updateMixer(); // harmless to let run. (not so harmless if updateMixer reloads meshcast actually) TODO; Do I need this? - } - } - } catch (e) { - errorlog(e); - } -}); - -function requestRotateGuest(ele) { - var UUID = ele.dataset.UUID; - var data = {}; - //data.mirrorGuestTarget = UUID; - //session.sendPeers(data, false, UUID); - - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - - data.rotate = true; - session.sendRequest(data, UUID); - - setTimeout( - function (el) { - el.value = 0; - el.classList.remove("pressed"); - el.ariaPressed = "false"; - }, - 500, - ele - ); -} - -function requestMirrorGuest(ele) { - var UUID = ele.dataset.UUID; - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - applyMirrorGuest(false, session.rpcs[UUID].videoElement, session.rpcs[UUID].flipState); - var data = {}; - data.mirrorGuestState = false; - - data.mirrorGuestTarget = UUID; - session.sendPeers(data, false, UUID); - - data.mirrorGuestTarget = true; - session.sendPeers(data, UUID); - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - applyMirrorGuest(true, session.rpcs[UUID].videoElement, session.rpcs[UUID].flipState); - var data = {}; - data.mirrorGuestState = true; - - data.mirrorGuestTarget = UUID; - session.sendPeers(data, false, UUID); - - data.mirrorGuestTarget = true; - session.sendPeers(data, UUID); - } -} - -function requestKeyframeScene(ele) { - var UUID = ele.dataset.UUID; - if (ele.value == 1) { - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - session.requestKeyframe(UUID, true); - setTimeout( - function (el) { - el.value = 0; - el.classList.remove("pressed"); - el.ariaPressed = "false"; - }, - 1000, - ele - ); - } -} - -function pokeIframeAPI(action, value = null, UUID = null, SID = null, CID = null) { - if (!isIFrame) { - return; - } - try { - var data = {}; - - data.action = action; - - if (value !== null) { - data.value = value; - } - if (UUID !== null) { - data.UUID = UUID; - } - if (!SID) { - if (UUID && UUID in session.rpcs) { - if (session.rpcs[UUID].streamID) { - SID = session.rpcs[UUID].streamID; - } - } - } - if (!SID) { - if (UUID && UUID in session.pcs) { - if (session.pcs[UUID].streamID) { - SID = session.pcs[UUID].streamID; - } - } - } - - if (SID) { - data.streamID = SID; - } - - if (CID) { - data.cib = CID; - } - - if (isIFrame) { - parent.postMessage(data, session.iframetarget); - } - } catch (e) { - errorlog(e); - } -} - -async function jumptoroom2() { - var arr = window.location.href.split("?"); - - var roomname = getById("videoname1").value; - roomname = sanitizeRoomName(roomname); - if (roomname.length) { - var pass = getById("passwordRoom").value; - pass = sanitizePassword(pass); - - var passStr = ""; - if (pass && pass.length) { - passStr = "&password=" + pass; - } - - if (arr.length > 1 && arr[1] !== "") { - window.location += "&room=" + roomname + passStr + "&host"; - } else { - window.location += "?room=" + roomname + passStr + "&host"; - } - } else { - getById("videoname1").focus(); - getById("videoname1").classList.remove("shake"); - setTimeout(function () { - getById("videoname1").classList.add("shake"); - }, 10); - } -} - -async function jumptoroom(event = null) { - if (event) { - if (event.which !== 13) { - return; - } - } - - var arr = window.location.href.split("?"); - var roomname = getById("joinroomID").value; - roomname = sanitizeRoomName(roomname); - if (roomname.length) { - var passStr = ""; - window.focus(); - var pass = await promptAlt(getTranslation("enter-password-if-desired"), false, true); //sanitizePassword(session.password); - if (pass && pass.length) { - session.password = sanitizePassword(pass); - passStr = "&password=" + session.password; - } else { - session.password = false; - } - - sessionStorage.setItem("jvi", "1"); // joined via input - for showing invite link header - if (arr.length > 1 && arr[1] !== "") { - window.location += "&room=" + roomname + passStr; - } else { - window.location += "?room=" + roomname + passStr; - } - } else { - getById("joinroomID").focus(); - getById("joinroomID").classList.remove("shake"); - setTimeout(function () { - getById("joinroomID").classList.add("shake"); - }, 10); - } -} - -async function jumptoURL(event = null) { - // this is for the native app - - var url = getById("joinbyURL").value; - if (url.length) { - if (url.startsWith("?")) { - url = "./" + url; - } - - if (url.startsWith("&")) { - url = "./?" + url; - } - - if (!url.startsWith("http") && !url.startsWith(".")) { - url = "./?" + url; - } - - setStorage("jumptoURL", url, 1008); // should be really only used by the native app; 6 months - - window.location = url; - } else { - getById("joinbyURL").focus(); - getById("joinbyURL").classList.remove("shake"); - setTimeout(function () { - getById("joinbyURL").classList.add("shake"); - }, 10); - } -} - -function sleep(ms = 0) { - return new Promise(r => setTimeout(r, ms)); // LOLz! -} - -function sleepCancellable(ms = 0) { - let resolve; - const promise = new Promise(r => { - resolve = r; - setTimeout(resolve, ms); - }); - return { promise, resolve }; -} - -async function changeAvatarImage(ev, ele, set = false) { - log("changeAvatarImage() triggered"); - - if (session.avatar && session.avatar.timer) { - clearInterval(session.avatar.timer); - session.avatar.timer = null; - } - - if (!session.streamSrc) { - checkBasicStreamsExist(); - } - - if (ele.files && ele.files.length) { - session.avatar = document.querySelector("img"); - session.avatar.ready = false; - session.avatar.onload = () => { - URL.revokeObjectURL(session.avatar.src); // no longer needed, free memory - session.avatar.ready = true; - getById("noAvatarSelected3").classList.remove("selected"); - getById("noAvatarSelected").classList.remove("selected"); - getById("defaultAvatar1").classList.add("selected"); - getById("defaultAvatar2").classList.add("selected"); - - var tracks = session.streamSrc.getVideoTracks(); - if (!tracks.length || session.videoMuted) { - updateRenderOutpipe(); - } - }; - - session.avatar.src = URL.createObjectURL(ele.files[0]); // set src to blob url - return; - } else if (ele.tagName.toLowerCase() == "img") { - session.avatar = ele; - session.avatar.ready = true; - getById("noAvatarSelected3").classList.remove("selected"); - getById("noAvatarSelected").classList.remove("selected"); - getById("defaultAvatar1").classList.add("selected"); - getById("defaultAvatar2").classList.add("selected"); - var tracks = session.streamSrc.getVideoTracks(); - if (!tracks.length || session.videoMuted) { - updateRenderOutpipe(); - } - } else { - session.avatar = false; - var tracks = session.streamSrc.getVideoTracks(); - if (!tracks.length || session.videoMuted) { - var msg = {}; - msg.videoMuted = true; - session.sendMessage(msg); - if (document.getElementById("videosource")) { - document.getElementById("videosource").load(); - } else if (document.getElementById("previewWebcam")) { - document.getElementById("previewWebcam").load(); - } - updateRenderOutpipe(); - } - - getById("noAvatarSelected3").classList.add("selected"); - getById("noAvatarSelected").classList.add("selected"); - getById("defaultAvatar1").classList.remove("selected"); - getById("defaultAvatar2").classList.remove("selected"); - return; - } -} - -session.autoSyncCallback = function (UUID = null) { - // session.autoSyncObject has been updated. You can overwrite this with your own function - log(session.autoSyncObject); - pokeIframeAPI("auto-sync-updated", session.autoSyncObject, UUID); -}; - -session.autoSync = function (alternative = null) { - // Update session.autoSyncObject and then run session.autoSync() - var msg = {}; - if (alternative === null) { - msg.autoSync = session.autoSyncObject; - } else { - session.autoSyncObject = alternative; - msg.autoSync = session.autoSyncObject; - } - session.sendPeers(msg); -}; - -//function updateRemotePTZControls(videoOptions, UUID){ -// console.log(videoOptions); -// console.log(UUID); -//} - -function isolateIncomingChannel(channel, UUID) { - if (!session.rpcs[UUID]) return; - - if (channel === 0 || channel === false) { - delete session.rpcs[UUID].isolatedChannel; - } else { - session.rpcs[UUID].isolatedChannel = channel || session.rpcs[UUID].isolatedChannel; - } - updateIncomingAudioElement(UUID); -} - -function isolateChannel(source, channel) { - if (!channel || channel === 0) { - return source; // No isolation, return the original source - } - - const splitter = session.audioCtx.createChannelSplitter(6); // Assuming max 6 channels - const merger = session.audioCtx.createChannelMerger(1); // Mono output - - source.connect(splitter); - splitter.connect(merger, channel - 1, 0); // Connect the specified channel to the mono output - - return merger; -} - -function directIsolateChannel(UUID, channel = null) { // isolateChannel() - try { - if (UUID) { - - var targets = document.querySelectorAll("[data--u-u-i-d='" + UUID + "'][data-action-type='isolate-channel']"); - var add = false; - - if (channel) { - add = true; - } - targets.forEach(ele => { - if (channel && parseInt(ele.dataset.channel) && (parseInt(ele.dataset.channel) == channel)) { - - if (ele.classList.contains("pressed")) { - add = false; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - - } else { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } - } else { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } - }); - - var msg = {}; - - if (add) { - msg.isolateChannel = channel - } else { - msg.isolateChannel = false; - } - return session.sendMessage(msg, UUID); - - } - } catch (e) { - errorlog(e); - } - return false; -} - -function uploadImageSnapshot(PostURL) { - if (!session.videoElement) { - return; - } - const video = session.videoElement; - const canvas = document.createElement("canvas"); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - canvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoHeight); // for drawing the video element on the canvas - const playImage = new Image(); - canvas.getContext("2d").drawImage(playImage, 0, 0, playImage.width, playImage.height); - canvas.toBlob( - function (blob) { - var request = new XMLHttpRequest(); - - if (PostURL.includes("?")) { - request.open("POST", PostURL + "&name=" + session.streamID); - } else { - request.open("POST", PostURL + "?name=" + session.streamID); - } - request.setRequestHeader("Content-Type", "image/jpeg"); - request.send(blob); - log("Posted image"); - }, - "image/jpeg", - 0.9 - ); -} - -var slideshowImage = false; -var slideshowCanvas = false; -var slideshowActive = false; - -function slideshowHack() { - // just shows an animated IMAGE of the first video. audio plays, but no "video" - if (!session.manual) { - session.manual = session.manual === null ? true : session.manual; - } - if (slideshowActive) { - return; - } - slideshowActive = true; - try { - if (!slideshowImage) { - slideshowImage = document.createElement("img"); - slideshowImage.style.width = "100%"; - slideshowImage.style.height = "100%"; - slideshowImage.style.objectFit = "contain"; - slideshowImage.style.border = "0"; - slideshowImage.style.padding = "0"; - slideshowImage.style.margin = "0"; - getById("gridlayout").innerHTML = ""; - getById("gridlayout").appendChild(slideshowImage); - } - var keys = Object.keys(session.rpcs); - if (!keys.length) { - slideshowActive = false; - return; - } - var video = false; - var key = false; - - for (var i = 0; i < keys.length; i++) { - if (session.rpcs[keys[i]].videoElement) { - key = keys[i]; - video = session.rpcs[keys[i]].videoElement; - break; - } - } - if (!video) { - slideshowCanvas = false; - slideshowActive = false; - return; - } - - if (!slideshowCanvas) { - slideshowCanvas = document.createElement("canvas"); - slideshowCanvas.getContext("2d").imageSmoothingEnabled = false; - session.requestResolution(key, session.viewwidth || 480, session.viewheight || 480); - } - slideshowCanvas.width = video.videoWidth; - slideshowCanvas.height = video.videoHeight; - slideshowCanvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoHeight); // for drawing the video element on the canvas - var image = slideshowCanvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); // here is the most important part because if you dont replace you will get a DOM 18 exception. - slideshowImage.src = image; - } catch (e) { - errorlog(e); - } - slideshowActive = false; -} - -function setAvatarImage(tracks) { - if (session.avatar && session.avatar.ready) { - if (session.avatar && session.avatar.timer) { - clearInterval(session.avatar.timer); - session.avatar.timer = null; - } - setupCanvas(); - - var width = 512; - var height = 288; - - var maxW = 1280; - var maxH = 720; - if (session.quality == 0) { - maxW = 1920; - maxH = 1080; - } else if (session.quality == 2) { - maxW = 640; - maxH = 360; - } - - if (session.width) { - maxW = session.width; - } - if (session.height) { - maxH = session.height; - } - - if (session.avatar.naturalHeight && session.avatar.naturalHeight > maxH) { - width = parseInt((maxH / session.avatar.naturalHeight) * session.avatar.naturalWidth); - height = maxH; - - if (width > maxW) { - width = maxW; - height = parseInt((maxW / width) * height); - } - } else if (session.avatar.naturalWidth && session.avatar.naturalWidth > maxW) { - width = maxW; - height = parseInt((maxW / session.avatar.naturalWidth) * session.avatar.naturalHeight); - } else { - width = session.avatar.naturalWidth; - height = session.avatar.naturalHeight; - } - - session.canvasSource.width = width; - session.canvasSource.height = height; - - session.canvas.height = 2 * parseInt(height / 2); - session.canvas.width = 2 * parseInt(width / 2); - - session.canvasCtx.drawImage(session.avatar, 0, 0, session.canvas.width, session.canvas.height); - - session.avatar.timer = setInterval(function () { - log("drawing"); - session.canvasCtx.drawImage(session.avatar, 0, 0, session.canvas.width, session.canvas.height); - }, 200); // too slow and it takes way too long for the video to udpate when a new guest joins - - applyMirror(true); - - session.avatar.tracks = session.canvas.captureStream().getVideoTracks(); - - return session.avatar.tracks; - } - applyMirror(session.mirrorExclude); - return tracks; -} - -var drawOnScreenObject = null; -function drawOnScreen() { - var canvas = document.getElementById("drawOnScreen"); - if (!canvas) { - canvas = document.createElement("canvas"); - document.getElementById("gridlayout").appendChild(canvas); - document.getElementById("gridlayout").style.position = "relative"; - } else { - return; - } - - var ctx = canvas.getContext("2d"); - canvas.width = parseInt(document.getElementById("gridlayout").clientWidth / 2); - canvas.height = parseInt(document.getElementById("gridlayout").clientHeight / 2); - canvas.style.width = "100%"; - canvas.style.height = "100%"; - canvas.style.display = "block"; - canvas.style.position = "absolute"; - canvas.style.bottom = "0"; - canvas.style.left = "0"; - - var flag = false, - prevX = 0, - currX = 0, - prevY = 0, - currY = 0, - dot_flag = false; - - var x = "black", - y = 2; - - var object = {}; - - function findxy(res, e) { - if (res == "down") { - prevX = currX; - prevY = currY; - currX = e.clientX - canvas.offsetLeft; - currY = e.clientY - canvas.offsetTop; - - flag = true; - dot_flag = true; - if (dot_flag) { - ctx.beginPath(); - ctx.fillStyle = x; - ctx.fillRect(currX, currY, 2, 2); - ctx.closePath(); - dot_flag = false; - } - } - if (res == "up" || res == "out") { - flag = false; - } - if (res == "move") { - if (flag) { - prevX = currX; - prevY = currY; - currX = e.clientX - canvas.offsetLeft; - currY = e.clientY - canvas.offsetTop; - draw(); - } - } - } - - function draw() { - ctx.beginPath(); - - var mx = canvas.width / parseInt(document.getElementById("gridlayout").clientWidth); - var my = canvas.height / parseInt(document.getElementById("gridlayout").clientHeight); - var mo = parseInt(document.getElementById("header").clientHeight); - - ctx.moveTo(prevX * mx, prevY * my - mo * my); - ctx.lineTo(currX * mx, currY * my - mo * my); - ctx.strokeStyle = x; - ctx.lineWidth = y; - ctx.stroke(); - ctx.closePath(); - } - - function onMouseMove(e) { - findxy("move", e); - } - - function onMouseDown(e) { - findxy("down", e); - } - - function onMouseUp(e) { - findxy("up", e); - } - - function onMouseOut(e) { - findxy("out", e); - } - - object.stop = function stop() { - canvas.removeEventListener("mousemove", onMouseMove, false); - canvas.removeEventListener("mousedown", onMouseDown, false); - canvas.removeEventListener("mouseup", onMouseUp, false); - canvas.removeEventListener("mouseout", onMouseOut, false); - canvas.remove(); - - document.getElementById("startDrawScreen").classList.remove("hidden"); - - document.querySelectorAll(".drawActive").forEach(ele => { - ele.classList.add("hidden"); - }); - - drawOnScreenObject = null; - }; - - object.init = function init() { - canvas.addEventListener("mousemove", onMouseMove, false); - canvas.addEventListener("mousedown", onMouseDown, false); - canvas.addEventListener("mouseup", onMouseUp, false); - canvas.addEventListener("mouseout", onMouseOut, false); - - document.getElementById("startDrawScreen").classList.add("hidden"); - - document.querySelectorAll(".drawActive").forEach(ele => { - ele.classList.remove("hidden"); - }); - }; - - object.color = function color(obj) { - switch (obj.dataset.color) { - case "green": - x = "green"; - break; - case "blue": - x = "blue"; - break; - case "red": - x = "red"; - break; - case "yellow": - x = "yellow"; - break; - case "orange": - x = "orange"; - break; - case "black": - x = "black"; - break; - case "white": - x = "white"; - break; - } - if (x == "white") y = 14; - else y = 2; - }; - - object.erase = function erase() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - }; - - object.save = function save() { - var dataURL = canvas.toDataURL(); - }; - - object.init(); - drawOnScreenObject = object; - return object; -} - -// SENDER DRAWERS LOGIC PORTION START -function fitCurve(points) { - if (points.length <= 1) return points; - if (points.length === 2) return [{ t: 'l', p: [points[0], points[1]] }]; - if (points.length === 3) return [{ t: 'q', p: points }]; - - let result = []; - for (let i = 0; i < points.length - 1; i += 3) { - let p0 = points[i]; - let p1 = points[i + 1] || p0; - let p2 = points[i + 2] || p1; - let p3 = points[i + 3] || p2; - - result.push({ - t: 'b', - p: [p0, p1, p2, p3] - }); - } - - return result; -} - -function isSharpTurn(points) { - if (points.length < 3) return false; - let angle1 = Math.atan2(points[1].y - points[0].y, points[1].x - points[0].x); - let angle2 = Math.atan2(points[2].y - points[1].y, points[2].x - points[1].x); - let angleDiff = Math.abs(angle2 - angle1); - return angleDiff > Math.PI / 4; -} - -function drawOnThis(video) { - try { - if (!video || !video.container) { - warnlog("no video holder; not compatible"); - return; - } - var container = video.container || video.parentNode; - var holder = container.holder || null; - - var canvas = document.createElement('canvas'); - if (!holder) { - holder = document.createElement("div"); - container.holder = holder; - holder.className = "holder"; - holder.dataset.holder = true; - - container.style = "display: flex;\ - align-items: center;\ - justify-content: center;"; - - container.appendChild(holder); - holder.appendChild(video); - video.style.setProperty('top', '0', 'important'); - video.style.setProperty('left', '0', 'important'); - - session.windowed = false; - applyMirror(); - - canvas.style.position = "fixed"; - holder.style = "position: relative;\ - width: 800px;\ - height: 450px; \ - display: flex;\ - align-items: center;\ - justify-content: center;" - } else { - canvas.className = "drawingCanvas"; - } - - canvas.style.pointerEvents = "none"; - holder.appendChild(canvas); - video.canvas = canvas; - - const ctx = canvas.getContext('2d'); - ctx.lineWidth = 3; - - const buttonContainer = document.createElement('div'); - const enableDrawingBtn = document.createElement('button'); - const clearDrawingBtn = document.createElement('button'); - const undoDrawingBtn = document.createElement('button'); // Undo button - - enableDrawingBtn.textContent = "Enable Drawing"; - clearDrawingBtn.textContent = "Clear"; - undoDrawingBtn.textContent = "Undo"; // Undo button text - buttonContainer.className = "buttonContainer"; - - buttonContainer.appendChild(enableDrawingBtn); - buttonContainer.appendChild(clearDrawingBtn); - buttonContainer.appendChild(undoDrawingBtn); // Add undo button to container - holder.appendChild(buttonContainer); - - let isDrawing = false; - let drawingEnabled = false; - let drawingData = []; - let lastPoint = null; - let lastSentTime = 0; - const sendInterval = 1000; // 1 second - let lastPoints = []; - - function startDrawing(e) { - if (!drawingEnabled) return; - isDrawing = true; - draw(e); - } - - function draw(e) { - if (!isDrawing || !drawingEnabled) return; - - const rect = canvas.getBoundingClientRect(); - let x = (e.clientX - rect.left) / rect.width; - let y = (e.clientY - rect.top) / rect.height; - - // Check if the mouse is within bounds - if (x < 0 || x > 1 || y < 0 || y > 1) { - stopDrawing(); - return; - } - - ctx.lineCap = 'round'; - ctx.strokeStyle = 'red'; - - const canvasX = x * canvas.width; - const canvasY = y * canvas.height; - - if (lastPoint) { - ctx.beginPath(); - ctx.moveTo(lastPoint.x * canvas.width, lastPoint.y * canvas.height); - ctx.lineTo(canvasX, canvasY); - ctx.stroke(); - } - - x = Math.round(x * 4000) / 4000; - y = Math.round(y * 4000) / 4000; - - lastPoint = { x, y }; - lastPoints.push({ x, y }); - - // Send data more frequently - if (lastPoints.length >= 5 || Date.now() - lastSentTime >= 50) { - sendDrawingData(); - } - } - - function redrawCanvas() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.beginPath(); - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - ctx.strokeStyle = "red"; - - let isNewPath = true; - - for (let i = 0; i < drawingData.length; i++) { - let segment = drawingData[i]; - - if (!segment) { - // End of a path - ctx.stroke(); - ctx.beginPath(); - isNewPath = true; - continue; - } - - if (segment.t) { - // This is a complex segment (line or bezier) - switch (segment.t) { - case 'b': - let [p0, p1, p2, p3] = segment.p; - if (isNewPath) { - ctx.moveTo(p0.x * canvas.width, p0.y * canvas.height); - isNewPath = false; - } - ctx.bezierCurveTo( - p1.x * canvas.width, p1.y * canvas.height, - p2.x * canvas.width, p2.y * canvas.height, - p3.x * canvas.width, p3.y * canvas.height - ); - break; - case 'q': - let [q0, q1, q2] = segment.p; - if (isNewPath) { - ctx.moveTo(q0.x * canvas.width, q0.y * canvas.height); - isNewPath = false; - } - ctx.quadraticCurveTo( - q1.x * canvas.width, q1.y * canvas.height, - q2.x * canvas.width, q2.y * canvas.height - ); - break; - case 'l': - let [l0, l1] = segment.p; - if (isNewPath) { - ctx.moveTo(l0.x * canvas.width, l0.y * canvas.height); - isNewPath = false; - } - ctx.lineTo(l1.x * canvas.width, l1.y * canvas.height); - break; - default: - warnlog(segment); - } - } else if (segment.x !== undefined && segment.y !== undefined) { - // This is a simple point - const canvasX = segment.x * canvas.width; - const canvasY = segment.y * canvas.height; - if (isNewPath) { - ctx.moveTo(canvasX, canvasY); - isNewPath = false; - } else { - ctx.lineTo(canvasX, canvasY); - } - } - } - - ctx.stroke(); - } - - function processPoints(points) { - let processedPoints = []; - let currentSegment = []; - for (let i = 0; i < points.length; i++) { - if (points[i] === null) { - if (currentSegment.length > 0) { - if (currentSegment.length === 2) { - processedPoints.push({ t: 'l', p: currentSegment }); - } else { - processedPoints.push(...fitCurve(currentSegment)); - } - currentSegment = []; - } - processedPoints.push(null); - } else if (i > 0 && isSignificantBreak(points[i - 1], points[i])) { - if (currentSegment.length > 0) { - if (currentSegment.length === 2) { - processedPoints.push({ t: 'l', p: currentSegment }); - } else { - processedPoints.push(...fitCurve(currentSegment)); - } - currentSegment = []; - } - currentSegment.push(points[i]); - } else { - currentSegment.push(points[i]); - if (currentSegment.length >= 4) { - processedPoints.push(...fitCurve(currentSegment)); - currentSegment = [currentSegment[currentSegment.length - 1]]; - } - } - } - if (currentSegment.length > 0) { - if (currentSegment.length === 2) { - processedPoints.push({ t: 'l', p: currentSegment }); - } else { - processedPoints.push(...fitCurve(currentSegment)); - } - } - return processedPoints; - } - - function isSignificantBreak(point1, point2) { - const distance = Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2)); - return distance > 0.05; // Increased threshold - } - - function processCurrentSegment(segment) { - if (segment.length < 3) return segment; - - if (isSharpTurn(segment)) { - return segment; - } else { - return fitCurve(segment); - } - } - - function drawBezierCurve(points) { - const [start, control1, control2, end] = points; - ctx.moveTo(start.x * canvas.width, start.y * canvas.height); - ctx.bezierCurveTo( - control1.x * canvas.width, control1.y * canvas.height, - control2.x * canvas.width, control2.y * canvas.height, - end.x * canvas.width, end.y * canvas.height - ); - } - - function sendDrawingData(alt = false) { - if (alt === "clear") { - drawingData = []; - if (video.id === "videosource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing) { - session.sendMessage({ draw: "clear" }, UUID); - } - } - } else if (video.id === "screensharesource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { - session.sendMessage({ draw: "clear", altUUID: true }, UUID); - } - } - } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { - session.sendRequest({ draw: "clear" }, video.dataset.UUID); - } - return; - } - if (alt === "cleanup") { - drawingData = []; - if (video.id === "videosource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing) { - session.sendMessage({ draw: "cleanup" }, UUID); - } - } - } else if (video.id === "screensharesource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { - session.sendMessage({ draw: "cleanup", altUUID: true }, UUID); - } - } - } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { - session.sendRequest({ draw: "cleanup" }, video.dataset.UUID); - } - return; - } - if (alt === "undo") { - if (drawingData.length > 0) { - // Find the last segment to remove - let found = false; - for (let i = drawingData.length - 1; i >= 0; i--) { - if (drawingData[i] === null) { - drawingData = drawingData.slice(0, i); - if (found) { - drawingData.push(null); - break; - } - } else { - found = true; - } - if (i === 0) { // Handle case when there's no null in drawingData - drawingData = []; - } - } - redrawCanvas(); - - // Send the undo command - if (video.id === "videosource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing) { - session.sendMessage({ draw: "undo" }, UUID); - } - } - } else if (video.id === "screensharesource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { - session.sendMessage({ draw: "undo", altUUID: true }, UUID); - } - } - } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { - session.sendRequest({ draw: "undo" }, video.dataset.UUID); - } - - } - return; - } - if (alt === "sync") { - if (!drawingData.length) { return; } - // Send the processed points - if (video.id === "videosource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing) { - if (!session.pcs[UUID].initialDrawing) { - session.pcs[UUID].initialDrawing = true; - session.sendMessage({ draw: { p: drawingData } }, UUID); - } - } - } - } else if (video.id === "screensharesource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { - if (!session.pcs[UUID].initialDrawing2) { - session.pcs[UUID].initialDrawing2 = true; - session.sendMessage({ draw: { p: drawingData }, altUUID: true }, UUID); - } - } - } - } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { - if (!session.rpcs[video.dataset.UUID].initialDrawing) { - session.rpcs[video.dataset.UUID].initialDrawing = true; - session.sendRequest({ draw: { p: drawingData } }, video.dataset.UUID); - } - } - return; - } - if (lastPoints.length > 0) { - var processedPoints = processPoints(lastPoints); - lastPoints = []; - lastSentTime = Date.now(); - - var dataToSend = { - p: processedPoints - }; - - drawingData.push(...processedPoints); // Store only points in drawingData - - // Send the processed points with timestamp - if (video.id === "videosource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing) { - if (session.pcs[UUID].initialDrawing) { - session.sendMessage({ draw: dataToSend }, UUID); - } else { - session.pcs[UUID].initialDrawing = true; - session.sendMessage({ draw: { p: drawingData } }, UUID); - } - } - } - } else if (video.id === "screensharesource") { - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { - if (session.pcs[UUID].initialDrawing2) { - session.sendMessage({ draw: dataToSend, altUUID: true }, UUID); - } else { - session.pcs[UUID].initialDrawing2 = true; - session.sendMessage({ draw: { p: drawingData }, altUUID: true }, UUID); - } - } - } - } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { - if (session.rpcs[video.dataset.UUID].initialDrawing) { - session.sendRequest({ draw: dataToSend }, video.dataset.UUID); - } else { - session.rpcs[video.dataset.UUID].initialDrawing = true; - session.sendRequest({ draw: { p: drawingData } }, video.dataset.UUID); - } - } - } - } - - function resizeCanvas() { - canvas.width = video.offsetWidth; - canvas.height = video.offsetHeight; - redrawCanvas(); - } - - resizeCanvas(); - window.addEventListener('resize', resizeCanvas); - video.addEventListener('resize', resizeCanvas); - - enableDrawingBtn.addEventListener('click', () => { - drawingEnabled = !drawingEnabled; - enableDrawingBtn.textContent = drawingEnabled ? 'Disable Drawing' : 'Enable Drawing'; - canvas.style.pointerEvents = drawingEnabled ? "auto" : "none"; - }); - - clearDrawingBtn.addEventListener('click', () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - drawingData = []; - lastPoints = []; - sendDrawingData("clear"); - }); - - undoDrawingBtn.addEventListener('click', () => { - sendDrawingData("undo"); - }); - - function stopDrawing() { - if (isDrawing) { - - isDrawing = false; - ctx.beginPath(); - lastPoint = null; - lastPoints.push(null); // Add null to mark end of path - sendDrawingData(); - } - } - - canvas.addEventListener('mousedown', startDrawing); - canvas.addEventListener('mousemove', draw); - canvas.addEventListener('mouseup', stopDrawing); - canvas.addEventListener('mouseout', stopDrawing); - canvas.addEventListener('mouseleave', stopDrawing); - canvas.addEventListener('mouseenter', (e) => { - if (e.buttons !== 1) { // If left mouse button is not pressed - stopDrawing(); - } - }); - - function createCleanupFunction() { - return function cleanup() { - sendDrawingData("cleanup"); - - window.removeEventListener('resize', resizeCanvas); - video.removeEventListener('resize', resizeCanvas); - - if (canvas) { - canvas.removeEventListener('mousedown', startDrawing); - canvas.removeEventListener('mousemove', draw); - canvas.removeEventListener('mouseup', stopDrawing); - canvas.removeEventListener('mouseout', stopDrawing); - canvas.removeEventListener('mouseleave', stopDrawing); - canvas.removeEventListener('mouseenter', stopDrawing); - - if (canvas.parentNode) { - canvas.parentNode.removeChild(canvas); - } - } - - if (buttonContainer && buttonContainer.parentNode) { - buttonContainer.parentNode.removeChild(buttonContainer); - } - }; - } - - function syncNewConnections() { - return function syncDrawing() { - setTimeout(() => { - sendDrawingData("sync") - }, 4000); - } - } - - video.syncDrawOnVideo = syncNewConnections(); - video.clearDrawOnVideo = createCleanupFunction(); - return video.clearDrawOnVideo; - - } catch (e) { - errorlog(e); - } -} - -// END SENDING LOGIC -// var cleanUp = drawOnThis(document.getElementById('videoElement')); -// cleanUp(); -// START RECEIVING LOGIC - -function receiveDrawingOnVideo(video, UUID = false) { - try { - if (!video || !video.container) { - warnlog("no video holder; not compatible"); - return; - } - const canvas = document.createElement('canvas'); - canvas.className = "drawingCanvas"; - canvas.style.pointerEvents = "none"; - var receivedDrawingData = []; - - var container = video.parentNode; - if (!container) { - return; - } - container.appendChild(canvas); - - var color = 'red'; - if (UUID) { - color = getColorFromName(UUID); - } - - function positionCanvas() { - const videoRect = video.getBoundingClientRect(); - const computedStyle = getComputedStyle(video); - - canvas.style.width = computedStyle.width; - canvas.style.height = computedStyle.height; - canvas.style.top = `${videoRect.top + window.scrollY}px`; - canvas.style.left = `${videoRect.left + window.scrollX}px`; - canvas.width = video.clientWidth; - canvas.height = video.clientHeight; - - if (video.dataset.transform) { - canvas.style.transform = video.dataset.transform; - } - redrawCanvas(); - } - - const ctx = canvas.getContext('2d'); - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; - ctx.lineWidth = 3; - - function resizeCanvas() { - canvas.width = video.offsetWidth; - canvas.height = video.offsetHeight; - if (video.dataset.transform) { - canvas.style.transform = video.dataset.transform; - } - redrawCanvas(); - } - - let observer = null; - if (!(video && video.container && video.container.holder)) { - positionCanvas(); - window.addEventListener('resize', positionCanvas); - video.addEventListener('resize', positionCanvas); - observer = new ResizeObserver(positionCanvas); - observer.observe(video); - } else { - resizeCanvas(); - window.addEventListener('resize', resizeCanvas); - video.addEventListener('resize', resizeCanvas); - } - - function drawBezierCurve(points) { - const [start, control1, control2, end] = points; - ctx.moveTo(start.x * canvas.width, start.y * canvas.height); - ctx.bezierCurveTo( - control1.x * canvas.width, control1.y * canvas.height, - control2.x * canvas.width, control2.y * canvas.height, - end.x * canvas.width, end.y * canvas.height - ); - } - - function redrawCanvas() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - ctx.strokeStyle = color; - - let isNewPath = true; - - for (let i = 0; i < receivedDrawingData.length; i++) { - let segment = receivedDrawingData[i]; - - if (!segment) { - // End of a path - ctx.stroke(); - ctx.beginPath(); - isNewPath = true; - continue; - } - - switch (segment.t) { - case 'b': - let [p0, p1, p2, p3] = segment.p; - if (isNewPath) { - ctx.moveTo(p0.x * canvas.width, p0.y * canvas.height); - isNewPath = false; - } - ctx.bezierCurveTo( - p1.x * canvas.width, p1.y * canvas.height, - p2.x * canvas.width, p2.y * canvas.height, - p3.x * canvas.width, p3.y * canvas.height - ); - break; - case 'q': - let [q0, q1, q2] = segment.p; - if (isNewPath) { - ctx.moveTo(q0.x * canvas.width, q0.y * canvas.height); - isNewPath = false; - } - ctx.quadraticCurveTo( - q1.x * canvas.width, q1.y * canvas.height, - q2.x * canvas.width, q2.y * canvas.height - ); - break; - case 'l': - let [l0, l1] = segment.p; - if (isNewPath) { - ctx.moveTo(l0.x * canvas.width, l0.y * canvas.height); - isNewPath = false; - } - ctx.lineTo(l1.x * canvas.width, l1.y * canvas.height); - break; - default: - // Fallback for non-curve points - if (isNewPath) { - ctx.moveTo(segment.x * canvas.width, segment.y * canvas.height); - isNewPath = false; - } else { - ctx.lineTo(segment.x * canvas.width, segment.y * canvas.height); - } - } - } - - ctx.stroke(); - } - - function updateDrawing(newData) { - if (newData === "clear") { - receivedDrawingData = []; - ctx.clearRect(0, 0, canvas.width, canvas.height); - } else if (newData === "undo") { - if (receivedDrawingData.length > 0) { - // Find the last segment to remove - let found = false; - for (let i = receivedDrawingData.length - 1; i >= 0; i--) { - if (receivedDrawingData[i] === null) { - receivedDrawingData = receivedDrawingData.slice(0, i); - if (found) { - receivedDrawingData.push(null); - break; - } - } else { - found = true; - } - if (i === 0) { // Handle case when there's no null in receivedDrawingData - receivedDrawingData = []; - } - } - redrawCanvas(); - } - } else { - // Handle both new and old data formats - if (newData.p) { - // New format - receivedDrawingData.push(...newData.p); - } else if (Array.isArray(newData)) { - // Old format or array of points - receivedDrawingData.push(...newData); - } else if (typeof newData === 'object' && newData.x !== undefined && newData.y !== undefined) { - // Single point - receivedDrawingData.push(newData); - } else { - console.error("Unexpected data format:", newData); - return; - } - - redrawCanvas(); - } - } - - function clearDrawing() { - receivedDrawingData = []; - ctx.clearRect(0, 0, canvas.width, canvas.height); - } - - function cleanup() { - - try { - if (observer) { - window.removeEventListener('resize', positionCanvas); - video.removeEventListener('resize', positionCanvas); - observer.disconnect(); - } else { - window.removeEventListener('resize', resizeCanvas); - video.removeEventListener('resize', resizeCanvas); - } - - clearDrawing(); - } catch (e) { - errorlog(e); - } - - container.removeChild(canvas); - } - - return { - updateDrawing, - clearDrawing, - cleanup - }; - } catch (e) { - errorlog(e); - } -} - -// END REMOTE DRAWING LOGIC - -////////// Canvas Effects /////////////// - -var drawFrameMirroredActive = false; -function drawFrameMirrored(mirror = true, flip = false) { - if (drawFrameMirroredActive) return; - drawFrameMirroredActive = true; - - if (session.effect == "2") { - mirror = true; - flip = false; - } else if (session.effect == "-2") { - mirror = true; - flip = true; - } else if (session.effect == "-1") { - mirror = false; - flip = true; - } - - try { - session.canvasCtx.save(); - if (flip) { - session.canvasCtx.scale(mirror ? -1 : 1, -1); - session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * (mirror ? -1 : 1), session.canvas.height * -1); - } else { - session.canvasCtx.scale(mirror ? -1 : 1, 1); - session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * (mirror ? -1 : 1), session.canvas.height); - } - session.canvasCtx.restore(); - } catch (e) { - errorlog(e); - } - - drawFrameMirroredActive = false; -} - -function motionDetection(video, threshold = 15, sensitivity = 75) { - var targetSize = 16; - - if (!video.motionDetector) { - video.motionDetector = {}; - video.motionDetector.canvas = document.createElement("canvas"); - video.motionDetector.canvas.width = targetSize; - video.motionDetector.canvas.height = targetSize; - try { - video.motionDetector.ctx = video.motionDetector.canvas.getContext("2d", { willReadFrequently: true }); - } catch (e) { - video.motionDetector.ctx = video.motionDetector.canvas.getContext("2d"); - } - video.motionDetector.previous = []; - for (var y = 0; y < targetSize; y++) { - for (var x = 0; x < targetSize; x++) { - video.motionDetector.previous.push(0); - } - } - } - var motionDetector = video.motionDetector; - - motionDetector.ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, targetSize, targetSize); - var data = motionDetector.ctx.getImageData(0, 0, targetSize, targetSize).data; - var matches = 0; - for (var y = 0; y < targetSize; y++) { - for (var x = 0; x < targetSize; x++) { - var pos = y * targetSize + x; - var pos2 = pos * 3; - var value = data[pos2] + data[pos2 + 1] + data[pos2 + 2]; // convert to to greyscale - if (motionDetector.previous[pos] && Math.abs(motionDetector.previous[pos] - value) > sensitivity) { - matches += 1; - } - motionDetector.previous[pos] = value; - } - } - if (matches >= threshold) { - log("MOTION DETECTED: " + matches); - if (session.motionSwitch && window.obsstudio && window.obsstudio["setCurrentScene"]) { - if (!changeSceneEnabled) { - // the bit cut scene change is already active. - if (session.obsState && session.obsState.details && session.obsState.details.thisScene && session.obsState.details.currentScene) { - if (session.obsState.details.thisScene !== session.obsState.details.currentScene.name) { - // don't trigger it multiple times; makes it hard to prep next scene - window.obsstudio["setCurrentScene"](session.obsState.details.thisScene); - } - } - } - } - pokeIframeAPI("motion-detected", true, video.dataset.UUID || true); - - if (session.infocus !== (video.dataset.UUID || true)) { - if (!session.layout) { - session.infocus = video.dataset.UUID || true; - updateMixer(); - } - } - - if (session.motionRecord) { - if (!session.motionRecordTimeout) { - session.motionRecordTimeout = setTimeout(function () { - session.motionRecordTimeout = null; - }, 1000); - saveVideoFrameToDisk(video); - } - } - } -} - -let currentOscillatorId = 0; -function clearOscillator() { - const thisOscillatorId = ++currentOscillatorId; - if (session.canvasOscillator) { - session.canvasOscillator.stop(); - session.canvasOscillator.disconnect(); - session.canvasOscillator = null; - } - - if (session.canvasSilence) { - session.canvasSilence.disconnect(); - session.canvasSilence = null; - } - if (session.stats) { - delete session.stats.canvas_draw_rate; - } -} -function setupOscillator(callbackFunction, frameRate = 30, timeOne = null, thisOscillatorId = null) { - if (!thisOscillatorId) { - thisOscillatorId = ++currentOscillatorId; - } else if (currentOscillatorId !== thisOscillatorId) { - return false; - } - - if (session.canvasOscillator) { - session.canvasOscillator.stop(); - session.canvasOscillator.disconnect(); - session.canvasOscillator = null; - } - - if (session.canvasSilence) { - session.canvasSilence.disconnect(); - session.canvasSilence = null; - } - - if (!session.audioCtx) { - session.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - } - - let oscillator = session.audioCtx.createOscillator(); - session.canvasOscillator = oscillator; - let silence = session.audioCtx.createGain(); - session.canvasSilence = silence; - - silence.gain.value = 0; - oscillator.connect(silence); - silence.connect(session.audioCtx.destination); - - if (!timeOne) { - timeOne = session.audioCtx.currentTime; - } - - oscillator.onended = () => { - oscillator.disconnect(); - silence.disconnect(); - if (currentOscillatorId === thisOscillatorId) { - let timeTwo = session.audioCtx.currentTime; - - if (typeof callbackFunction === "function") { - callbackFunction(); - } - - if (session.stats) { - let actualRate = 1 / (timeTwo - timeOne); // Calculate the actual FPS - session.stats.canvas_draw_rate = parseInt(actualRate * 10) / 10; - } - setupOscillator(callbackFunction, frameRate, timeTwo, thisOscillatorId); - } - }; - - oscillator.start(timeOne); - oscillator.stop(timeOne + 1 / frameRate); - - return function (check = false) { - if (check && currentOscillatorId !== thisOscillatorId) { - clearOscillator(); - return true; - } else if (check) { - return false; - } - if (currentOscillatorId === thisOscillatorId) { - clearOscillator(); // clear only if needs to be cleared - } - return false; - }; -} - -function setupCanvas() { - clearOscillator(); - - log("SETUP CANVAS"); - - if (session.canvas === null) { - session.canvas = document.createElement("canvas"); - session.canvas.width = 512; - session.canvas.height = 288; - try { - session.canvasCtx = session.canvas.getContext("2d", { alpha: true, willReadFrequently: true }); - } catch (e) { - errorlog(e); - session.canvasCtx = session.canvas.getContext("2d"); - } - - session.canvasCtx.fillStyle = "black"; - session.canvasCtx.fillRect(0, 0, 512, 288); - session.canvasSource = createVideoElement(); - session.canvasSource.autoplay = true; - session.canvasSource.srcObject = createMediaStream(); - session.canvasSource.id = "effectsVideoSource"; - - if (session.canvasSource.srcObject.getVideoTracks().length) { - session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; - session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; - } - - if (iOS || iPad) { - session.canvasSource.style.position = "absolute"; - session.canvasSource.style.left = "0"; - session.canvasSource.style.top = "0"; - session.canvasSource.controls = session.showControls || false; - session.canvasSource.style.maxWidth = "1px"; - session.canvasSource.style.maxHeight = "1px"; - session.canvasSource.setAttribute("playsinline", ""); - document.body.appendChild(session.canvasSource); - //session.canvasSource.play(); - } - } else { - session.canvasSource.srcObject.getVideoTracks().forEach(function (trk) { - session.canvasSource.srcObject.removeTrack(trk); - }); - } -} - -function applyEffects(track) { - // video only please. do not touch audio. Run update Render Outpipe () instead of this directly. - log("applyEffects()"); - - if (session.effect == "0" || !session.effect) { - // auto align face - return track; - } else if (session.effect == "1") { - // auto align face - setupCanvas(); - session.canvasSource.srcObject.addTrack(track); - - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - drawFace(); - } else if (session.effect == "7") { - // manual zoom - setupCanvas(); - session.canvasSource.srcObject.addTrack(track); - - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - digitalZoom(true); - } else if (["8", "overlay"].includes(session.effect)) { - // manual zoom - setupCanvas(); - session.canvasSource.srcObject.addTrack(track); - - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - simpleDraw(); - } else if (["-2", "-1", "2"].includes(session.effect)) { - // mirror video at a canvas level - setupCanvas(); - session.canvasSource.srcObject.addTrack(track); - - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - setupOscillator(drawFrameMirrored, track.getSettings().frameRate || 30); - } else if (session.effect == "3" || session.effect == "4" || session.effect == "5") { - // blur & greenscreen (low and high) - setupCanvas(); - session.canvasSource.srcObject.addTrack(track); - - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - TFLiteWorker(); - } else if (session.effect == "6") { - setupCanvas(); - session.canvasSource.srcObject.addTrack(track); - - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - if (session.canvasSource.readyState >= 3) { - mainMeshMask(); - } else { - session.canvasSource.onloadeddata = mainMeshMask; - } - } else if (session.effect == "13") { - // ha, no way to turn it off once it's started, except to change cameras? not sure. - try { - track - .applyConstraints({ backgroundBlur: true }) - .then(() => { - const settings = track.getSettings(); - log(`Background blur is ${settings.backgroundBlur ? "ON" : "OFF"}`); - }) - .catch(errorlog); - } catch (e) { - errorlog(e); - } - return track; - } else if (session.effect == "14" || session.effect == "15") { - // chroma key effects - setupCanvas(); - session.canvasSource.srcObject.addTrack(track); - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - chromaKey(); - } else { - if (session.canvasource) { - session.canvasSource.srcObject.getVideoTracks().forEach(function (trk) { - session.canvasSource.srcObject.removeTrack(trk); - }); - } else { - session.canvasSource = createVideoElement(); - session.canvasSource.srcObject = createMediaStream(); - } - - session.canvasSource.autoplay = true; - session.canvasSource.id = "effectsVideoSource"; - session.canvasSource.srcObject.addTrack(track); - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - session.canvas.width = 512; - session.canvas.height = 288; - - if (iOS || iPad) { - session.canvasSource.style.position = "absolute"; - session.canvasSource.style.left = "0"; - session.canvasSource.style.top = "0"; - session.canvasSource.style.maxWidth = "1px"; - session.canvasSource.style.maxHeight = "1px"; - session.canvasSource.controls = session.showControls || false; - - session.canvasSource.setAttribute("playsinline", ""); - document.body.appendChild(session.canvasSource); - //session.canvasSource.play(); - } - - try { - JEELIZFACEFILTER.destroy(); - } catch (e) { } - if (session.canvasWebGL) { - session.canvasWebGL.remove(); - session.canvasWebGL = null; - } - session.canvasWebGL = document.createElement("canvas"); - session.canvasWebGL.width = track.getSettings().width || 1280; - session.canvasWebGL.height = track.getSettings().height || 720; - session.canvasWebGL.id = "effectsCanvasTarget"; - session.canvasWebGL.style.position = "fixed"; - session.canvasWebGL.style.top = "-9999px"; - session.canvasWebGL.style.left = "-9999px"; - - document.body.appendChild(session.canvasWebGL); - loadEffect(session.effect); - - return session.canvasWebGL.captureStream().getVideoTracks()[0]; - } - try { - return session.canvas.captureStream().getVideoTracks()[0]; - - } catch (e) { - if (!session.cleanOutput) { - warnUser(getTranslation("not-clean-session"), false, false); - } - return track; - } -} - -function dataURItoArraybuffer(dataURI) { - var byteString = atob(dataURI.split(",")[1]); - var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0]; - var ab = new ArrayBuffer(byteString.length); - var ia = new Uint8Array(ab); - for (var i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); - } - return ab; -} - -var makeImagesActive = null; -async function makeImages(startup = false) { - if (!session.webp) { - return; - } else if (makeImagesActive === true) { - return; - } else if (!session.videoElement) { - return; - } else if (session.videoMuted) { - return; - } - - var stream = session.getLocalStream(); - - if (!stream || !stream.getVideoTracks().length) { - errorlog("No video element; can't make images for webp mode"); - if (makeImagesActive) { - var exit = true; - for (var i in session.pcs) { - if (session.pcs[i].allowWebp) { - // just for safety, to avoid a race condition, double check that it's still not active. - exit = false; - } - } - if (exit) { - makeImagesActive = false; - return; - } - - if (session.webPcanvas.makeImagesTimeout) { - session.webPcanvas.makeImagesTimeout.onended = null; - session.webPcanvas.makeImagesTimeout = null; - } - - var osc = session.webPcanvas.aCtx.createOscillator(); - osc.connect(session.webPcanvas.silence); - session.webPcanvas.makeImagesTimeout = osc; - osc.start(0); - osc.onended = function () { - this.disconnect(); - makeImages(); - }; - osc.stop(session.webPcanvas.aCtx.currentTime + 0.5); - } - return; - } - - if (makeImagesActive === null) { - makeImagesActive = true; - session.webPcanvas = document.createElement("canvas"); - session.webPcanvas.makeImagesTimeout = null; - - session.webPcanvas.aCtx = new AudioContext(); - session.webPcanvas.silence = session.webPcanvas.aCtx.createGain(); - session.webPcanvas.silence.gain.value = 0; - session.webPcanvas.silence.connect(session.webPcanvas.aCtx.destination); - - session.webPcanvas.nowTime = new Date().getTime(); - - session.webPcanvasCtx = session.webPcanvas.getContext("2d", { alpha: session.alpha }); - } else { - if (session.webPcanvas.makeImagesTimeout) { - session.webPcanvas.makeImagesTimeout.onended = null; - } - makeImagesActive = true; - } - - if (startup) { - var exit = true; - for (var i in session.pcs) { - if (session.pcs[i].allowWebp) { - // just for safety, to avoid a race condition, double check that it's still not active. - exit = false; - } - } - if (exit) { - makeImagesActive = false; - return; - } - log("MAKE IMAGES STARTING?"); - } - - var track = stream.getVideoTracks()[0]; - var settings = track.getSettings(); - - try { - var broadcasting = false; - var arrayBuffer = false; - - var width = 480; - var height = 270; - var timeout = settings.frameRate > 24 ? 1000 / 24 : 1000 / settings.frameRate; // the answer to everything. - var quality = 0.66; - - if (session.webPquality === 0) { - width = 1920; - height = 1080; - timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; - } else if (session.webPquality === 1) { - width = 1280; - height = 720; - timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; - } else if (session.webPquality === 2) { - width = 960; - height = 540; - timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; - } else if (session.webPquality === 3) { - width = 853; - height = 480; - timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; - } else if (session.webPquality === 4) { - width = 640; - height = 360; - timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; - } else if (session.webPquality === 5) { - width = 480; - height = 270; - timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; - } else if (session.webPquality === 6) { - width = 480; - height = 270; - timeout = 1000 / 15; - } else if (session.webPquality === 7) { - width = 480; - height = 270; - timeout = 1000 / 5; - } else if (session.webPquality === 8) { - width = 480; - height = 270; - timeout = 1000 / 3; - } else if (session.webPquality === 9) { - width = 640; - height = 360; - timeout = 1000; - } - - session.webPcanvas.timeout = timeout; - session.webPcanvas.quality = quality; - - if (settings.width < width) { - session.webPcanvas.width = settings.width; - session.webPcanvasCtx.width = settings.width; - } else { - session.webPcanvas.width = width; - session.webPcanvasCtx.width = width; - } - - if (settings.height < height) { - session.webPcanvas.height = settings.height; - session.webPcanvasCtx.height = settings.height; - } else { - session.webPcanvas.height = height; - session.webPcanvasCtx.height = height; - } - - var ar = session.webPcanvas.width / session.webPcanvas.height; - - if (session.forceAspectRatio && session.forceAspectRatio > ar) { - session.webPcanvas.width = session.webPcanvas.height * session.forceAspectRatio; - } else if (session.forceAspectRatio && session.forceAspectRatio <= ar) { - session.webPcanvas.height = session.webPcanvas.width * session.forceAspectRatio; - } - - for (var i in session.pcs) { - try { - if (session.pcs[i].allowWebp) { - // only publish to those seeking this stream - broadcasting = true; - if (!session.pcs[i].sendChannel.bufferedAmount) { - try { - if (!arrayBuffer) { - session.webPcanvasCtx.drawImage(session.videoElement, 0, 0, session.webPcanvas.width, session.webPcanvas.height); - arrayBuffer = dataURItoArraybuffer(session.webPcanvas.toDataURL("image/" + session.webp, session.webPcanvas.quality)); - } - session.pcs[i].sendChannel.send(arrayBuffer); - } catch (e) { - errorlog(e); - } - } - } - } catch (e) { } - } - } catch (e) { - errorlog(e); - makeImagesActive = false; - return; - } - makeImagesActive = false; - - session.webPcanvas.lastTime = session.webPcanvas.nowTime; - session.webPcanvas.nowTime = new Date().getTime(); - - if (broadcasting) { - // wait a bit of time, now that we sent a frame out. - - var time = session.webPcanvas.timeout - (session.webPcanvas.nowTime - session.webPcanvas.lastTime); - if (time <= 0) { - var osc = session.webPcanvas.aCtx.createOscillator(); - osc.connect(session.webPcanvas.silence); - session.webPcanvas.makeImagesTimeout = osc; - osc.start(0); - osc.onended = function () { - this.disconnect(); - makeImages(); - }; - osc.stop(session.webPcanvas.aCtx.currentTime); - - //session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},0); - } else { - var osc = session.webPcanvas.aCtx.createOscillator(); - osc.connect(session.webPcanvas.silence); - session.webPcanvas.makeImagesTimeout = osc; - osc.start(0); - osc.onended = function () { - this.disconnect(); - makeImages(); - }; - osc.stop(session.webPcanvas.aCtx.currentTime + time / 1000); - - //session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},time); - } - } else { - // just double check that we shoulnd't be broadcasting. - for (var i in session.pcs) { - if (session.pcs[i].allowWebp) { - var osc = session.webPcanvas.aCtx.createOscillator(); - osc.connect(session.webPcanvas.silence); - session.webPcanvas.makeImagesTimeout = osc; - osc.start(0); - osc.onended = function () { - this.disconnect(); - makeImages(); - }; - osc.stop(session.webPcanvas.aCtx.currentTime + time / 1000); - //session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},0); - return; - } - } - log("Stopping webP broadcast."); - } -} - -var updateUserListTimeout = null; -var updateUserListActive = false; -function updateUserList() { - if (session.showList === true) { - // continue - } else if (session.showList !== true && (session.cleanOutput || session.scene !== false || !session.roomid || session.director || session.showList === false)) { - return; - } - clearInterval(updateUserListTimeout); - updateUserListTimeout = setTimeout(function () { - if (updateUserListActive) { - return; - } - updateUserListActive = true; - try { - var added = false; - getById("userList").innerHTML = ""; - - for (var UUID in session.rpcs) { - if ((session.rpcs[UUID].videoElement && session.rpcs[UUID].streamSrc && session.rpcs[UUID].streamSrc.getTracks().length) || session.rpcs[UUID].canvas || session.rpcs[UUID].imageElement) { - if (session.rpcs[UUID].videoElement && document.body.contains(session.rpcs[UUID].videoElement)) { - continue; - } else if (session.rpcs[UUID].canvas && document.body.contains(session.rpcs[UUID].canvas)) { - continue; - } else if (session.rpcs[UUID].imageElement && document.body.contains(session.rpcs[UUID].imageElement)) { - continue; - } - } - if (session.rpcs[UUID].virtualHangup) { - // end of screen share / director ? - continue; - } - if (session.rpcs[UUID].videoMuted || (!session.rpcs[UUID].imageElement && !session.rpcs[UUID].canvas) || (session.infocus && session.infocus !== UUID) || (!session.rpcs[UUID].defaultSpeaker && session.activeSpeaker)) { - if (session.directorList.indexOf(UUID) >= 0) { - if (!session.rpcs[UUID].streamSrc) { - // director not active yet, so we won't bother showing it. - continue; - } - } - - var insert = document.createElement("div"); - if (session.rpcs[UUID].label) { - insert.innerText = session.rpcs[UUID].label.split("\\n")[0] + ""; - } else if (session.directorList.indexOf(UUID) >= 0) { - miniTranslate(insert, "director"); - //insert.innerHTML = getTranslation("director"); - } else { - miniTranslate(insert, "unknown-user"); - //insert.innerHTML = getTranslation("unknown-user"); - } - try { - insert.dataset.UUID = UUID; - insert.dataset.sid = session.rpcs[UUID].streamID; - insert.title = "Stream ID: " + session.rpcs[UUID].streamID; - insert.addEventListener("click", function (e) { - // show stats of video if double clicked - log("clicked"); - try { - if (session.statsMenu !== false) { - var uid = e.currentTarget.dataset.UUID; - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, uid); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - e.stopPropagation(); - return false; - } - } - } catch (e) { - errorlog(e); - } - }); - if (session.statsMenu) { - if ("stats" in session.rpcs[UUID]) { - if (getById("menuStatsBox")) { - clearInterval(getById("menuStatsBox").interval); - getById("menuStatsBox").remove(); - } - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, UUID); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID); - } - } - } catch (e) { } - - getById("userList").appendChild(insert); - - if (session.rpcs[UUID].videoElement) { - var volumeBarInsert = document.createElement("input"); - volumeBarInsert.type = "range"; - volumeBarInsert.className = "hidden"; - volumeBarInsert.setAttribute("orient", "vertical"); - volumeBarInsert.max = 100; - volumeBarInsert.min = 0; - volumeBarInsert.step = 1; - volumeBarInsert.dataset.UUID = UUID; - volumeBarInsert.setAttribute("value", parseFloat(session.rpcs[UUID].videoElement.volume) * 100); - volumeBarInsert.title = volumeBarInsert.value + "%"; - volumeBarInsert.oninput = function (e) { - session.rpcs[this.dataset.UUID].videoElement.volume = parseInt(this.value) / 100 || 0; - if (!session.rpcs[this.dataset.UUID].videoElement.volume) { - this.parentNode.classList.add("red"); - } else { - this.parentNode.classList.remove("red"); - } - }; - volumeBarInsert.onchange = function (e) { - this.parentNode.querySelector("input").classList.toggle("hidden"); - }; - var volumeButton = document.createElement("i"); - volumeButton.className = "las la-volume-up"; - volumeButton.onclick = function () { - this.parentNode.querySelector("input").classList.toggle("hidden"); - this.parentNode.volumeBarInsert.focus(); - }; - var volumeInsert = document.createElement("div"); - volumeInsert.className = "volume-control-userlist"; - volumeInsert.volumeBarInsert = volumeBarInsert; - insert.appendChild(volumeInsert); - volumeInsert.appendChild(volumeBarInsert); - volumeInsert.appendChild(volumeButton); - } - - if (session.rpcs[UUID].remoteMuteState || !session.rpcs[UUID].streamSrc) { - var muteInsert = document.createElement("div"); - muteInsert.className = "video-mute-state-userlist"; - muteInsert.innerHTML = ''; - insert.appendChild(muteInsert); - } else if (session.rpcs[UUID].voiceMeter) { - insert.appendChild(session.rpcs[UUID].voiceMeter); - } - //getById("userList").innerHTML += "
"; - added = true; - } - } - - if (!added) { - getById("connectUsers").style.display = "none"; - } else { - getById("connectUsers").style.display = "block"; - } - } catch (e) { } - updateUserListActive = false; - }, 200); -} - -function resetCanvas() { - log("resetCanvas();"); - if (!session.streamSrc) { - checkBasicStreamsExist(); - return; - } - session.streamSrc.getVideoTracks().forEach(track => { - session.canvasSource.width = track.getSettings().width || 1280; - session.canvasSource.height = track.getSettings().height || 720; - }); -} - -function initEffectsImage() { - if (!session.effectsImage) { - if (!session.selectedImage_contents) { - session.selectedImage_contents = getById("selectImage_contents"); - } - if (session.selectedImage_contents.querySelector("img")) { - session.effectsImage = session.selectedImage_contents.querySelector("img"); - session.effectsImage.classList.add("selectedContentEffectsImage"); - } else if (session.defaultBackgroundImages && session.defaultBackgroundImages.length) { - session.effectsImage = document.createElement("img"); - session.effectsImage.onload = function () { - URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory - }; - session.effectsImage.src = session.defaultBackgroundImages[0]; - session.effectsImage.classList.add("selectedContentEffectsImage"); - } else { - session.effectsImage = document.createElement("img"); - session.effectsImage.onload = function () { - URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory - }; - session.effectsImage.src = "./media/bg_sample.webp"; - } - } -} - -var LaunchTFWorkerCallback = false; -function TFLiteWorker() { - if (session.tfliteModule == false) { - LaunchTFWorkerCallback = true; - return; - } - if (TFLITELOADING) { - LaunchTFWorkerCallback = true; - return; - } - LaunchTFWorkerCallback = false; - log("TFLiteWorker() called"); - - initEffectsImage(); - //if (session.tfliteModule.looping){return;} - - const segmentationWidth = 256; - const segmentationHeight = 144; - const segmentationPixelCount = segmentationWidth * segmentationHeight; - const inputMemoryOffset = session.tfliteModule._getInputMemoryOffset() / 4; - const outputMemoryOffset = session.tfliteModule._getOutputMemoryOffset() / 4; - const segmentationMask = new ImageData(segmentationWidth, segmentationHeight); - const segmentationMaskCanvas = document.createElement("canvas"); - segmentationMaskCanvas.width = segmentationWidth; - segmentationMaskCanvas.height = segmentationHeight; - const segmentationMaskCtx = segmentationMaskCanvas.getContext("2d", { alpha: true, willReadFrequently: true }); - session.tfliteModule.nowTime = new Date().getTime(); - session.tfliteModule.offsetTime = 0; - - var slow = 0; - var slower = false; - - async function process() { - if (!(session.effect == "3" || session.effect == "4" || session.effect == "5")) { - //session.tfliteModule.looping=false; - errorlog("shouldn't happen"); - return; - } - - if (session.tfliteModule.activelyProcessing) { - return; - } - session.tfliteModule.activelyProcessing = true; - - if (session.mobile) { - if (screenWidth !== window.innerWidth) { - screenWidth = window.innerWidth; - - //session.tfliteModule.looping=false; - session.tfliteModule.activelyProcessing = false; - - setTimeout(function () { - updateRenderOutpipe(); - }, 200); - return; - } - } - - try { - segmentationMaskCtx.filter = "none"; - segmentationMaskCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, segmentationWidth, segmentationHeight); - - const imageData = segmentationMaskCtx.getImageData(0, 0, segmentationWidth, segmentationHeight); - - for (let i = 0; i < segmentationPixelCount; i++) { - session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3] = imageData.data[i * 4] / 255; - session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 1] = imageData.data[i * 4 + 1] / 255; - session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 2] = imageData.data[i * 4 + 2] / 255; - } - - session.tfliteModule._runInference(); - - for (let i = 0; i < segmentationPixelCount; i++) { - const background = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2]; - const person = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2 + 1]; - const shift = Math.max(background, person); - const backgroundExp = Math.exp(background - shift); - const personExp = Math.exp(person - shift); - segmentationMask.data[i * 4 + 3] = Math.min(Math.pow((255 * personExp) / (backgroundExp + personExp), 1.5) - 10, 255); // softmax - } - - segmentationMaskCtx.putImageData(segmentationMask, 0, 0); - - session.canvasCtx.globalCompositeOperation = "copy"; - if ((session.mobile && !session.flagship) || slower) { - session.canvasCtx.filter = "blur(4px)"; - } else { - session.canvasCtx.filter = "blur(8px)"; - } - - session.canvasCtx.drawImage(segmentationMaskCanvas, 0, 0, segmentationWidth, segmentationHeight, 0, 0, session.canvasSource.width, session.canvasSource.height); - - session.canvasCtx.globalCompositeOperation = "source-in"; - session.canvasCtx.filter = "none"; - session.canvasCtx.drawImage(session.canvasSource, 0, 0); - - session.canvasCtx.globalCompositeOperation = "destination-over"; - - if (session.effect == "4") { - // greenscreen - session.canvasCtx.filter = "none"; - session.canvasCtx.fillStyle = "#0F0"; - session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height); - } else if (session.effect == "5") { - session.canvasCtx.filter = "none"; - if (session.effectsImage.complete) { - try { - session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height); - } catch (e) { } - } - } else if (session.effect == "3") { - // BLUR - if (session.effectValue) { - session.canvasCtx.filter = "blur(" + parseInt(session.effectValue) * 2 + "px)"; - } else { - session.canvasCtx.filter = "blur(4px)"; // Does not work on Safari - } - session.canvasCtx.drawImage(session.canvasSource, 0, 0); - session.canvasCtx.filter = "none"; - } else { - session.tfliteModule.activelyProcessing = false; - //session.tfliteModule.looping=false; - return; - } - - ////// - } catch (e) { - errorlog(e); - session.tfliteModule.activelyProcessing = false; - //session.tfliteModule.looping=false; - return; - } - session.tfliteModule.lastTime = session.tfliteModule.nowTime; - session.tfliteModule.nowTime = new Date().getTime(); - - var time = 30 - (session.tfliteModule.nowTime - session.tfliteModule.lastTime || 0); - time = time + (session.tfliteModule.offsetTime || 0); - session.tfliteModule.activelyProcessing = false; - - slow -= 1; - - if (time <= 0) { - if (time < -40) { - slow += 1; - if (slow > 100) { - slower = true; - } - } - session.tfliteModule.offsetTime = 0; - } else { - slow -= 2; - session.tfliteModule.offsetTime = time || 0; - } - } - - async function processiOS() { - if (!(session.effect == "3" || session.effect == "4" || session.effect == "5")) { - errorlog("shouldn't happen"); - //session.tfliteModule.looping=false; - return; - } - if (session.tfliteModule.activelyProcessing) { - return; - } - session.tfliteModule.activelyProcessing = true; - - if (screenWidth !== window.innerWidth) { - screenWidth = window.innerWidth; - setTimeout(function () { - updateRenderOutpipe(); - }, 200); - //session.tfliteModule.looping=false; - session.tfliteModule.activelyProcessing = false; - return; - } - - try { - segmentationMaskCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, segmentationWidth, segmentationHeight); - - var imageData = segmentationMaskCtx.getImageData(0, 0, segmentationWidth, segmentationHeight); - - for (let i = 0; i < segmentationPixelCount; i++) { - session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3] = imageData.data[i * 4] / 255; - session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 1] = imageData.data[i * 4 + 1] / 255; - session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 2] = imageData.data[i * 4 + 2] / 255; - } - - session.tfliteModule._runInference(); - - for (let i = 0; i < segmentationPixelCount; i++) { - const background = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2]; - const person = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2 + 1]; - const shift = Math.max(background, person); - const backgroundExp = Math.exp(background - shift); - const personExp = Math.exp(person - shift); - segmentationMask.data[i * 4 + 3] = 255 - (255 * personExp) / (backgroundExp + personExp); // softmax - } - - segmentationMaskCtx.putImageData(segmentationMask, 0, 0); - - session.canvasCtx.globalCompositeOperation = "copy"; - session.canvasCtx.drawImage(session.canvasSource, 0, 0); - - session.canvasCtx.globalCompositeOperation = "destination-out"; - session.canvasCtx.drawImage(segmentationMaskCanvas, 0, 0, segmentationWidth, segmentationHeight, 0, 0, session.canvasSource.width, session.canvasSource.height); - - session.canvasCtx.globalCompositeOperation = "destination-over"; - - if (session.effect == "4") { - // greenscreen - session.canvasCtx.fillStyle = "#0F0"; - session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height); - } else if (session.effect == "5") { - if (session.effectsImage.complete) { - try { - session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height); - } catch (e) { } - } - } else if (session.effect == "3") { - // BLUR - - const width = canvasBG.width; - const height = canvasBG.height; - ctxBG.drawImage(session.canvasSource, 0, 0, width, height); - imageData = ctxBG.getImageData(0, 0, width, height); - - const { data } = imageData; - - // THE BELOW BLUR CODE polyfil is by David Enke - // MIT License: Copyright (c) 2019 - // https://github.com/steveseguin/context-filter-polyfill/blob/master/src/filters/blur.filter.ts - const wm = width - 1; - const hm = height - 1; - const rad1 = amount + 1; - const r = []; - const g = []; - const b = []; - //const a = []; - - const vmin = []; - const vmax = []; - - let iterations = 3; // 1 - 3 - let p, p1, p2; - while (iterations-- > 0) { - let yw = 0; - let yi = 0; - - for (let y = 0; y < height; y++) { - let rsum = data[yw] * rad1; - let gsum = data[yw + 1] * rad1; - let bsum = data[yw + 2] * rad1; - for (let i = 1; i <= amount; i++) { - p = yw + ((i > wm ? wm : i) << 2); - rsum += data[p++]; - gsum += data[p++]; - bsum += data[p++]; - } - for (let x = 0; x < width; x++) { - r[yi] = rsum; - g[yi] = gsum; - b[yi] = bsum; - if (y === 0) { - vmin[x] = ((p = x + rad1) < wm ? p : wm) << 2; - vmax[x] = (p = x - amount) > 0 ? p << 2 : 0; - } - p1 = yw + vmin[x]; - p2 = yw + vmax[x]; - rsum += data[p1++] - data[p2++]; - gsum += data[p1++] - data[p2++]; - bsum += data[p1++] - data[p2++]; - yi++; - } - yw += width << 2; - } - - for (let x = 0; x < width; x++) { - let yp = x; - let rsum = r[yp] * rad1; - let gsum = g[yp] * rad1; - let bsum = b[yp] * rad1; - for (let i = 1; i <= amount; i++) { - yp += i > hm ? 0 : width; - rsum += r[yp]; - gsum += g[yp]; - bsum += b[yp]; - } - yi = x << 2; - for (let y = 0; y < height; y++) { - data[yi] = (rsum * mulSum) >>> shgSum; - data[yi + 1] = (gsum * mulSum) >>> shgSum; - data[yi + 2] = (bsum * mulSum) >>> shgSum; - if (x === 0) { - vmin[y] = ((p = y + rad1) < hm ? p : hm) * width; - vmax[y] = (p = y - amount) > 0 ? p * width : 0; - } - p1 = x + vmin[y]; - p2 = x + vmax[y]; - rsum += r[p1] - r[p2]; - gsum += g[p1] - g[p2]; - bsum += b[p1] - b[p2]; - yi += width << 2; - } - } - } - ////////////// END OF BLUR CODE - MIT LICENCED. - ctxBG.putImageData(imageData, 0, 0); - session.canvasCtx.drawImage(canvasBG, 0, 0, width, height, 0, 0, session.canvas.width, session.canvas.height); - } else { - session.tfliteModule.activelyProcessing = false; - //session.tfliteModule.looping=false; - return; - } - } catch (e) { - session.tfliteModule.activelyProcessing = false; - //session.tfliteModule.looping=false; - errorlog(e); - return; - } - - session.tfliteModule.lastTime = session.tfliteModule.nowTime; - session.tfliteModule.nowTime = new Date().getTime(); - - var time = 30 - (session.tfliteModule.nowTime - session.tfliteModule.lastTime || 0); - time = time + (session.tfliteModule.offsetTime || 0); - - slow -= 1; - if (time <= 0) { - if (time < -40) { - slow += 1; - if (slow > 100) { - slower = true; - } - } - session.tfliteModule.offsetTime = 0; - } else { - slow -= 2; - session.tfliteModule.offsetTime = time || 0; - } - session.tfliteModule.activelyProcessing = false; - } - //session.tfliteModule.looping=true; - - var screenWidth = window.innerWidth; - - if (iOS || iPad || SafariVersion) { - var canvasBG = document.createElement("canvas"); - var ctxBG = canvasBG.getContext("2d", { alpha: false }); - var amount = 1.0; - var mulTable = [1, 57, 41, 21, 203, 34, 97, 73, 227, 91, 149, 62, 105, 45, 39, 137, 241, 107, 3, 173, 39, 71, 65, 238, 219, 101, 187, 87, 81, 151, 141, 133, 249, 117, 221, 209, 197, 187, 177, 169, 5, 153, 73, 139, 133, 127, 243, 233, 223, 107, 103, 99, 191, 23, 177, 171, 165, 159, 77, 149, 9, 139, 135, 131, 253, 245, 119, 231, 224, 109, 211, 103, 25, 195, 189, 23, 45, 175, 171, 83, 81, 79, 155, 151, 147, 9, 141, 137, 67, 131, 129, 251, 123, 30, 235, 115, 113, 221, 217, 53, 13, 51, 50, 49, 193, 189, 185, 91, 179, 175, 43, 169, 83, 163, 5, 79, 155, 19, 75, 147, 145, 143, 35, 69, 17, 67, 33, 65, 255, 251, 247, 243, 239, 59, 29, 229, 113, 111, 219, 27, 213, 105, 207, 51, 201, 199, 49, 193, 191, 47, 93, 183, 181, 179, 11, 87, 43, 85, 167, 165, 163, 161, 159, 157, 155, 77, 19, 75, 37, 73, 145, 143, 141, 35, 138, 137, 135, 67, 33, 131, 129, 255, 63, 250, 247, 61, 121, 239, 237, 117, 29, 229, 227, 225, 111, 55, 109, 216, 213, 211, 209, 207, 205, 203, 201, 199, 197, 195, 193, 48, 190, 47, 93, 185, 183, 181, 179, 178, 176, 175, 173, 171, 85, 21, 167, 165, 41, 163, 161, 5, 79, 157, 78, 154, 153, 19, 75, 149, 74, 147, 73, 144, 143, 71, 141, 140, 139, 137, 17, 135, 134, 133, 66, 131, 65, 129, 1]; - var mulSum = mulTable[amount]; - var shgTable = [0, 9, 10, 10, 14, 12, 14, 14, 16, 15, 16, 15, 16, 15, 15, 17, 18, 17, 12, 18, 16, 17, 17, 19, 19, 18, 19, 18, 18, 19, 19, 19, 20, 19, 20, 20, 20, 20, 20, 20, 15, 20, 19, 20, 20, 20, 21, 21, 21, 20, 20, 20, 21, 18, 21, 21, 21, 21, 20, 21, 17, 21, 21, 21, 22, 22, 21, 22, 22, 21, 22, 21, 19, 22, 22, 19, 20, 22, 22, 21, 21, 21, 22, 22, 22, 18, 22, 22, 21, 22, 22, 23, 22, 20, 23, 22, 22, 23, 23, 21, 19, 21, 21, 21, 23, 23, 23, 22, 23, 23, 21, 23, 22, 23, 18, 22, 23, 20, 22, 23, 23, 23, 21, 22, 20, 22, 21, 22, 24, 24, 24, 24, 24, 22, 21, 24, 23, 23, 24, 21, 24, 23, 24, 22, 24, 24, 22, 24, 24, 22, 23, 24, 24, 24, 20, 23, 22, 23, 24, 24, 24, 24, 24, 24, 24, 23, 21, 23, 22, 23, 24, 24, 24, 22, 24, 24, 24, 23, 22, 24, 24, 25, 23, 25, 25, 23, 24, 25, 25, 24, 22, 25, 25, 25, 24, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 25, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 22, 25, 25, 23, 25, 25, 20, 24, 25, 24, 25, 25, 22, 24, 25, 24, 25, 24, 25, 25, 24, 25, 25, 25, 25, 22, 25, 25, 25, 24, 25, 24, 25, 18]; - var shgSum = shgTable[amount]; - - log("session.canvas: " + session.canvas.width + "x" + session.canvas.height); - canvasBG.width = parseInt(session.canvas.width / 12); - canvasBG.height = parseInt(session.canvas.height / 12); - ctxBG.width = canvasBG.width; - ctxBG.height = canvasBG.height; - try { - session.tfliteModule.stopOscillator = setupOscillator(processiOS, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - errorlog(e); - session.tfliteModule.stopOscillator = setupOscillator(processiOS, 30); - } - } else { - try { - session.tfliteModule.stopOscillator = setupOscillator(process, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - errorlog(e); - session.tfliteModule.stopOscillator = setupOscillator(process, 30); - } - } -} - -var insertableStreamWorker = null; - -function setupSenderTransform(sender, UUID = false) { - if (!insertableStreamWorker) { - insertableStreamWorker = new Worker("./insertableStreamWorker.js", { name: "Insertable Stream worker" }); - insertableStreamWorker.onmessage = event => { - if (event.data === "insertableStreamWorkerLoaded") { - if (session.encodedInsertableStreams == "e2ee") { - if (session.password) { - insertableStreamWorker.postMessage({ cryptoPhrase: session.password + session.salt + "aDdedSaLt123" }); // salt ⚔️ rainbow - } else { - insertableStreamWorker.postMessage({ cryptoKey: "aabbccddeeff00112233445566778899" }); - } - } else if (session.encodedInsertableStreams == "lyra") { - insertableStreamWorker.postMessage({ - lyraCodecModule: session.lyraCodecModule - }); - } - } else { - console.log(event.data); - } - }; - } - - try { - const senderStreams = sender.createEncodedStreams(); - const { readable, writable } = senderStreams; - let operation = "pass"; - - if (session.encodedInsertableStreams == "e2ee") { - operation = "encode"; - } else if (session.encodedInsertableStreams == "red") { - operation = "redencode"; - } else if (session.encodedInsertableStreams == "lyra" && sender.track && sender.track.kind === "audio") { - operation = "lyraencode"; - } else if (UUID && session.pcs[UUID] && session.pcs[UUID].preferAudioCodec == "lyra" && sender.track && sender.track.kind === "audio") { - operation = "lyraencode"; - } - - insertableStreamWorker.postMessage( - { - operation: operation, - readable, - writable - }, - [readable, writable] - ); - } catch (e) { - errorlog(e); - } -} - -function setupReceiverTransform(receiver, UUID = false) { - if (!insertableStreamWorker) { - insertableStreamWorker = new Worker("./insertableStreamWorker.js", { name: "Insertable Stream worker" }); - insertableStreamWorker.onmessage = event => { - if (event.data === "insertableStreamWorkerLoaded") { - if (session.encodedInsertableStreams == "e2ee") { - if (session.password) { - insertableStreamWorker.postMessage({ cryptoPhrase: session.password + session.salt + "aDdedSaLt123" }); // salt ⚔️ rainbow - } else { - insertableStreamWorker.postMessage({ cryptoKey: "aabbccddeeff00112233445566778899" }); - } - } else if (session.encodedInsertableStreams == "lyra") { - insertableStreamWorker.postMessage({ - lyraCodecModule: session.lyraCodecModule - }); - } - } else { - console.log(event.data); - } - }; - } - - try { - let operation = "pass"; - - if (session.encodedInsertableStreams == "e2ee") { - operation = "decode"; - } else if (session.encodedInsertableStreams == "red") { - operation = "reddecode"; - } else if (session.encodedInsertableStreams == "lyra" && receiver.track && receiver.track.kind === "audio") { - operation = "lyradecode"; - } - - const receiverStreams = receiver.createEncodedStreams(); - const { readable, writable } = receiverStreams; - - insertableStreamWorker.postMessage( - { - operation: operation, - readable, - writable - }, - [readable, writable] - ); - } catch (e) { - errorlog(e); - } -} - -function mainMeshMask() { - if (session.TFJSModel === null || session.TFJSModel === true) { - setTimeout(function () { - mainMeshMask(); - }, 1000); - return; - } - function heatMapColorforValue(value) { - var h = parseInt((1.0 - value) * 240); - if (h < 0) { - h = 0; - } - if (h > 240) { - h = 240; - } - return "hsl(" + h + ", 100%, 50%)"; - } - async function process() { - if (session.TFJSModel.activelyProcessing) { - return; - } - session.TFJSModel.activelyProcessing = true; - - if (session.effect !== "6") { - if (session.TFJSModel.timeoutDraw) { - session.TFJSModel.timeoutDraw(); - session.TFJSModel.timeoutDraw = null; - } - session.TFJSModel.activelyProcessing = false; - return; - } - - const predictions = await session.TFJSModel.estimateFaces({ - input: session.canvasSource - }); - - var output = []; - if (predictions.length > 0) { - for (let j = 0; j < predictions.length; j++) { - const fp = predictions[j].annotations; - session.canvasCtx.fillStyle = "#000000"; - session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height); - const keypoints = predictions[j].scaledMesh; - for (let i = 0; i < keypoints.length; i++) { - var [x, y, z] = keypoints[i]; - x = parseInt(x); - y = parseInt(y); - z = parseInt(z); - if (session.pushEffectsData) { - output.push(x); - output.push(y); - } - session.canvasCtx.fillStyle = heatMapColorforValue((z + 40) / 60); - session.canvasCtx.fillRect(x, y, 5, 5); - } - } - } - - if (session.pushEffectsData) { - //output = FastIntegerCompression.compress(output); - //log(output); - if (isIFrame) { - parent.postMessage( - { - effectsData: output, - eID: session.pushEffectsData - }, - session.iframetarget - ); - } else { - for (var i in session.pcs) { - if (!session.pcs[i].sendChannel.bufferedAmount) { - // don't overload things. - session.sendMessage({ effectsData: output, eID: session.effect }, i); - } - } - } - } - - if (!session.TFJSModel.timeoutDraw) { - try { - session.TFJSModel.timeoutDraw = setupOscillator(process, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - session.TFJSModel.timeoutDraw = setupOscillator(process, 30); // setTimeout(function(){draw();},33); - } - } - session.TFJSModel.activelyProcessing = false; - } - process(); -} - -var faceDetector = false; -var faceAlignment = false; -var activeDetection = false; - -function drawFace() { - if (session.effect !== "1") { - return; - } - if (faceAlignment) { - faceAlignment(); - return; - } else if (faceAlignment === null) { - return; - } - faceAlignment = null; - - var timers = {}; - timers.activelyProcessingDraw = false; - - var ctx = session.canvasCtx; - - function fde1() { - warnlog("LOADED drawFace()"); - - var lastFace = {}; - - //session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; - //session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; - - lastFace.x = session.canvasSource.width / 2; - lastFace.y = session.canvasSource.height / 2; - lastFace.w = session.canvasSource.width; - lastFace.h = session.canvasSource.height; - - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - function detectFace() { - if (activeDetection) { - return; - } - activeDetection = true; - if (session.effect !== "1") { - return; - } - try { - faceDetector - .detect(session.canvasSource) - .then(faces => { - if (faces.length) { - for (let face of faces) { - lastFace.x = face.boundingBox.x; - lastFace.y = face.boundingBox.y; - lastFace.w = face.boundingBox.width; - lastFace.h = face.boundingBox.height; - break; - } - } - //setTimeout(function(){draw();},0); - }) - .catch(e => { - errorlog("Boo, Face Detection failed: " + e); - }); - } catch (e) { } - setTimeout(function () { - detectFace(); - }, 200); - activeDetection = false; - } - - var wh = null; - var xa = null; - var ya = null; - - function draw() { - if (timers.activelyProcessingDraw) { - return; - } - timers.activelyProcessingDraw = true; - - if (session.effect !== "1") { - timers.activelyProcessingDraw = false; - if (timers.timeoutDraw) { - timers.timeoutDraw(); - timers.timeoutDraw = null; - } - return; - } - - try { - if (!session.canvasSource.width) { - timers.activelyProcessingDraw = false; - return; - } - if (wh === null && session.canvasSource.width) { - wh = Math.pow((session.canvasSource.width * session.canvasSource.width) / 36, 0.5); - - xa = 0; - ya = 0; - } - - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - if (lastFace.w) { - wh = wh * 0.999 + Math.pow(lastFace.w * lastFace.h, 0.5) * 0.001; - - var w = wh * 6; - - if (w > session.canvasSource.width) { - w = session.canvasSource.width; - } - if (session.canvasSource.height > session.canvasSource.width) { - if (w > session.canvasSource.height && session.canvasSource.height > session.canvasSource.width) { - w = session.canvasSource.height; - } - } else if (w > session.canvasSource.width) { - w = session.canvasSource.width; - } - - var h = (w / session.canvasSource.width) * session.canvasSource.height; - - xa = xa * 0.998 + 0.002 * (lastFace.x + lastFace.w / 2); - ya = ya * 0.998 + 0.002 * (lastFace.y + lastFace.h / 2); - - var x = xa - w / 2; - var y = ya - h / 2; - - if (x < 0) { - x = 0; - } - if (y < 0) { - y = 0; - } - - if (x > session.canvasSource.width - w) { - x = session.canvasSource.width - w; - } - if (y > session.canvasSource.height - h) { - y = session.canvasSource.height - h; - } - - if (x < 0) { - x = 0; - } - if (y < 0) { - y = 0; - } - } - //console.log(x, y, w, h, session.canvasSource.width, session.canvasSource.height); - ctx.drawImage(session.canvasSource, x, y, w, h, 0, 0, session.canvasSource.width, session.canvasSource.height); - //ctx.beginPath(); - //ctx.rect(lastFace.x, lastFace.y, lastFace.w, lastFace.h); - // ctx.stroke(); - } catch (e) { } - - if (!timers.timeoutDraw) { - try { - timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33); - } - } else { - var res = timers.timeoutDraw("check"); - if (res) { - try { - timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33); - } - } - } - timers.activelyProcessingDraw = false; - } - - if (window.FaceDetector == undefined) { - if (!session.cleanOutput) { - warnUser("Face Detection API not detected.\n\nYou may be able to enable it here: chrome://flags/#enable-experimental-web-platform-features"); - } - faceDetector = false; - } else { - faceDetector = new FaceDetector(); - } - - function fde2() { - if (!timers.activelyProcessingDraw) { - draw(); - } - if (!activeDetection) { - detectFace(); - } - } - fde2(); - return fde2; - } - faceAlignment = fde1(); -} -//////// END CANVAS EFFECTS /////////////////// - -var getFacesActive = false; -async function getFaces() { - if (getFacesActive) { - return; - } - getFacesActive = true; - - if (session.grabFaceData) { - if (!faceDetector) { - if (window.FaceDetector == undefined) { - if (!session.cleanOutput) { - warnUser("Face Detection API not detected.\n\nYou may be able to enable it here: chrome://flags/#enable-experimental-web-platform-features"); - } - session.grabFaceData = false; - faceDetector = false; - - getFacesActive = false; - return; - } else { - session.grabFaceData = 1; - faceDetector = new FaceDetector(); - } - } - try { - var videos = {}; - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement) { - await faceDetector - .detect(session.rpcs[UUID].videoElement) - .then(faces => { - videos[session.rpcs[UUID].streamID] = {}; - videos[session.rpcs[UUID].streamID].videoWidth = session.rpcs[UUID].videoElement.videoWidth; - videos[session.rpcs[UUID].streamID].videoHeight = session.rpcs[UUID].videoElement.videoHeight; - videos[session.rpcs[UUID].streamID].faces = faces; - }) - .catch(e => { - //errorlog("Boo, Face Detection failed: " + e); - }); - } - } - log(videos); - } catch (e) { } - pokeIframeAPI("face-tracking-data", videos); - setTimeout(function () { - getFaces(); - }, 200); - } - - getFacesActive = false; -} - -////// - -var simpleDrawMain = false; -function simpleDraw(reinit = false) { - let supported = ["8", "overlay"]; - if (!supported.includes(session.effect)) { - errorlog("not a valid effeect?"); - return; - } - if (simpleDrawMain) { - simpleDrawMain(reinit); - return; - } else if (simpleDrawMain === null) { - return; - } - simpleDrawMain = null; - - var timers = {}; - timers.activelyProcessingDraw = false; - - function fde1() { - try { - log("LOADED simpleDraw()"); - - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - function draw() { - if (timers.activelyProcessingDraw) { - return; - } - timers.activelyProcessingDraw = true; - if (!supported.includes(session.effect)) { - if (timers.timeoutDraw) { - timers.timeoutDraw(); - timers.timeoutDraw = null; - } - timers.activelyProcessingDraw = false; - return; - } - try { - if (!session.canvasSource.width) { - timers.activelyProcessingDraw = false; - return; - } - - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, session.canvasSource.width, session.canvasSource.height); - - if (session.effect === "overlay" && session.foregroundImg && session.foregroundImg.complete) { - session.canvasCtx.drawImage(session.foregroundImg, 0, 0, session.canvas.width, session.canvas.height); - } - } catch (e) { - errorlog(e); - } - - if (!timers.timeoutDraw) { - try { - timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33); - } - } else { - var res = timers.timeoutDraw("check"); - if (res) { - try { - timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33); - } - } - } - timers.activelyProcessingDraw = false; - } - } catch (e) { - errorlog(e); - timers.activelyProcessingDraw = false; - } - - function fde2(reinit = false) { - if (reinit) { - if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) { - session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; - session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; - } - } - if (!timers.activelyProcessingDraw) { - draw(); - } - } - fde2(); - - return fde2; - } - simpleDrawMain = fde1(); -} -//////// END CANVAS EFFECTS /////////////////// -////// -function changeZoomPosition(event, ele) { - const value = parseFloat(ele.value); - - // Determine which slider was moved and update its pair - if (ele.id === "zoomPositionX1" || ele.id === "zoomPositionX") { - xPosition = value; - // Update other horizontal slider - const otherSlider = ele.id === "zoomPositionX1" ? - getById("zoomPositionX") : - getById("zoomPositionX1"); - if (otherSlider) { - otherSlider.value = value; - } - } else if (ele.id === "zoomPositionY1" || ele.id === "zoomPositionY") { - yPosition = value; - // Update other vertical slider - const otherSlider = ele.id === "zoomPositionY1" ? - getById("zoomPositionY") : - getById("zoomPositionY1"); - if (otherSlider) { - otherSlider.value = value; - } - } -} - -var xPosition = 0.5; // Center position horizontally (0 to 1) -var yPosition = 0.5; // Center position vertically (0 to 1) -var digitalZoomMain = false; -function digitalZoom(resetZoom = false) { - if (session.effect !== "7") { - return; - } - if (digitalZoomMain) { - digitalZoomMain(resetZoom); - return; - } else if (digitalZoomMain === null) { - return; - } - digitalZoomMain = null; - - var activelyProcessingDraw = false; - - function fde1() { - try { - warnlog("LOADED digitalZoom()"); - - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - var xa = null; - var ya = null; - var zz = 1; - - // Modify the draw function to use the position values - function draw() { - if (activelyProcessingDraw) { - return; - } - activelyProcessingDraw = true; - - if (session.effect !== "7") { - zz = 1.0; - xa = 0; - ya = 0; - activelyProcessingDraw = false; - return; - } - - try { - if (!session.canvasSource) { - activelyProcessingDraw = false; - return; - } - - // Use videoWidth/videoHeight for actual current dimensions - const srcWidth = session.canvasSource.videoWidth || session.canvasSource.width; - const srcHeight = session.canvasSource.videoHeight || session.canvasSource.height; - - if (!srcWidth) { - activelyProcessingDraw = false; - return; - } - - session.canvas.height = 2 * parseInt(srcHeight / 2); - session.canvas.width = 2 * parseInt(srcWidth / 2); - - if (session.effectValue) { - // Smooth out the zoom factor - zz = 0.9 * zz + session.effectValue * 0.1; - // Calculate the scaled dimensions - const scaledWidth = srcWidth / zz; - const scaledHeight = srcHeight / zz; - // Calculate the offset based on position sliders - // This centers the zoom on the selected position - xa = (srcWidth - scaledWidth) * xPosition; - ya = (srcHeight - scaledHeight) * yPosition; - // Draw the zoomed region - session.canvasCtx.drawImage( - session.canvasSource, - xa, ya, - scaledWidth, - scaledHeight, - 0, 0, - srcWidth, - srcHeight - ); - } else { - // If no zoom, draw the full image - session.canvasCtx.drawImage( - session.canvasSource, - 0, 0, - srcWidth, - srcHeight, - 0, 0, - srcWidth, - srcHeight - ); - } - } catch (e) { - errorlog(e); - } - activelyProcessingDraw = false; - } - } catch (e) { - errorlog(e); - activelyProcessingDraw = false; - } - - function fde2(resetZoom = false) { - if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) { - session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; - session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; - } - - if (resetZoom) { - xa = null; - ya = null; - zz = 1; - } - - try { - setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - setupOscillator(draw, 40); // setTimeout(function(){draw();},33); - } - - draw(); - } - - fde2(); - - return fde2; - } - digitalZoomMain = fde1(); - digitalZoomMain(resetZoom); -} - -function rgbToHsv(r, g, b) { - r /= 255; g /= 255; b /= 255; - const max = Math.max(r, g, b), min = Math.min(r, g, b); - let h, s, v = max; - const d = max - min; - s = max === 0 ? 0 : d / max; - - if (max === min) { - h = 0; - } else { - switch (max) { - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; break; - } - h /= 6; - } - return [h, s, v]; -} - -// real green scree filter -var chromaKeyMain = false; -function chromaKey(reinit = false) { - let supported = ["14", "15"]; - if (!supported.includes(session.effect)) { - warnlog("not a valid effect"); - return; - } - if (chromaKeyMain) { - chromaKeyMain(reinit); - return; - } else if (chromaKeyMain === null) { - return; - } - chromaKeyMain = null; - var timers = {}; - timers.activelyProcessingDraw = false; - - function fde1() { - try { - log("LOADED chromaKey()"); - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - - function processChromaKey() { - if (timers.activelyProcessingDraw) return; - timers.activelyProcessingDraw = true; - - if (!supported.includes(session.effect)) { - if (timers.timeoutDraw) { - timers.timeoutDraw(); - timers.timeoutDraw = null; - } - timers.activelyProcessingDraw = false; - return; - } - - try { - if (!session.canvasSource.width) { - timers.activelyProcessingDraw = false; - return; - } - - session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); - session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); - session.canvasCtx.drawImage(session.canvasSource, 0, 0); - - const imageData = session.canvasCtx.getImageData(0, 0, session.canvas.width, session.canvas.height); - const data = imageData.data; - - const threshold = parseInt(session.effectValue) || 80; - const smoothing = 15; - - // Green range in HSV (normalized to 0-1) - const targetHue = 0.33; // Pure green - const hueRange = 0.15; // Range around pure green - - for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - // Quick pre-check for obvious non-green pixels - if (g <= r || g <= b) { - continue; - } - const [h, s, v] = rgbToHsv(r, g, b); - // Calculate how "green" the pixel is - const hueDiff = Math.abs(h - targetHue); - const isInGreenRange = hueDiff <= hueRange || hueDiff >= (1 - hueRange); - if (!isInGreenRange) { - continue; - } - // Green intensity calculation - const greenDominance = (g - Math.max(r, b)) / 255; - const saturationBoost = s * 0.7; // Reduce impact of washed-out greens - const keyValue = (greenDominance * 0.6 + saturationBoost * 0.4) * 100; - let alpha = 255; - if (keyValue > threshold) { - const smoothFactor = Math.min((keyValue - threshold) / smoothing, 1); - alpha = (1 - smoothFactor) * 255; - } - data[i + 3] = alpha; - } - - session.canvasCtx.putImageData(imageData, 0, 0); - - if (session.effect === "15" && session.effectsImage && session.effectsImage.complete) { - session.canvasCtx.globalCompositeOperation = 'destination-over'; - session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height); - session.canvasCtx.globalCompositeOperation = 'source-over'; - } - } catch (e) { - errorlog(e); - } - - if (!timers.timeoutDraw) { - try { - timers.timeoutDraw = setupOscillator(processChromaKey, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - timers.timeoutDraw = setupOscillator(processChromaKey, 40); - } - } else { - var res = timers.timeoutDraw("check"); - if (res) { - try { - timers.timeoutDraw = setupOscillator(processChromaKey, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); - } catch (e) { - timers.timeoutDraw = setupOscillator(processChromaKey, 40); - } - } - } - timers.activelyProcessingDraw = false; - } - } catch (e) { - errorlog(e); - timers.activelyProcessingDraw = false; - } - - function fde2(reinit = false) { - if (reinit) { - if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) { - session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; - session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; - } - } - if (!timers.activelyProcessingDraw) { - processChromaKey(); - } - } - fde2(); - return fde2; - } - chromaKeyMain = fde1(); -} -//////// END CANVAS EFFECTS /////////////////// - -function getNativeOutputResolution() { - try { - if (session.videoElement && session.videoElement.srcObject) { - var tracks = session.videoElement.srcObject.getVideoTracks(); - if (tracks.length && tracks[0].getSettings) { - return tracks[0].getSettings(); - } else { - return false; - } - } else if (session.videoElement && session.videoElement.videoWidth && session.videoElement.videoHeight) { - return { width: session.videoElement.videoWidth, height: session.videoElement.videoHeight }; - } - } catch (e) { - return false; - } -} - -function toggleSceneStats(button) { - var UUID = button.dataset.UUID; - - if (button.value == 1) { - button.value = 0; - button.classList.remove("pressed"); - button.ariaPressed = "false"; - if (UUID) { - session.rpcs[UUID].allowGraphs = false; - } else { - session.allowDirectorGraph = false; - } - } else { - button.value = 1; - button.classList.add("pressed"); - button.ariaPressed = "true"; - if (UUID) { - session.rpcs[UUID].allowGraphs = true; - } else { - session.allowDirectorGraph = true; - } - } - - if (UUID) { - var controls = getById("container_" + UUID); - } else { - var controls = getById("container_director"); - } - - if (button.value == 1) { - controls.querySelectorAll("[data-no-scenes]").forEach(ele => { - ele.classList.remove("hidden"); - if (ele.dataset.message) { - ele.innerHTML = "Requesting data .."; - } - }); - - if (controls.querySelector('[data-action-type="stats-graphs-bitrate"]')) { - controls.querySelector('[data-action-type="stats-graphs-bitrate"]').classList.remove("hidden"); - } - if (controls.querySelector('[data-action-type="stats-graphs-details"]')) { - controls.querySelector('[data-action-type="stats-graphs-details"]').classList.remove("hidden"); - } - if (UUID) { - session.sendRequest({ requestStatsContinuous: true }, UUID); - } - } else { - if (UUID) { - session.sendRequest({ requestStatsContinuous: false }, UUID); - } - if (controls.querySelector('[data-action-type="stats-graphs-bitrate"]')) { - controls.querySelector('[data-action-type="stats-graphs-bitrate"]').classList.add("hidden"); - } - if (controls.querySelector('[data-action-type="stats-graphs-details"]')) { - controls.querySelector('[data-action-type="stats-graphs-details"]').classList.add("hidden"); - } - } -} -function getColor(value) { - var hue = (value * 120).toString(10); - return ["hsl(", hue, ",100%,50%)"].join(""); -} - -function plotData(info, UUID, uuid) { - // type = "bitrate" or "nacks" - log("plot data"); - - var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-bitrate"]'); - - if (!container) { - log("container not found"); - return; - } - var canvas = getById("container_" + UUID).querySelector('canvas[data-uid="' + uuid + '"]'); - var canvasNew = false; - if (!canvas) { - canvasNew = true; - canvas = document.createElement("canvas"); - canvas.height = 50; - canvas.width = 124; - canvas.className = "canvasStats"; - canvas.history_nacks = []; - canvas.history_bitrate = []; - canvas.target = 4000; - if (info.scene) { - canvas.title = "Scene: " + info.scene + ". Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments."; - } else if (info.label) { - canvas.title = "Label: " + info.label + ". Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments"; - } else { - canvas.title = "Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments"; - } - - canvas.dataset.uid = uuid; - container.appendChild(canvas); - } - - selfDestructElement(UUID, uuid); - - var context = canvas.getContext("2d"); - - var bitrate = 0; - if ("video_bitrate_kbps" in info) { - bitrate = info.video_bitrate_kbps; - } - if (isNaN(bitrate)) { - bitrate = 0; - } - - if (bitrate < 0) { - bitrate = 0; - } - - var nacks = 0; - if ("nacks_per_second" in info) { - nacks = info.nacks_per_second; - } - if (isNaN(nacks)) { - nacks = 0; - } - if (nacks < 0) { - nacks = 0; - } - - var height = context.canvas.height; - var width = context.canvas.width; - - canvas.history_nacks.push(nacks); - canvas.history_bitrate.push(bitrate); - - canvas.history_nacks = canvas.history_nacks.slice(-125); - canvas.history_bitrate = canvas.history_bitrate.slice(-125); - - var maxBitrate = Math.max(...canvas.history_bitrate); - - var target = canvas.target || 4000; - if (target && maxBitrate > target) { - canvas.target = maxBitrate * 1.5; // set it higher than it needs to be, so it doens't jump around a lot - var yScale = height / canvas.target; - context.clearRect(0, 0, width, height); - var x = width - 1; - var w = 1; - - for (var i = 0; i < canvas.history_bitrate.length; i++) { - var nacks = canvas.history_nacks[i]; - var bitrate = canvas.history_bitrate[i]; - - var val = (10 - nacks) / 10; - if (val > 1) { - val = 1; - } else if (val < 0) { - val = 0; - } - var color = getColor(val); - var y = height - bitrate * yScale; - context.fillStyle = color; - context.fillRect(x, y, w, height); - context.fillStyle = "#DDD5"; - context.fillRect(x, y - 2, w, 4); - - if (y - 5 > 0) { - context.fillStyle = "#FFF3"; - context.fillRect(x, y + 2, w, 1); - } - - var imageData = context.getImageData(1, 0, width - 1, height); - context.putImageData(imageData, 0, 0); - context.clearRect(width - 1, 0, 1, height); - } - - for (var tt = 2500; tt < canvas.target; tt += 2500) { - var y = parseInt(height - tt * yScale); - context.fillStyle = "#0555"; - context.fillRect(0, y, width, 1); - } - log("finished plotting a new y-axis"); - return; - } - //if (info.available_outgoing_bitrate_kbps){ - // limit target, but requires a history - //} - var val = (10 - nacks) / 10; - if (val > 1) { - val = 1; - } else if (val < 0) { - val = 0; - } - var color = getColor(val); - - var yScale = height / target; - - var x = width - 1; - var y = height - bitrate * yScale; - var w = 1; - - context.fillStyle = color; - context.fillRect(x, y, w, height); - context.fillStyle = "#DDD5"; - context.fillRect(x, y - 2, w, 4); - - if (y - 5 > 0) { - context.fillStyle = "#FFF3"; - context.fillRect(x, y + 2, w, 1); - } - - context.fillStyle = "#0555"; - if (canvasNew) { - for (var tt = 2500; tt < target; tt += 2500) { - var y = parseInt(height - tt * yScale); - context.fillRect(0, y, width, 1); - } - } else { - for (var tt = 2500; tt < target; tt += 2500) { - var y = parseInt(height - tt * yScale); - context.fillRect(x, y, 1, 1); - } - } - - var imageData = context.getImageData(1, 0, width - 1, height); - context.putImageData(imageData, 0, 0); - context.clearRect(width - 1, 0, 1, height); - - log("finished plotting"); -} - -function selfDestructElement(UUID, uid) { - getById("container_" + UUID) - .querySelectorAll('[data-uid="' + uid + '"]') - .forEach(ele => { - ele.classList.remove("greyout"); - clearTimeout(ele.selfFadeout); - ele.selfFadeout = setTimeout( - function (ele) { - ele.classList.add("greyout"); - }, - 4000, - ele - ); - - clearTimeout(ele.selfDestruct); - ele.selfDestruct = setTimeout( - function (ele) { - ele.remove(); - }, - 10000, - ele - ); - }); -} - -function directorGraphStats() { - if (!(session.allowDirectorGraph || session.allowGraphs)) { - return; - } - - if (session.director) { - var UUID = "director"; - var maincon = getById("container_director"); - - var sceneStats = {}; - for (var uuid in session.pcs) { - if (session.pcs[uuid].scene !== false) { - sceneStats[uuid] = {}; - sceneStats[uuid].label = session.pcs[uuid].label; - sceneStats[uuid].scene = session.pcs[uuid].scene; - sceneStats[uuid].resolution = session.pcs[uuid].stats.resolution; - sceneStats[uuid].video_bitrate_kbps = session.pcs[uuid].stats.video_bitrate_kbps; - sceneStats[uuid].video_encoder = session.pcs[uuid].stats.video_encoder; - } - } - if (!Object.keys(sceneStats).length) { - maincon.querySelectorAll("[data-no-scenes]").forEach(ele => { - ele.classList.remove("hidden"); - if (ele.dataset.message) { - ele.innerHTML = "No scenes active"; - } - }); - - log("zero size"); - return; - } - maincon.querySelectorAll("[data-no-scenes]").forEach(ele => { - ele.classList.add("hidden"); - }); - - for (var uuid in sceneStats) { - var container = maincon.querySelector('[data-action-type="stats-graphs-details-container"][data-uid="' + uuid + '"]'); - if (!container) { - container = maincon.querySelector('[data-action-type="stats-graphs-details-container"]').cloneNode(true); - container.dataset.uid = uuid; - container.classList.remove("hidden"); - maincon.querySelector('[data-action-type="stats-graphs-details"]').appendChild(container); - } - plotData(sceneStats[uuid], UUID, uuid); - - if ("video_bitrate_kbps" in sceneStats[uuid] && sceneStats[uuid].video_bitrate_kbps !== "video_bitrate_kbps") { - var span = container.querySelector("[data-bitrate]"); - if (span) { - span.classList.remove("hidden"); - span.innerHTML = "video bitrate: " + parseInt(sceneStats[uuid].video_bitrate_kbps) + " (kbps)"; - span.style.cursor = "pointer"; - span.title = "Click to adjust bitrate"; - span.onclick = async function (e) { - e.preventDefault(); - e.stopPropagation(); - var currentUUID = this.closest('[data-action-type="stats-graphs-details-container"]').dataset.uid; - const result = await promptAlt("Select target bitrate (kbps)", false, false, false, false, false, false, { - type: 'select', - options: ['50', '500', '1000', '2000', '5000', '10000', '20000', '[Custom]'], - placeholder: 'Enter custom bitrate in kbps' - }); - if (result) { - var msg = { - targetBitrate: parseInt(result), - UUID: currentUUID, - requestAs: uuid - }; - if (isIFrame) { - parent.postMessage(msg, session.iframetarget); - } - session.sendRequest(msg); - } - }; - } - } - - var span = container.querySelector("[data-scene-name]"); - if (span && "label" in sceneStats[uuid] && sceneStats[uuid].label) { - span.classList.remove("hidden"); - span.innerHTML = "stats for viewer: " + sceneStats[uuid].label; - } else if (span && "scene" in sceneStats[uuid] && sceneStats[uuid].scene !== false) { - span.classList.remove("hidden"); - span.innerHTML = "stats for scene: " + sceneStats[uuid].scene; - } else if (uuid === "meshcast") { - span.classList.remove("hidden"); - span.innerHTML = "stats for meshcast ingest"; - span.title = "You can use &label=xxxx to give your view links a unique label"; - } else { - span.classList.remove("hidden"); - span.innerHTML = "stats for some viewer"; - span.title = "You can use &label=xxxx to give your view links a unique label"; - } - - if ("resolution" in sceneStats[uuid]) { - var span = container.querySelector("[data-resolution]"); - if (span) { - span.classList.remove("hidden"); - span.innerHTML = sceneStats[uuid].resolution; - span.style.cursor = "pointer"; - span.title = "Click to adjust resolution"; - span.onclick = async function (e) { - e.preventDefault(); - e.stopPropagation(); - var currentUUID = this.closest('[data-action-type="stats-graphs-details-container"]').dataset.uid; - const result = await promptAlt("Select target resolution", false, false, false, false, false, false, { - type: 'select', - options: ['360', '720', '1080', '[Custom]'], - placeholder: 'Enter custom height in pixels' - }); - if (result) { - session.requestResolution(currentUUID, 4096, result || 2160, false, uuid); - } - }; - } - } - - if ("video_encoder" in sceneStats[uuid]) { - var span = container.querySelector("[data-video-codec]"); - if (span) { - span.classList.remove("hidden"); - span.innerHTML = "video codec: " + sceneStats[uuid].video_encoder; - } - } - } - } -} - -function remoteStats(msg, UUID) { - - var rpc = session.rpcs && session.rpcs[UUID] ? session.rpcs[UUID] : null; - - if (isIFrame && rpc) { - parent.postMessage({ remoteStats: msg.remoteStats, streamID: rpc.streamID, UUID: UUID }, session.iframetarget); - } - - if (!rpc) { - return; - } - - var allowUI = rpc.allowGraphs || session.allowGraphs; - if (allowUI && session.director) { - var size = 0; - for (var key in msg.remoteStats) { - if (msg.remoteStats.hasOwnProperty(key)) { - size++; - } - } - - if (!size) { - getById("container_" + UUID) - .querySelectorAll("[data-no-scenes]") - .forEach(ele => { - ele.classList.remove("hidden"); - if (ele.dataset.message) { - ele.innerHTML = "No scenes active"; - } - }); - log("zero size"); - } else { - getById("container_" + UUID) - .querySelectorAll("[data-no-scenes]") - .forEach(ele => { - ele.classList.add("hidden"); - }); - - for (var uuid in msg.remoteStats) { - var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-details-container"][data-uid="' + uuid + '"]'); - if (!container) { - container = getById("container_" + UUID) - .querySelector('[data-action-type="stats-graphs-details-container"]') - .cloneNode(true); - container.dataset.uid = uuid; - container.classList.remove("hidden"); - getById("container_" + UUID) - .querySelector('[data-action-type="stats-graphs-details"]') - .appendChild(container); - } - - plotData(msg.remoteStats[uuid], UUID, uuid); - - if ("video_bitrate_kbps" in msg.remoteStats[uuid] && msg.remoteStats[uuid].video_bitrate_kbps !== "video_bitrate_kbps") { - var span = container.querySelector("[data-bitrate]"); - if (span) { - span.classList.remove("hidden"); - span.innerHTML = "video bitrate: " + parseInt(msg.remoteStats[uuid].video_bitrate_kbps) + " (kbps)"; - span.style.cursor = "pointer"; - span.title = "Click to adjust bitrate"; - span.onclick = async function (e) { - e.preventDefault(); - e.stopPropagation(); - const result = await promptAlt("Select target bitrate (kbps)", false, false, false, false, false, false, { - type: 'select', - options: ['50', '500', '1000', '2000', '5000', '10000', '20000', '[Custom]'], - placeholder: 'Enter custom bitrate in kbps' - }); - if (result) { - var msg = { - targetBitrate: parseInt(result), - UUID: UUID, - requestAs: uuid - }; - if (isIFrame) { - parent.postMessage(msg, session.iframetarget); - } - session.sendRequest(msg); - } - }; - } - } - - var span = container.querySelector("[data-scene-name]"); - if (span && "label" in msg.remoteStats[uuid] && msg.remoteStats[uuid].label) { - span.classList.remove("hidden"); - span.innerHTML = "stats for viewer: " + msg.remoteStats[uuid].label; - } else if (span && "scene" in msg.remoteStats[uuid] && msg.remoteStats[uuid].scene !== false) { - span.classList.remove("hidden"); - span.innerHTML = "stats for scene: " + msg.remoteStats[uuid].scene; - } else if (uuid === "meshcast") { - span.classList.remove("hidden"); - span.innerHTML = "stats for meshcast ingest"; - span.title = "You can use &label=xxxx to give your view links a unique label"; - } else { - span.classList.remove("hidden"); - span.innerHTML = "stats for some viewer"; - span.title = "You can use &label=xxxx to give your view links a unique label"; - } - - if ("resolution" in msg.remoteStats[uuid]) { - var span = container.querySelector("[data-resolution]"); - if (span) { - span.classList.remove("hidden"); - span.innerHTML = msg.remoteStats[uuid].resolution; - span.style.cursor = "pointer"; - span.title = "Click to adjust resolution"; - span.onclick = async function (e) { - e.preventDefault(); - e.stopPropagation(); - const result = await promptAlt("Select target resolution", false, false, false, false, false, false, { - type: 'select', - options: ['360', '720', '1080', '1440', '2160', '[Custom]'], - placeholder: 'Enter custom height in pixels' - }); - if (result) { - session.requestResolution(UUID, 4096, result || 2160, false, uuid); - } - }; - } - } - - if ("video_encoder" in msg.remoteStats[uuid]) { - var span = container.querySelector("[data-video-codec]"); - if (span) { - span.classList.remove("hidden"); - span.innerHTML = "video codec: " + msg.remoteStats[uuid].video_encoder; - } - } - } - } - } -} - -function processStats(UUID) { - // for (pc in session.pcs){session.pcs[pc].getStats().then(function(stats) {stats.forEach(stat=>{if (stat.id.includes("RTCIce")){console.log(stat)}})})}; - - if (!session.rpcs || !(UUID in session.rpcs)) { - return; - } - - try { - if (session.rpcs[UUID].videoElement.paused) { - if (session.firstPlayTriggered) { - if (session.audioCtx.state == "suspended") { - // added oct 9th 2022 - try { - session.audioCtx.resume(); - } catch (e) { - warnlog(e); - } - } - if (session.audioCtx.state == "running") { - // NOTE: I Don't know why this was - log("trying to play"); - session.rpcs[UUID].videoElement - .play() - .then(_ => { - log("playing 8"); - //if ((session.audioEffects===true) || session.pushLoudness){ - // updateIncomingAudioElement(UUID); - //} - }) - .catch(warnlog); - } - } - } - } catch (e) { } - - try { - if (session.rpcs[UUID].realUUID && session.rpcs[session.rpcs[UUID].realUUID]) { - var node = session.rpcs[session.rpcs[UUID].realUUID]; - } else { - var node = session.rpcs[UUID]; - } - - var validTrackIds = []; - if (session.rpcs[UUID].streamSrc) { - session.rpcs[UUID].streamSrc.getTracks().forEach(trk => { - validTrackIds.push(trk.id); - }); - } - - if (session.rpcs[UUID].whep) { - processMeshcastStats(UUID); - if (!node.getStats) { - clearTimeout(session.rpcs[UUID].getStatsTimeout); - session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, session.statsInterval, UUID); - - //setTimeout(processStats, session.statsInterval, UUID); // no p2p, so lets do WHEP again manually. - } - } - - if (node.getStats) { - node.getStats().then(function (stats) { - if (!(UUID in session.rpcs)) { - return; - } - - clearTimeout(session.rpcs[UUID].getStatsTimeout); - session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, session.statsInterval, UUID); - - if (!session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"] = {}; - } - - var nominatedCandidate = false; - var candidates = {}; - var ipleakingAllowedRemote = false; - var ipleakingAllowedLocal = false; - - stats.forEach(stat => { - try { - if (stat.id && stat.id.startsWith("DEPRECATED_")) { - return; - } - var trackID = stat.trackIdentifier || stat.id || false; - if (stat.type == "track" && stat.remoteSource) { - if (stat.trackIdentifier && !validTrackIds.includes(stat.trackIdentifier)) { - return; - } - if (stat.id in session.rpcs[UUID].stats) { - session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier; - session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0; - session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0; - session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0; - if ("frameWidth" in stat) { - if ("frameHeight" in stat) { - session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth + " x " + stat.frameHeight; - session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth; - session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight; - } - } - } else { - var media = {}; - media._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0; - media._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0; - media.Jitter_Buffer_ms = 0; - media._trackID = stat.trackIdentifier; - session.rpcs[UUID].stats[stat.id] = media; - if (stat.kind && stat.kind == "audio") { - session.rpcs[UUID].stats[stat.id].type = "Audio Track"; - session.rpcs[UUID].stats[stat.id]._type = "audio"; - } else if (stat.kind && stat.kind == "video") { - session.rpcs[UUID].stats[stat.id].type = "Video Track"; - session.rpcs[UUID].stats[stat.id]._type = "video"; - } - } - } else if (stat.type == "remote-candidate") { - candidates[stat.id] = stat; - if (stat.candidateType != "relay") { - ipleakingAllowedRemote = true; - } - } else if (stat.type == "local-candidate") { - candidates[stat.id] = stat; - if (stat.candidateType != "relay") { - ipleakingAllowedLocal = true; - } - } else if (stat.type == "candidate-pair" && stat.nominated) { - if (!nominatedCandidate) { - nominatedCandidate = stat; - } else if (nominatedCandidate.priority < stat.priority) { - nominatedCandidate = stat; - } - } else if (stat.type == "transport") { - if ("bytesReceived" in stat) { - if ("_bytesReceived" in session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) { - if (session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp) { - if (stat.timestamp) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].total_recv_bitrate_kbps = parseInt((8 * (stat.bytesReceived - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._bytesReceived)) / (stat.timestamp - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp)); - hideStreamLowBandwidth(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].total_recv_bitrate_kbps, UUID); - //changeSceneLowBandwidth(session.rpcs[UUID].stats['Peer-to-Peer_Connection'].total_recv_bitrate_kbps, UUID); - } - } - } - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._bytesReceived = stat.bytesReceived; - } - if ("timestamp" in stat) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp = stat.timestamp; - if (!session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart = stat.timestamp; - } else { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart) / 600) / 100; - } - } - } else if (stat.type == "inbound-rtp" && trackID) { - if (stat.trackIdentifier && !validTrackIds.includes(stat.trackIdentifier)) { - return; - } - session.rpcs[UUID].stats[trackID] = session.rpcs[UUID].stats[trackID] || {}; - if (stat.trackIdentifier) { - session.rpcs[UUID].stats[trackID]._trackID = stat.trackIdentifier; - } - if ("jitterBufferDelay" in stat) { - session.rpcs[UUID].stats[trackID].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[trackID]._jitter_delay_2)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[trackID]._jitter_count_2)) || 0; - session.rpcs[UUID].stats[trackID]._jitter_delay_2 = parseFloat(stat.jitterBufferDelay) || 0; - session.rpcs[UUID].stats[trackID]._jitter_count_2 = parseInt(stat.jitterBufferEmittedCount) || 0; - } - if ("frameWidth" in stat) { - if ("frameHeight" in stat) { - session.rpcs[UUID].stats[trackID].Resolution = stat.frameWidth + " x " + stat.frameHeight; - session.rpcs[UUID].stats[trackID]._frameWidth = stat.frameWidth; - session.rpcs[UUID].stats[trackID]._frameHeight = stat.frameHeight; - } - } - session.rpcs[UUID].stats[trackID].Bitrate_in_kbps = parseInt((8 * (stat.bytesReceived - (session.rpcs[UUID].stats[trackID]._last_bytes || 0))) / (stat.timestamp - session.rpcs[UUID].stats[trackID]._last_time)); - session.rpcs[UUID].stats[trackID]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[trackID]._last_bytes; - session.rpcs[UUID].stats[trackID]._last_time = stat.timestamp || session.rpcs[UUID].stats[trackID]._last_time; - if (stat.mediaType == "video") { - session.rpcs[UUID].stats._codecId = stat.codecId; - session.rpcs[UUID].stats._codecIdTrackId = trackID; - session.rpcs[UUID].stats[trackID].type = "Video Stream"; - session.rpcs[UUID].stats[trackID]._type = "video"; - if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP8") { - session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0; - session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0; - log("OBS PLI FIX MODE ON"); - if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix) { - // heavy packet loss with no pliCount? - session.requestKeyframe(UUID); - session.rpcs[UUID].stats[trackID].nackTrigger = 0; - log("TRYING KEYFRAME"); - } else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) { - session.rpcs[UUID].stats[trackID].nackTrigger = 0; - } - } else if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP9") { - session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0; - session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0; - log("OBS PLI FIX MODE ON"); - if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix * 4) { - // heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often. - session.requestKeyframe(UUID); - session.rpcs[UUID].stats[trackID].nackTrigger = 0; - log("TRYING KEYFRAME"); - } else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) { - session.rpcs[UUID].stats[trackID].nackTrigger = 0; - } - } - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0; - session.rpcs[UUID].stats[trackID].streamErrors_nackCount = stat.nackCount || 0; - //warnlog(stat); - if ("framesPerSecond" in stat) { - session.rpcs[UUID].stats[trackID].FPS = parseInt(stat.framesPerSecond); - } else if ("framesDecoded" in stat && stat.timestamp) { - var lastFramesDecoded = 0; - var lastTimestamp = 0; - try { - lastFramesDecoded = session.rpcs[UUID].stats[trackID]._framesDecoded; - lastTimestamp = session.rpcs[UUID].stats[trackID]._timestamp; - } catch (e) { } - session.rpcs[UUID].stats[trackID].FPS = parseInt((10 * (stat.framesDecoded - lastFramesDecoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10; - //session.rpcs[UUID].stats[trackID].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp)); - session.rpcs[UUID].stats[trackID]._framesDecoded = stat.framesDecoded; - session.rpcs[UUID].stats[trackID]._timestamp = stat.timestamp / 1000; - } - } else if (stat.mediaType == "audio") { - //log("AUDIO LEVEL: "+stat.audioLevel); - session.rpcs[UUID].stats._audioCodecId = stat.codecId; - session.rpcs[UUID].stats._audioCodecIdTrackId = trackID; - session.rpcs[UUID].stats[trackID].type = "Audio Stream"; - session.rpcs[UUID].stats[trackID]._type = "audio"; - if ("audioLevel" in stat) { - session.rpcs[UUID].stats[trackID].audio_level = parseInt(parseFloat(stat.audioLevel) * 10000) / 10000.0; - } - } - if ("packetsLost" in stat && "packetsReceived" in stat) { - if (!("_packetsLost" in session.rpcs[UUID].stats[trackID])) { - session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost; - } - if (!("_packetsReceived" in session.rpcs[UUID].stats[trackID])) { - session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived; - } - if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[trackID])) { - session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = 0; - } - let packetLoss = ((stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost) * 100.0) / (stat.packetsReceived - session.rpcs[UUID].stats[trackID]._packetsReceived + (stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost)) || 0; - /* if (session.rpcs[UUID].stats[trackID]._type && (session.rpcs[UUID].stats[trackID]._type =="video")){ - if (packetLoss>1){ - var data = {}; - data.bitrate = parseInt(session.rpcs[UUID].stats[trackID].Bitrate_in_kbps*0.8); - session.sendRequest(data,UUID); - } else { - var data = {}; - data.bitrate = parseInt(session.rpcs[UUID].stats[trackID].Bitrate_in_kbps*1.1); - session.sendRequest(data,UUID); - } - } */ - session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage * 0.35 + 0.65 * packetLoss; - if (session.rpcs[UUID].signalMeter && session.rpcs[UUID].stats[trackID]._type === "video") { - if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.01) { - if (session.rpcs[UUID].stats[trackID].Bitrate_in_kbps == 0) { - session.rpcs[UUID].signalMeter.dataset.level = 0; - } else { - session.rpcs[UUID].signalMeter.dataset.level = 5; - } - } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.3) { - session.rpcs[UUID].signalMeter.dataset.level = 4; - } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 1.0) { - session.rpcs[UUID].signalMeter.dataset.level = 3; - } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 3.5) { - session.rpcs[UUID].signalMeter.dataset.level = 2; - } else { - session.rpcs[UUID].signalMeter.dataset.level = 1; - } - } - session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived; - session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost; - } - } else if ("_codecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._codecId) { - if ("mimeType" in stat) { - if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]) { - session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType; - } else { - session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {}; - session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType; - } - } - if ("frameHeight" in stat) { - if ("frameWidth" in stat) { - session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth) + " x " + parseInt(stat.frameHeight); - } - } - } else if ("_audioCodecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._audioCodecId) { - if ("mimeType" in stat) { - var addOnDescription = stat.mimeType; - addOnDescription = addOnDescription.replace("audio/", ""); - if ("sdpFmtpLine" in stat) { - if (stat.sdpFmtpLine.includes("useinbandfec=1")) { - addOnDescription += ", /w fec"; - } - } - if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId]) { - } else { - session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId] = {}; - } - session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].codec = addOnDescription; - if (stat.clockRate) { - session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].clockRate = stat.clockRate; - if (stat.channels) { - session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].clockRate += " / " + stat.channels; - } - } - } - } else if (Firefox) { - if ("frameWidth" in stat) { - session.rpcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight; - if ("framesPerSecond" in stat) { - session.rpcs[UUID].stats.resolution += " @ " + stat.framesPerSecond; - } - } - if ("mimeType" in stat && "type" in stat && "id" in stat && stat.type == "codec") { - if (stat.mimeType.includes("video")) { - session.rpcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1]; - } else if (stat.mimeType.includes("audio")) { - session.rpcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1]; - if (stat.clockRate) { - session.rpcs[UUID].stats.audio_clockRate = stat.clockRate; - if (stat.channels) { - session.rpcs[UUID].stats.audio_clockRate += " / " + stat.channels; - } - } else if (stat.sdpFmtpLine) { - session.rpcs[UUID].stats.fmtp = stat.sdpFmtpLine; - } - } - } - /* if ("jitter" in stat){ - if (("kind" in stat) && (stat.kind=="video")){ - session.rpcs[UUID].stats.video_jitter_ms = parseInt(stat.jitter*1000); - } else if (("kind" in stat) && (stat.kind=="audio")){ - session.rpcs[UUID].stats.audio_jitter_ms = parseInt(stat.jitter*1000); - } - } */ - if ("bytesReceived" in stat) { - if ("kind" in stat && stat.kind == "video") { - if ("_bytesReceived_video" in session.rpcs[UUID].stats) { - session.rpcs[UUID].stats.videoBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_video) / ((1024 * session.statsInterval) / 8000)); - } - session.rpcs[UUID].stats._bytesReceived_video = stat.bytesReceived; - } else if ("kind" in stat && stat.kind == "audio") { - if ("_bytesReceived_audio" in session.rpcs[UUID].stats) { - session.rpcs[UUID].stats.audioBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_audio) / ((1024 * session.statsInterval) / 8000)); - } - session.rpcs[UUID].stats._bytesReceived_audio = stat.bytesReceived; - } - } - } - } catch (e) { - errorlog(e); - } - }); - - ////////// - - if (nominatedCandidate) { - if (nominatedCandidate.localCandidateId && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id !== nominatedCandidate.localCandidateId) { - if ("candidateType" in nominatedCandidate) { - try { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_local; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol; - } catch (e) { } - } - } - if (nominatedCandidate.remoteCandidateId && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id !== nominatedCandidate.remoteCandidateId) { - if ("candidateType" in nominatedCandidate) { - try { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_remote; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol; - } catch (e) { } - } - } - if (nominatedCandidate.localCandidateId) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id = nominatedCandidate.localCandidateId; - } - if (nominatedCandidate.remoteCandidateId) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id = nominatedCandidate.remoteCandidateId; - } - if ("currentRoundTripTime" in nominatedCandidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime * 1000; - } - } - - if (nominatedCandidate && nominatedCandidate.localCandidateId) { - if (candidates[nominatedCandidate.localCandidateId]) { - var candidate = candidates[nominatedCandidate.localCandidateId]; - if ("candidateType" in candidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_local = candidate.candidateType; - if (candidate.candidateType === "relay") { - if ("relayProtocol" in candidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol = candidate.relayProtocol; - } else { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol; - } - if ("ip" in candidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP = candidate.ip; - } else { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP; - } - // Store URL for QoS hostname extraction (may not exist in all browsers) - if ("url" in candidate && candidate.url) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_url = candidate.url; - } else { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_url; - } - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_ip_blocking = !ipleakingAllowedLocal; - } else { - try { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_url; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_ip_blocking; - } catch (e) { } - } - } - if ("networkType" in candidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_networkType = candidate.networkType; - } - } - } - if (nominatedCandidate && nominatedCandidate.remoteCandidateId) { - if (candidates[nominatedCandidate.remoteCandidateId]) { - var candidate = candidates[nominatedCandidate.remoteCandidateId]; - if ("candidateType" in candidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_remote = candidate.candidateType; - if (candidate.candidateType === "relay") { - if ("ip" in candidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP = candidate.ip; - } else { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP; - } - if ("relayProtocol" in candidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol = candidate.relayProtocol; - } else { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol; - } - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_ip_blocking = !ipleakingAllowedRemote; - } else { - try { - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol; - delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_ip_blocking; - } catch (e) { } - } - } - if ("networkType" in candidate) { - session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_networkType = candidate.networkType; - } - } - } - - // QoS data accumulation - if (session.qosEnabled && session.qosData && !session.qosData.sent) { - try { - var qd = session.qosData; - var peerStats = session.rpcs[UUID].stats["Peer-to-Peer_Connection"]; - - // Accumulate RTT samples - if (peerStats && peerStats.Round_Trip_Time_ms) { - qd.rttSamples.push(peerStats.Round_Trip_Time_ms); - if (qd.rttSamples.length > 500) qd.rttSamples.shift(); - } - - // Track transport type and TURN servers - if (peerStats && peerStats.candidateType_local) { - if (peerStats.candidateType_local === "relay") { - qd.transportType = "turn"; - // Use URL-based hostname instead of IP (graceful degradation if unavailable) - if (peerStats.local_relay_url && session.qosTurnAllowlist && session.qosTurnAllowlist.length) { - var host = extractTurnHostnameFromUrl(peerStats.local_relay_url); - if (host && session.qosTurnAllowlist.includes(host) && !qd.turnServersUsed.includes(host)) { - qd.turnServersUsed.push(host); - } - } - // Safari/Firefox may not have url - we still get transportType="turn" - } else if (!qd.transportType) { - qd.transportType = "p2p"; - } - if (!qd.candidateTypesLocal.includes(peerStats.candidateType_local)) { - qd.candidateTypesLocal.push(peerStats.candidateType_local); - } - } - if (peerStats && peerStats.candidateType_remote && !qd.candidateTypesRemote.includes(peerStats.candidateType_remote)) { - qd.candidateTypesRemote.push(peerStats.candidateType_remote); - } - - // Accumulate packet loss and jitter from tracks - for (var tid in session.rpcs[UUID].stats) { - var trackStat = session.rpcs[UUID].stats[tid]; - if (trackStat && typeof trackStat === "object") { - if (trackStat._type === "video") { - if (trackStat.packetLoss_in_percentage !== undefined) { - qd.packetLossVideoSamples.push(trackStat.packetLoss_in_percentage); - if (qd.packetLossVideoSamples.length > 500) qd.packetLossVideoSamples.shift(); - } - if (trackStat.Bitrate_in_kbps) { - qd.bitrateSamples.push(trackStat.Bitrate_in_kbps); - if (qd.bitrateSamples.length > 500) qd.bitrateSamples.shift(); - } - if (trackStat.Jitter_Buffer_ms) { - qd.jitterSamples.push(trackStat.Jitter_Buffer_ms); - if (qd.jitterSamples.length > 500) qd.jitterSamples.shift(); - } - if (trackStat.codec) qd.lastVideoCodec = trackStat.codec; - if (trackStat.Resolution) qd.lastResolution = trackStat.Resolution; - } else if (trackStat._type === "audio") { - if (trackStat.packetLoss_in_percentage !== undefined) { - qd.packetLossAudioSamples.push(trackStat.packetLoss_in_percentage); - if (qd.packetLossAudioSamples.length > 500) qd.packetLossAudioSamples.shift(); - } - if (trackStat.codec) qd.lastAudioCodec = trackStat.codec; - } - } - } - } catch (e) { warnlog("QoS accumulation error: " + e); } - } - - playoutdelay(UUID); - - setTimeout(function () { - session.directorSpeakerMute(); - session.directorDisplayMute(); - }, 0); - }); - } - } catch (e) { - errorlog(e); - } - - pokeIframeAPI("view-stats-updated", true, UUID); -} - -// QoS stats collection for publisher outbound connections (session.pcs) -// This runs periodically when QoS is enabled to gather stats from publisher connections -function processPcsQosStats(UUID) { - if (!session.qosEnabled || !session.qosData || session.qosData.sent) return; - if (!session.pcs || !(UUID in session.pcs)) return; - - try { - session.pcs[UUID].getStats().then(function(stats) { - if (!(UUID in session.pcs)) return; - if (!session.qosEnabled || !session.qosData || session.qosData.sent) return; - - var qd = session.qosData; - var nominatedCandidate = null; - var candidates = {}; - - stats.forEach(function(stat) { - try { - if (stat.id && stat.id.startsWith("DEPRECATED_")) return; - - if (stat.type === "outbound-rtp") { - if (stat.kind === "video") { - if (stat.qualityLimitationReason) { - session.pcs[UUID].stats.quality_limitation_reason = stat.qualityLimitationReason; - } - if (stat.frameWidth && stat.frameHeight) { - var res = stat.frameWidth + " x " + stat.frameHeight; - if (stat.framesPerSecond) { - res += " @ " + stat.framesPerSecond; - } - session.pcs[UUID].stats.resolution = res; - if (!qd.lastResolution) qd.lastResolution = res; - } - if (stat.encoderImplementation) { - session.pcs[UUID].stats.encoder = stat.encoderImplementation; - if (!qd.lastVideoCodec) qd.lastVideoCodec = stat.encoderImplementation; - } - // Track bitrate for publishers - if (stat.bytesSent !== undefined && stat.timestamp) { - if (session.pcs[UUID].stats._lastBytesSent !== undefined) { - var timeDiff = stat.timestamp - session.pcs[UUID].stats._lastTimestamp; - if (timeDiff > 0) { - var bitrate = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._lastBytesSent)) / timeDiff); - if (bitrate > 0) { - qd.bitrateSamples.push(bitrate); - if (qd.bitrateSamples.length > 500) qd.bitrateSamples.shift(); - } - } - } - session.pcs[UUID].stats._lastBytesSent = stat.bytesSent; - session.pcs[UUID].stats._lastTimestamp = stat.timestamp; - } - } - } else if (stat.type === "remote-candidate") { - candidates[stat.id] = stat; - if (stat.relayProtocol && stat.ip) { - session.pcs[UUID].stats.remote_relay_IP = stat.ip; - session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol; - } - if (stat.candidateType) { - session.pcs[UUID].stats.candidateType_remote = stat.candidateType; - } - } else if (stat.type === "local-candidate") { - candidates[stat.id] = stat; - if (stat.relayProtocol && stat.ip) { - session.pcs[UUID].stats.local_relayIP = stat.ip; - session.pcs[UUID].stats.local_relayProtocol = stat.relayProtocol; - } - if (stat.candidateType) { - session.pcs[UUID].stats.candidateType_local = stat.candidateType; - } - } else if (stat.type === "candidate-pair" && stat.nominated) { - if (!nominatedCandidate || nominatedCandidate.priority < stat.priority) { - nominatedCandidate = stat; - } - } else if (stat.type === "remote-inbound-rtp") { - // Packet loss from remote peer - if (stat.packetsLost !== undefined && stat.packetsReceived !== undefined) { - var total = stat.packetsLost + stat.packetsReceived; - if (total > 0) { - var lossPercent = (stat.packetsLost / total) * 100; - if (stat.kind === "video") { - qd.packetLossVideoSamples.push(lossPercent); - if (qd.packetLossVideoSamples.length > 500) qd.packetLossVideoSamples.shift(); - } else if (stat.kind === "audio") { - qd.packetLossAudioSamples.push(lossPercent); - if (qd.packetLossAudioSamples.length > 500) qd.packetLossAudioSamples.shift(); - } - } - } - // Jitter from remote peer - if (stat.jitter !== undefined) { - var jitterMs = stat.jitter * 1000; - qd.jitterSamples.push(jitterMs); - if (qd.jitterSamples.length > 500) qd.jitterSamples.shift(); - } - } - } catch (e) { } - }); - - // Process nominated candidate for RTT and transport type - if (nominatedCandidate) { - // RTT - if (nominatedCandidate.totalRoundTripTime && nominatedCandidate.responsesReceived) { - var rtt = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000); - session.pcs[UUID].stats.average_roundTripTime_ms = rtt; - qd.rttSamples.push(rtt); - if (qd.rttSamples.length > 500) qd.rttSamples.shift(); - } - if (nominatedCandidate.currentRoundTripTime) { - var currentRtt = parseInt(nominatedCandidate.currentRoundTripTime * 1000); - qd.rttSamples.push(currentRtt); - if (qd.rttSamples.length > 500) qd.rttSamples.shift(); - } - - // Transport type from nominated candidate - var localCandidate = candidates[nominatedCandidate.localCandidateId]; - var remoteCandidate = candidates[nominatedCandidate.remoteCandidateId]; - - if (localCandidate) { - if (localCandidate.candidateType === "relay") { - qd.transportType = "turn"; - // Use stat.url to get TURN hostname (official servers only) - // Note: stat.url may not be available in all browsers (graceful degradation) - if (localCandidate.url && session.qosTurnAllowlist && session.qosTurnAllowlist.length) { - var host = extractTurnHostnameFromUrl(localCandidate.url); - if (host && session.qosTurnAllowlist.includes(host) && !qd.turnServersUsed.includes(host)) { - qd.turnServersUsed.push(host); - } - } - // If url unavailable (Safari/Firefox), we still track transportType="turn" - // but skip hostname - better no data than wrong/private data - } else if (!qd.transportType || qd.transportType === "unknown") { - qd.transportType = "p2p"; - } - if (!qd.candidateTypesLocal.includes(localCandidate.candidateType)) { - qd.candidateTypesLocal.push(localCandidate.candidateType); - } - } - if (remoteCandidate && !qd.candidateTypesRemote.includes(remoteCandidate.candidateType)) { - qd.candidateTypesRemote.push(remoteCandidate.candidateType); - } - } - - // Schedule next stats collection (every 5 seconds) - if (UUID in session.pcs && session.qosEnabled && session.qosData && !session.qosData.sent) { - clearTimeout(session.pcs[UUID].qosStatsTimeout); - session.pcs[UUID].qosStatsTimeout = setTimeout(processPcsQosStats, 5000, UUID); - } - }).catch(function(e) { - warnlog("QoS pcs stats error: " + e); - }); - } catch (e) { - warnlog("QoS pcs stats error: " + e); - } -} - -function createConnectionDetailsEle(UUID) { - if (!session.rpcs[UUID]) { - return false; - } - - session.rpcs[UUID].connectionDetails = document.createElement("div"); - session.rpcs[UUID].connectionDetails.id = "remoteConnections_" + UUID; - if (session.rpcs[UUID].stats.info && "total_outbound_p2p_connections" in session.rpcs[UUID].stats.info) { - session.rpcs[UUID].connectionDetails.innerText = "🔗" + session.rpcs[UUID].stats.info.total_outbound_p2p_connections; - session.rpcs[UUID].connectionDetails.dataset.value = session.rpcs[UUID].stats.info.total_outbound_p2p_connections; - } - session.rpcs[UUID].connectionDetails.dataset.UUID = UUID; - session.rpcs[UUID].connectionDetails.title = getTranslation("viewer-count"); - session.rpcs[UUID].connectionDetails.className = "rem-con-count"; - - session.rpcs[UUID].connectionDetails.addEventListener("click", function (e) { - // show stats of video if double clicked - log("clicked connectionDetails icon "); - try { - e.preventDefault(); - if (session.statsMenu !== false) { - var uid = e.currentTarget.dataset.UUID; - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, uid); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - e.stopPropagation(); - return false; - } catch (e) { - errorlog(e); - } - }); - return true; -} - -function playoutdelay(UUID) { - // applies a delay to all videos - try { - if ((session.rpcs[UUID].buffer !== false) || (session.buffer != false) || (session.audioBuffer !== false)) { - - // if buffer is set, then session.sync will be set; at least to 0. - var receivers = getReceivers2(UUID).reverse() || []; //session.rpcs[UUID].getReceivers().reverse(); - - if (session.rpcs[UUID].whep) { - receivers = receivers.concat(getReceiversMC(UUID).reverse()); // if I try to reuse getReceivers2, I get some confused stats (not able to tell tracks apart) TODO: see if this issue is a problem else where, esp with screen shares. sstype==3 - } - receivers.forEach(function (receiver) { - try { - for (var tid in session.rpcs[UUID].stats) { - if (typeof session.rpcs[UUID].stats[tid] == "object" && "_trackID" in session.rpcs[UUID].stats[tid] && session.rpcs[UUID].stats[tid]._trackID === receiver.track.id && session.rpcs[UUID].stats[tid]._type == receiver.track.kind && "Jitter_Buffer_ms" in session.rpcs[UUID].stats[tid]) { - //if (ChromiumVersion<=103){ // I don't know the exact version, except I know OBS Studio is 103 and it uses the old way still.netwqor - var sync_offset = 0.0; - if (session.rpcs[UUID].stats[tid]._sync_offset) { - sync_offset = session.rpcs[UUID].stats[tid]._sync_offset || 0; - } else { - session.rpcs[UUID].stats[tid]._sync_offset = 0; - } - var target_buffer = 0; - if (session.rpcs[UUID].stats[tid]._type == "audio") { - if (session.audioBuffer !== false) { - target_buffer = parseFloat(session.audioBuffer || 0); - } else { - target_buffer = parseFloat(session.buffer || 0); - } - } else { - target_buffer = parseFloat(session.buffer || 0); - } - if (session.rpcs[UUID].buffer !== false) { - target_buffer = parseFloat(session.rpcs[UUID].buffer); - } - sync_offset += target_buffer; - sync_offset -= session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0; - if (session.includeRTT && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) { - sync_offset -= parseInt(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms / 2) || 0; // I can't be sure what the actual one-way delay is - } - if (sync_offset > target_buffer) { - sync_offset = target_buffer || 0; - } - if (sync_offset < 0) { - sync_offset = 0; - } - session.rpcs[UUID].stats[tid].Added_Buffer_Delay_ms = sync_offset || 0; - if (session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) { - session.rpcs[UUID].stats[tid].Total_Playout_Delay_ms = sync_offset + parseInt(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms / 2) + session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0; - } - if (session.rpcs[UUID].stats[tid]._type == "audio") { - session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0; - if ("jitterBufferTarget" in receiver) { - receiver.jitterBufferTarget = parseFloat(sync_offset) || 0; - } else { - receiver.playoutDelayHint = parseFloat(sync_offset / 1000) || 0; - } - // receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe - if (session.sync !== false) { - var audio_delay = session.sync || 0; // video is typically showing greater delay than audio. - audio_delay += target_buffer - session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0; - if (receiver.track.kind == "audio" && receiver.track.id in session.rpcs[UUID].inboundAudioPipeline) { - if (session.rpcs[UUID].inboundAudioPipeline[receiver.track.id] && session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode) { - if (audio_delay < 0) { - audio_delay = 0; - } - try { - session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.linearRampToValueAtTime(parseFloat(audio_delay / 1000.0), session.audioCtx.currentTime + parseFloat(session.statsInterval / 9000)); - } catch (e) { - session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.setValueAtTime(parseFloat(audio_delay / 1000.0), session.audioCtx.currentTime + 1); - } - session.rpcs[UUID].stats[tid].Audio_Sync_Delay_ms = audio_delay || 0; - } - } - } - } else if (session.rpcs[UUID].stats[tid]._type == "video") { - session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0; - if ("jitterBufferTarget" in receiver) { - receiver.jitterBufferTarget = parseFloat(sync_offset) || 0; - } else { - receiver.playoutDelayHint = parseFloat(sync_offset / 1000) || 0; - } - // receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe - } - } - } - } catch (e) { - errorlog(e); - } - }); - } - } catch (e) { - errorlog(e); - warnlog("device does not support playout delay"); - } -} - -function printViewStats(menu, UUID) { - // Stats for viewing a remote video - if (session.statsMenu === false) { - return false; - } - - if (!session.rpcs[UUID]) { - menu.innerHTML = "


Remote Publisher Disconnected"; - return false; - } - - var statsObj = session.rpcs[UUID].stats; - var streamID = session.rpcs[UUID].streamID; - var scrollLeft = menu.scrollLeft; - var scrollTop = menu.scrollTop; - menu.innerHTML = "StreamID: " + streamID + "
"; - - if (statsObj.chunked_mode_video && typeof statsObj.chunked_mode_video.buffer_buffer !== "undefined") { - var chunkVideo = statsObj.chunked_mode_video; - var chunkSummary = "Video Buffer: " + parseInt(chunkVideo.buffer_buffer || 0) + " ms / Δ " + parseInt(chunkVideo.buffer_delta || 0) + " ms"; - if (chunkVideo.rebuffering) { - chunkSummary += " (rebuffering)"; - } - menu.innerHTML += chunkSummary + "
"; - if (typeof chunkVideo.fec_repairs !== "undefined" || typeof chunkVideo.nacks_sent !== "undefined") { - var repairSummary = []; - if (typeof chunkVideo.fec_repairs !== "undefined") { - repairSummary.push("FEC " + parseInt(chunkVideo.fec_repairs || 0)); - } - if (typeof chunkVideo.nacks_sent !== "undefined") { - repairSummary.push("NACK " + parseInt(chunkVideo.nacks_sent || 0)); - } - if (repairSummary.length) { - menu.innerHTML += "Video Repairs: " + repairSummary.join(" / ") + "
"; - } - } - } - if (statsObj.chunked_mode_audio && typeof statsObj.chunked_mode_audio.buffer_buffer !== "undefined") { - var chunkAudio = statsObj.chunked_mode_audio; - var audioSummary = "Audio Buffer: " + parseInt(chunkAudio.buffer_buffer || 0) + " ms / Δ " + parseInt(chunkAudio.buffer_delta || 0) + " ms"; - if (chunkAudio.rebuffering) { - audioSummary += " (rebuffering)"; - } - menu.innerHTML += audioSummary + "
"; - } - - //// doesn't work on viewer side. - //if (session.rpcs && session.rpcs[UUID] && session.rpcs[UUID] && session.rpcs[UUID].restartIce){ // only show if available - // menu.innerHTML += ""; - //} - - menu.innerHTML += printValues(statsObj); - menu.scrollTop = scrollTop; - menu.scrollLeft = scrollLeft; - return true; -} - -function plotDataSimple(canvas, bitrate, nacks = 0) { - canvas.height = 50; - canvas.width = 124; - canvas.className = "canvasStats"; - var context = canvas.getContext("2d"); - if (isNaN(bitrate)) { - bitrate = 0; - } - if (isNaN(nacks)) { - nacks = 0; - } - var height = context.canvas.height; - var width = context.canvas.width; - - var val = (10 - nacks) / 10; - if (val > 1) { - val = 1; - } else if (val < 0) { - val = 0; - } - - var yScale = height / 4000; - var x = width - 1; - var y = height - bitrate * yScale; - var w = 1; - - context.fillStyle = getColor(val); - context.fillRect(x, y, w, height); - context.fillStyle = "#FFFFFF55"; - context.fillRect(x, y - 2, w, 4); - - if (y - 5 > 0) { - context.fillStyle = "#FFFFFF44"; - context.fillRect(x, y + 2, w, 1); - } - - context.putImageData(context.getImageData(1, 0, width - 1, height), 0, 0); - context.clearRect(width - 1, 0, 1, height); -} - -function printValues(obj, sort = false) { - // see: printViewStats - var out = ""; - - var keys = Object.keys(obj); - - if (sort) { - keys.sort(); - } - var lat = false; - var lon = false; - - keys.forEach(key => { - if (key.startsWith("_")) return; - if (typeof obj[key] === "object" && obj[key] !== null) { - let tmp = sanitizeChat(key); - out += `
  • ${tmp}

  • `; - if (key == "info") { - out += printValues(obj[key]); - } else if (key == "meta") { - out += `
  • `; - Object.entries(obj[key]).forEach(([category, data]) => { - if (data.type === "file" && data.filetype && data.filetype.startsWith("image/")) { - out += `
    - ${data.label || category} -
    - ${category} - ${formatFileSize(data.size)} -
    -
    `; - } else if (data.type === "url") { - out += `
    - ${category}: - - ${truncateUrl(data.value)} - -
    `; - } else { - out += `
    - ${category}: - ${data.value || ''} -
    `; - } - }); - - out += `
  • `; - } else { - out += printValues(obj[key], true); - } - } else { - try { - var unit = ""; - - var value = obj[key]; - - var stat = sanitizeChat(key); - - stat = stat.charAt(0).toUpperCase() + stat.slice(1); - - var hint = ""; - - if (typeof obj[key] == "string") { - value = sanitizeChat(value); - } - - if (key == "useragent") { - value = "" + value + ""; - } - - if (key == "Bitrate_in_kbps") { - var unit = " kbps"; - stat = "Bitrate"; - hint = "You can refer to the documentation for ways to increase the target bitrate"; - } else if (key == "type") { - var unit = ""; - stat = "Type"; - - if (value == "Audio Track") { - value = "🔊 " + value; - //out += ""; - } - - if (value == "Video Track") { - value = "📺 " + value; - } - } else if (key == "packetLoss_in_percentage") { - var unit = " %"; - stat = "Packet Loss 📶"; - value = parseInt(parseFloat(value) * 10000) / 10000.0; - hint = "A high packet loss will lower quality of the media"; - } else if (key == "local_relay_IP") { - value = "" + value + ""; - } else if (key == "remote_relay_IP") { - value = "" + value + ""; - } else if (key == "local_ip_blocking" && value) { - console.warn("Your system or connection is blocking p2p traffic"); - value = "⚠️ You're blocking"; - hint = "no direct p2p connection made because of YOUR browser or system setting"; - } else if (key == "remote_ip_blocking" && value) { - console.warn("A remote client is blocking p2p traffic"); - value = "⚠️ They're blocking"; - hint = "no direct p2p connection made because of THEIR browser or system setting"; - } else if (key == "candidateType_local" && value == "relay") { - value = "💸 relay server"; - hint = "no direct p2p connection made; using the TURN relay servers."; - stat = "Candidate type - Local"; - } else if (key == "candidateType_remote" && value == "relay") { - value = "💸 relay server"; - hint = "no direct p2p connection made; using the TURN relay servers."; - stat = "Candidate type - Remote"; - } else if (key == "candidateType_local" && value == "host") { - hint = "No NAT firewall, typical of LAN to LAN"; - stat = "Candidate type - Local"; - } else if (key == "candidateType_remote" && value == "host") { - hint = "No NAT firewall, typical of LAN to LAN"; - stat = "Candidate type - Remote"; - } else if (key == "candidateType_local" && value == "srflx") { - hint = "direct p2p, but NAT firewall likely"; - stat = "Candidate type - Local"; - } else if (key == "candidateType_remote" && value == "srflx") { - hint = "direct p2p, but NAT firewall likely"; - stat = "Candidate type - Remote"; - } else if (key == "height_url") { - if (value == false) { - return; - } - } else if (key == "width_url") { - if (value == false) { - return; - } - } else if (key == "height_url") { - if (value == false) { - return; - } - } else if (key == "version") { - stat = "VDO.Ninja Version"; - } else if (key == "platform") { - stat = "Platform (OS)"; - } else if (key == "iPhone12Up") { - stat = "iPhone 12 and up"; - } else if (key == "aec_url") { - stat = "Echo-Cancellation"; - } else if (key == "agc_url") { - stat = "Auto-Gain (agc)"; - } else if (key == "denoise_url") { - stat = "De-noising "; - } else if (key == "audio_level") { - stat = "Audio Level"; - } else if (key == "Jitter_Buffer_ms") { - var unit = " ms"; - stat = "Jitter Buffer Delay"; - } else if (key == "Added_Buffer_Delay_ms") { - var unit = " ms"; - stat = "Added Buffer Delay"; - hint = "Value of playout buffer delay added if using &buffer"; - } else if (key == "Total_Playout_Delay_ms") { - // doesn't include bluetooth / monitor / capture delay, etc. - var unit = " ms"; - stat = "Total Playout Delay"; - hint = "Network latency + Jitter buffer + any manually added playout delay"; - } else if (value === null) { - value = "null"; - } else if (key == "stereo_url") { - stat = "Pro-Audio
    (Stereo-mode)"; - if (value == 3) { - value = "3 (outbound hi-fi)
    Use Headphones"; - } else if (value == 1) { - value = "1 (in & out hi-fi)
    Use Headphones"; - } else if (value == 2) { - value = "3 (inbound hi-fi)"; - } else if (value == 4) { - value = "3 (multichannel)
    Use Headphones"; - } else if (value == 5) { - value = "5 (auto-mode)
    Use Headphones"; - } - } else if (value === false) { - return; - } else if (value === "false") { - return; - } else if (key == "lat") { - lat = value; - if (lat && lon) { - const mapWidth = 250; - const mapHeight = 250; - const x = (lon + 180) * (mapWidth / 360); - const mapRatio = mapHeight / mapWidth; - const latRad = (lat * Math.PI) / 180; - const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2)); - const y = mapHeight / 2 - ((mapWidth * mercN) / (2 * Math.PI)) * mapRatio; - out += - '
    \ - World Map\ -
    \ -
    '; - } - } else if (key == "lon") { - lon = value; - if (lat && lon) { - const mapWidth = 250; - const mapHeight = 250; - const x = (lon + 180) * (mapWidth / 360); - const mapRatio = mapHeight / mapWidth; - const latRad = (lat * Math.PI) / 180; - const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2)); - const y = mapHeight / 2 - ((mapWidth * mercN) / (2 * Math.PI)) * mapRatio; - out += - '
    \ - World Map\ -
    \ -
    '; - out += 'Open Location in Google Maps'; - } - } - - stat = stat.replaceAll("_", " "); - stat = stat.trim(); - - if (hint) { - out += "
  • " + stat + "" + value + unit + "
  • "; - } else { - out += "
  • " + stat + "" + value + unit + "
  • "; - } - } catch (e) { - warnlog(e); - } - - } - }); - return out; -} - -function processMeshcastStats(UUID) { - try { - session.rpcs[UUID].whep.getStats().then(function (stats) { - if (!(UUID in session.rpcs)) { - return; - } - - if (!session.rpcs[UUID].stats["WHEP_Connection"]) { - // meshcast - session.rpcs[UUID].stats["WHEP_Connection"] = {}; - } - - // var qos = false;] - - var nominatedCandidate = false; - var candidates = {}; - var ipleakingAllowedRemote = false; - var ipleakingAllowedLocal = false; - - stats.forEach(stat => { - if (stat.id && stat.id.startsWith("DEPRECATED_")) { - return; - } - - var trackID = stat.trackIdentifier || stat.id || false; - - if (stat.type == "remote-candidate") { - candidates[stat.id] = stat; - if (stat.candidateType != "relay") { - ipleakingAllowedRemote = true; - } - } else if (stat.type == "local-candidate") { - candidates[stat.id] = stat; - if (stat.candidateType != "relay") { - ipleakingAllowedLocal = true; - } - } else if (stat.type == "candidate-pair" && stat.nominated) { - if (!nominatedCandidate) { - nominatedCandidate = stat; - } else if (nominatedCandidate.priority < stat.priority) { - nominatedCandidate = stat; - } - } else if (stat.type == "track" && stat.remoteSource) { - if (stat.id in session.rpcs[UUID].stats) { - session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier; - session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0; - session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0; - session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0; - if ("frameWidth" in stat) { - if ("frameHeight" in stat) { - session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth + " x " + stat.frameHeight; - session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth; - session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight; - } - } - } else { - session.rpcs[UUID].stats[stat.id] = {}; - session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0; - session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0; - session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = 0; - session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier; - if (stat.kind && stat.kind == "audio") { - session.rpcs[UUID].stats[stat.id].type = "Audio Track"; - session.rpcs[UUID].stats[stat.id]._type = "audio"; - } else if (stat.kind && stat.kind == "video") { - session.rpcs[UUID].stats[stat.id].type = "Video Track"; - session.rpcs[UUID].stats[stat.id]._type = "video"; - } - } - } else if (stat.type == "transport") { - if ("bytesReceived" in stat) { - if ("_bytesReceived" in session.rpcs[UUID].stats["WHEP_Connection"]) { - if (session.rpcs[UUID].stats["WHEP_Connection"]._timestamp) { - if (stat.timestamp) { - session.rpcs[UUID].stats["WHEP_Connection"].total_recv_bitrate_kbps = parseInt((8 * (stat.bytesReceived - session.rpcs[UUID].stats["WHEP_Connection"]._bytesReceived)) / (stat.timestamp - session.rpcs[UUID].stats["WHEP_Connection"]._timestamp)); - } - } - } - session.rpcs[UUID].stats["WHEP_Connection"]._bytesReceived = stat.bytesReceived; - } - if ("timestamp" in stat) { - session.rpcs[UUID].stats["WHEP_Connection"]._timestamp = stat.timestamp; - if (!session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart) { - session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart = stat.timestamp; - } else { - session.rpcs[UUID].stats["WHEP_Connection"].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart) / 600) / 100; - } - } - } else if (stat.type == "inbound-rtp" && trackID) { - session.rpcs[UUID].stats[trackID] = session.rpcs[UUID].stats[trackID] || {}; - - if (stat.trackIdentifier) { - session.rpcs[UUID].stats[trackID]._trackID = stat.trackIdentifier; - } - if ("jitterBufferDelay" in stat) { - session.rpcs[UUID].stats[trackID].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[trackID]._jitter_delay_2)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[trackID]._jitter_count_2)) || 0; - session.rpcs[UUID].stats[trackID]._jitter_delay_2 = parseFloat(stat.jitterBufferDelay) || 0; - session.rpcs[UUID].stats[trackID]._jitter_count_2 = parseInt(stat.jitterBufferEmittedCount) || 0; - } - - if ("frameWidth" in stat) { - if ("frameHeight" in stat) { - session.rpcs[UUID].stats[trackID].Resolution = stat.frameWidth + " x " + stat.frameHeight; - session.rpcs[UUID].stats[trackID]._frameWidth = stat.frameWidth; - session.rpcs[UUID].stats[trackID]._frameHeight = stat.frameHeight; - } - } - - session.rpcs[UUID].stats[trackID].Bitrate_in_kbps = parseInt((8 * (stat.bytesReceived - (session.rpcs[UUID].stats[trackID]._last_bytes || 0))) / (stat.timestamp - session.rpcs[UUID].stats[trackID]._last_time)); - - session.rpcs[UUID].stats[trackID]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[trackID]._last_bytes; - session.rpcs[UUID].stats[trackID]._last_time = stat.timestamp || session.rpcs[UUID].stats[trackID]._last_time; - - session.rpcs[UUID].stats._codecId = stat.codecId; - session.rpcs[UUID].stats._codecIdTrackId = trackID; - - if (stat.mediaType == "video") { - session.rpcs[UUID].stats[trackID].type = "Video Stream"; - session.rpcs[UUID].stats[trackID]._type = "video"; - if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP8") { - session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0; - session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0; - log("OBS PLI FIX MODE ON"); - if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix) { - // heavy packet loss with no pliCount? - session.requestKeyframe(UUID); - session.rpcs[UUID].stats[trackID].nackTrigger = 0; - log("TRYING KEYFRAME"); - } else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) { - session.rpcs[UUID].stats[trackID].nackTrigger = 0; - } - } else if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP9") { - session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0; - session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0; - log("OBS PLI FIX MODE ON"); - if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix * 4) { - // heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often. - session.requestKeyframe(UUID); - session.rpcs[UUID].stats[trackID].nackTrigger = 0; - log("TRYING KEYFRAME"); - } else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) { - session.rpcs[UUID].stats[trackID].nackTrigger = 0; - } - } - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0; - session.rpcs[UUID].stats[trackID].streamErrors_nackCount = stat.nackCount || 0; - //warnlog(stat); - if ("framesPerSecond" in stat) { - session.rpcs[UUID].stats[trackID].FPS = parseInt(stat.framesPerSecond); - } else if ("framesDecoded" in stat && stat.timestamp) { - var lastFramesDecoded = 0; - var lastTimestamp = 0; - try { - lastFramesDecoded = session.rpcs[UUID].stats[trackID]._framesDecoded; - lastTimestamp = session.rpcs[UUID].stats[trackID]._timestamp; - } catch (e) { } - session.rpcs[UUID].stats[trackID].FPS = parseInt((10 * (stat.framesDecoded - lastFramesDecoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10; - //session.rpcs[UUID].stats[trackID].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp)); - session.rpcs[UUID].stats[trackID]._framesDecoded = stat.framesDecoded; - session.rpcs[UUID].stats[trackID]._timestamp = stat.timestamp / 1000; - } - } else if (stat.mediaType == "audio") { - //log("AUDIO LEVEL: "+stat.audioLevel); - session.rpcs[UUID].stats[trackID].type = "Audio Stream"; - session.rpcs[UUID].stats[trackID]._type = "audio"; - if ("audioLevel" in stat) { - session.rpcs[UUID].stats[trackID].audio_level = parseInt(parseFloat(stat.audioLevel) * 10000) / 10000.0; - } - } - - if ("packetsLost" in stat && "packetsReceived" in stat) { - if (!("_packetsLost" in session.rpcs[UUID].stats[trackID])) { - session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost; - } - if (!("_packetsReceived" in session.rpcs[UUID].stats[trackID])) { - session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived; - } - if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[trackID])) { - session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = 0; - } - session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage * 0.35 + (0.65 * ((stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost) * 100.0)) / (stat.packetsReceived - session.rpcs[UUID].stats[trackID]._packetsReceived + (stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost)) || 0; - if (session.rpcs[UUID].stats[trackID]._type === "video") { - qos = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage; // packet loss of video track - } - if (session.rpcs[UUID].signalMeter && session.rpcs[UUID].stats[trackID]._type === "video") { - if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.01) { - if (session.rpcs[UUID].stats[trackID].Bitrate_in_kbps == 0) { - session.rpcs[UUID].signalMeter.dataset.level = 0; - } else { - session.rpcs[UUID].signalMeter.dataset.level = 5; - } - } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.3) { - session.rpcs[UUID].signalMeter.dataset.level = 4; - } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 1.0) { - session.rpcs[UUID].signalMeter.dataset.level = 3; - } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 3.5) { - session.rpcs[UUID].signalMeter.dataset.level = 2; - } else { - session.rpcs[UUID].signalMeter.dataset.level = 1; - } - } - session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived; - session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost; - } - } else if ("_codecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._codecId) { - if ("mimeType" in stat) { - if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]) { - session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType; - } else { - session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {}; - session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType; - } - } - if ("frameHeight" in stat) { - if ("frameWidth" in stat) { - session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth) + " x " + parseInt(stat.frameHeight); - } - } - } else if (Firefox) { - if ("mimeType" in stat && "type" in stat && "id" in stat && stat.type == "codec") { - if (stat.mimeType.includes("video")) { - session.rpcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1]; - } else if (stat.mimeType.includes("audio")) { - session.rpcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1]; - if (stat.clockRate) { - session.rpcs[UUID].stats.audio_clockRate = stat.clockRate; - if (stat.channels) { - session.rpcs[UUID].stats.audio_clockRate += " / " + stat.channels; - } - } else if (stat.sdpFmtpLine) { - session.rpcs[UUID].stats.fmtp = stat.sdpFmtpLine; - } - } - } - - if ("frameWidth" in stat) { - session.rpcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight; - if ("framesPerSecond" in stat) { - session.rpcs[UUID].stats.resolution += " @ " + stat.framesPerSecond; - } - } - - if ("bytesReceived" in stat) { - if ("kind" in stat && stat.kind == "video") { - if ("_bytesReceived_video" in session.rpcs[UUID].stats) { - session.rpcs[UUID].stats.videoBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_video) / ((1024 * session.statsInterval) / 8000)); - } - session.rpcs[UUID].stats._bytesReceived_video = stat.bytesReceived; - } else if ("kind" in stat && stat.kind == "audio") { - if ("_bytesReceived_audio" in session.rpcs[UUID].stats) { - session.rpcs[UUID].stats.audioBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_audio) / ((1024 * session.statsInterval) / 8000)); - } - session.rpcs[UUID].stats._bytesReceived_audio = stat.bytesReceived; - } - } - } - }); - - //////////// - - if (nominatedCandidate) { - if ("currentRoundTripTime" in nominatedCandidate) { - session.rpcs[UUID].stats["WHEP_Connection"].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime * 1000; - } - } - - if (nominatedCandidate && nominatedCandidate.remoteCandidateId) { - if (candidates[nominatedCandidate.remoteCandidateId]) { - var candidate = candidates[nominatedCandidate.remoteCandidateId]; - if ("candidateType" in candidate) { - session.rpcs[UUID].stats["WHEP_Connection"].candidateType_remote = candidate.candidateType; - if (candidate.candidateType === "relay") { - if ("relayProtocol" in candidate) { - session.rpcs[UUID].stats["WHEP_Connection"].remote_relay_protocol = candidate.relayProtocol; - } - if ("ip" in candidate) { - session.rpcs[UUID].stats["WHEP_Connection"].remote_relay_IP = candidate.ip; - } - } else { - try { - delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP; - delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol; - } catch (e) { } - } - if ("networkType" in candidate) { - session.rpcs[UUID].stats["WHEP_Connection"].remote_networkType = candidate.networkType; - } - } - } - } - if (nominatedCandidate && nominatedCandidate.localCandidateId) { - if (candidates[nominatedCandidate.localCandidateId]) { - var candidate = candidates[nominatedCandidate.localCandidateId]; - if ("candidateType" in candidate) { - session.rpcs[UUID].stats["WHEP_Connection"].candidateType_local = candidate.candidateType; - if (candidate.candidateType === "relay") { - if ("relayProtocol" in candidate) { - session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol = candidate.relayProtocol; - } - if ("ip" in candidate) { - session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP = candidate.ip; - } - } else { - try { - delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP; - delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol; - } catch (e) { } - } - } - - if ("networkType" in candidate) { - session.rpcs[UUID].stats["WHEP_Connection"].local_networkType = candidate.networkType; - } - } - } - - /* try{ // we want to let meshcast know if our node is getting overloaded, to avoid making it worse - if ((qos!==false) && session.rpcs[UUID].settings && session.rpcs[UUID].settings.url){ - var request = new XMLHttpRequest(); - var node = session.rpcs[UUID].settings.url.split("https://")[1].split(".meshcast.io")[0]; - if (node){ - request.open('POST', " https://qos.meshcast.io/?name="+node); - request.send(qos); - } - } - } catch(e){ - errorlog(e); - } */ - - //if (session.buffer!==false){ - playoutdelay(UUID); // it will handle itself for now on I guess - //} - }); - } catch (e) { - errorlog(e); - } -} - -function safeAppendToMenu(menu, text) { - const li = document.createElement("li"); - const h2 = document.createElement("h2"); - h2.title = text; - h2.textContent = text; // Safely assigns text content, avoiding HTML parsing - li.appendChild(h2); - menu.appendChild(li); -} - -function printMyStats(menu, screenshare = false) { - // see: setupStatsMenu - - if (!session) { - return; - } - - var scrollLeft = getById("menuStatsBox").scrollLeft; - var scrollTop = getById("menuStatsBox").scrollTop; - menu.innerHTML = ""; - - try { - session.stats.outbound_connections = Object.keys(session.pcs).length; - session.stats.inbound_connections = Object.keys(session.rpcs).length; - } catch (e) { } - - try { - var obscam = false; - if (document.querySelector("select#videoSource3")) { - var videoSelect = document.querySelector("select#videoSource3").options; - if (videoSelect.length) { - if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) { - // OBS Virtualcam - obscam = true; - } else if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) { - // OBS Virtualcam - obscam = true; - } - } - } - - if (session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(function (track) { - session.currentCameraConstraints = track.getSettings(); - - if (screen && screen.orientation && screen.orientation.type) { - if (!screen.orientation.type.includes("portrait")) { - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - } else if (!window.matchMedia("(orientation: portrait)").matches) { - // legacy - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - - if (obscam && parseInt(session.currentCameraConstraints.frameRate) == 30) { - session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0); - } else { - var frameRateFPS = session.currentCameraConstraints.frameRate; - if (frameRateFPS) { - session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0) + " @ " + parseInt(frameRateFPS * 100) / 100.0 + "fps"; - } else { - session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0); - } - } - }); - } - } catch (e) { - errorlog(e); - } - - function printViewValues(obj, UUID = false) { - if (!document.getElementById("menuStatsBox")) { - return; - } - - var keys = Object.keys(obj).sort(); - - keys.forEach(key => { - if (typeof obj[key] === "object") { - try { - let sanitizedKey = sanitizeChat(key); - if (sanitizedKey === "info") { - sanitizedKey = "Remote Peer Info"; - } - safeAppendToMenu(menu, sanitizedKey); - } catch (e) { - errorlog(e); - } - - try { - printViewValues(obj[key]); - } catch (e) { - errorlog(e); - } - - menu.innerHTML += "
    "; - } - }); - - if (session.promptAccess) { - if (UUID && session.pcs[UUID]) { - menu.innerHTML += ""; - } - } - - if (UUID && session.pcs[UUID] && session.pcs[UUID].restartIce) { - // only show if available - menu.innerHTML += ""; - } - - keys.forEach(key => { - if (typeof obj[key] !== "object") { - if (key.startsWith("_")) { - return; - } - - let unit = ""; - let hint = ""; - - var stat = sanitizeChat(key); - - stat = stat.charAt(0).toUpperCase() + stat.slice(1); - - var value = obj[key]; - if (typeof value == "string") { - value = sanitizeChat(value); - } - - if (value === false) { - return; - } - - if (key == "useragent") { - value = "" + value + ""; - } - - if (key == "local_relay_IP") { - value = "" + value + ""; - } - if (key == "remote_relay_IP") { - value = "" + value + ""; - } - if (key == "watch_URL") { - value = "" + value + ""; - } - if (key == "candidateType_local" && value == "relay") { - stat = "Candidate type - Local"; - value = "💸

    relay server

    "; - } else if (key == "candidateType_remote" && value == "relay") { - stat = "Candidate type - Remote"; - value = "💸

    relay server

    "; - } else if (key == "candidateType_local" && value == "host") { - stat = "Candidate type - Local"; - value = "

    host

    "; - } else if (key == "candidateType_remote" && value == "host") { - stat = "Candidate type - Remote"; - value = "

    host

    "; - } else if (key == "candidateType_local" && value == "srflx") { - stat = "Candidate type - Local"; - value = "

    srflx

    "; - } else if (key == "candidateType_remote" && value == "srflx") { - stat = "Candidate type - Remote"; - value = "

    srflx

    "; - } else if (key == "local_ip_blocking" && value) { - value = "⚠️ You're blocking"; - hint = "no direct p2p connection made because of YOUR browser or system setting"; - } else if (key == "remote_ip_blocking" && value) { - value = "⚠️ They're blocking"; - hint = "no direct p2p connection made because of THEIR browser or system setting"; - } else if (key == "label" && value) { - value = "" + value + ""; - } - - stat = stat.replaceAll("_", " "); - - if (hint) { - menu.innerHTML += "
  • " + stat + "" + value + unit + "
  • "; - } else { - menu.innerHTML += "
  • " + stat + "" + value + unit + "
  • "; - } - } - }); - - if (UUID && session.pcs[UUID]) { - if (session.pcs[UUID].maxBandwidth) { - menu.innerHTML += "
  • max bandwidth target" + session.pcs[UUID].maxBandwidth + "
  • "; - } - if (session.pcs[UUID].setBitrate) { - menu.innerHTML += '
  • init bitrate target" + session.pcs[UUID].setBitrate + "
  • "; - } - if (session.pcs[UUID].savedBitrate) { - menu.innerHTML += "
  • current bitrate target" + session.pcs[UUID].savedBitrate + "
  • "; - } - if (session.showSlider || (!session.roomid && !session.pcs[UUID].whipout && session.meshcast !== "audio")) { - menu.innerHTML += "
  • adjust video bitrate
  • "; - - if (!session.hidehome) { - menu.innerHTML += "
    More info on setting bitrates higher here
    "; - } - } - } - } - - printViewValues(session.stats); - menu.innerHTML += ""; - - if (!screenshare && session.meshcast && session.whipOut && session.whipOut.stats) { - printViewValues({ Meshcast_connection: session.whipOut.stats }); - menu.innerHTML += "
    "; - } else if (!screenshare && session.whipOut && session.whipOut.stats) { - printViewValues({ Whip_Out_connection: session.whipOut.stats }); - menu.innerHTML += "
    "; - } - if (!screenshare && session.whepIn && session.whepIn.stats) { - printViewValues({ Whep_In_connection: session.whepIn.stats }); - menu.innerHTML += "
    "; - } - - for (var uuid in session.pcs) { - if (screenshare) { - if (session.pcs[uuid].realUUID) { - printViewValues(session.pcs[uuid].stats, uuid); - menu.innerHTML += "
    "; - } - } else if (!session.pcs[uuid].realUUID) { - printViewValues(session.pcs[uuid].stats, uuid); - menu.innerHTML += "
    "; - } - } - if (iOS || iPad) { - menu.innerHTML += "
    "; - } - try { - getById("menuStatsBox").scrollLeft = scrollLeft; - getById("menuStatsBox").scrollTop = scrollTop; - } catch (e) { } -} - -function updateLocalStats() { - if (!session) { - return; - } - - var totalBitrate = 0; - var totalBitrate2 = 0; - var cpuLimited = false; - var relayUsed = false; - var totalVideo = 0; - var totalAudio = 0; - var totalScenes = 0; - var meshcastActive = false; - var nackRate = 0; - var totalStreams = 0; - - var miscSenders = []; - - if (session.whipOut && session.whipOut.getSenders && session.whipOut.stats) { - miscSenders.push(session.whipOut); - } - - miscSenders.forEach(data => { - try { - var atot = 0; - var senders = data.getSenders(); // for any connected peer, update the video they have if connected with a video already. - senders.forEach(sender => { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track && sender.track.kind == "video" && sender.track.enabled) { - meshcastActive = true; - } else if (sender.track && sender.track.kind == "audio" && sender.track.enabled && !session.muted) { - meshcastActive = true; - } - }); - //totalAudio += atot; - - if ("video_bitrate_kbps" in data.stats) { - totalBitrate += data.stats.video_bitrate_kbps || 0; - } - if ("audio_bitrate_kbps" in data.stats) { - totalBitrate += data.stats.audio_bitrate_kbps || 0; - } - if ("total_sending_bitrate_kbps" in data.stats) { - totalBitrate2 += data.stats.total_sending_bitrate_kbps || 0; - } - - if ("quality_limitation_reason" in data.stats) { - if (data.stats.quality_limitation_reason == "cpu") { - cpuLimited = true; - } - } - - if ("nacks_per_second" in data.stats) { - nackRate += data.stats.nacks_per_second; - totalStreams += 1; - } - - setTimeout( - function (data) { - if (!data) { - return; - } - data.getStats().then(function (stats) { - if ("audio_bitrate_kbps" in data.stats) { - data.stats.audio_bitrate_kbps = 0; - } - var nominatedCandidate = false; - var candidates = {}; - var ipleakingAllowedRemote = false; - var ipleakingAllowedLocal = false; - stats.forEach(stat => { - if (stat.id && stat.id.startsWith("DEPRECATED_")) { - return; - } - if (stat.type == "transport") { - if ("bytesSent" in stat) { - if ("_bytesSent" in data.stats) { - if (data.stats._timestamp) { - if (stat.timestamp) { - data.stats.total_sending_bitrate_kbps = parseInt((8 * (stat.bytesSent - data.stats._bytesSent)) / (stat.timestamp - data.stats._timestamp)); - } - } - } - data.stats._bytesSent = stat.bytesSent; - } - if ("timestamp" in stat) { - data.stats._timestamp = stat.timestamp; - } - } else if (stat.type == "outbound-rtp") { - if (stat.kind == "video") { - if ("framesPerSecond" in stat) { - data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond; - } else if ("frameHeight" in stat) { - if ("framesEncoded" in stat && stat.timestamp) { - var lastFramesEncoded = 0; - var lastTimestamp = 0; - try { - lastFramesEncoded = data.stats._framesEncoded; - lastTimestamp = data.stats._timestamp; - } catch (e) { } - data.stats._FPS = parseInt((10 * (stat.framesEncoded - lastFramesEncoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10 || "?"; - data.stats._framesEncoded = stat.framesEncoded; - data.stats._timestamp = stat.timestamp / 1000; - data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + data.stats._FPS; - } else { - data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight; - } - } - if ("encoderImplementation" in stat) { - data.stats.video_encoder = stat.encoderImplementation; - if (stat.encoderImplementation == "ExternalEncoder") { - data.stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly - data.encoder = true; - } else if (stat.encoderImplementation == "MediaFoundationVideoEncodeAccelerator") { - data.stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly - data.encoder = true; - } else { - data.encoder = false; // this may not be actually accurate, but lets assume so. - } - } - if ("qualityLimitationReason" in stat) { - if (data.stats.quality_limitation_reason) { - if (data.stats.quality_limitation_reason !== stat.qualityLimitationReason) { - try { - var miniInfo = {}; - miniInfo.qlr = stat.qualityLimitationReason; - if ("_hardwareEncoder" in data.stats) { - miniInfo.hw_enc = data.stats._hardwareEncoder; - } else { - miniInfo.hw_enc = null; - } - session.sendMessage({ miniInfo: miniInfo }); - } catch (e) { - warnlog(e); - } - } - } - data.stats.quality_limitation_reason = stat.qualityLimitationReason; - } - if ("bytesSent" in stat) { - if ("_bytesSentVideo" in data.stats) { - if (data.stats._timestamp1) { - data.stats.video_bitrate_kbps = parseInt((8 * (stat.bytesSent - data.stats._bytesSentVideo)) / (stat.timestamp - data.stats._timestamp1)); - if (stat.timestamp) { - } - } - } - data.stats._bytesSentVideo = stat.bytesSent; - } - if ("nackCount" in stat) { - if ("_nackCount" in data.stats) { - if (data.stats._timestamp1) { - if (stat.timestamp) { - data.stats.nacks_per_second = parseInt((10000 * (stat.nackCount - data.stats._nackCount)) / (stat.timestamp - data.stats._timestamp1)) / 10; - } - } - } - } - if ("retransmittedBytesSent" in stat) { - if ("_retransmittedBytesSent" in data.stats) { - if (data.stats._timestamp1) { - if (stat.timestamp) { - data.stats.retransmitted_kbps = parseInt((8 * (stat.retransmittedBytesSent - data.stats._retransmittedBytesSent)) / (stat.timestamp - data.stats._timestamp1)); - } - } - } - } - if ("nackCount" in stat) { - data.stats._nackCount = stat.nackCount; - } - if ("retransmittedBytesSent" in stat) { - data.stats._retransmittedBytesSent = stat.retransmittedBytesSent; - } - if ("timestamp" in stat) { - data.stats._timestamp1 = stat.timestamp; - } - if ("pliCount" in stat) { - data.stats.total_pli_count = stat.pliCount; - } - if ("keyFramesEncoded" in stat) { - data.stats.total_key_frames_encoded = stat.keyFramesEncoded; - } - } else if (stat.kind == "audio") { - if ("bytesSent" in stat) { - if (data.stats._bytesSentAudio) { - if (data.stats._timestamp2) { - if (stat.timestamp) { - if ("audio_bitrate_kbps" in data.stats) { - data.stats.audio_bitrate_kbps += parseInt((8 * (stat.bytesSent - data.stats._bytesSentAudio)) / (stat.timestamp - data.stats._timestamp2)); - } else { - data.stats.audio_bitrate_kbps = 0; - } - } - } - } - } - if ("timestamp" in stat) { - data.stats._timestamp2 = stat.timestamp; - } - if ("bytesSent" in stat) { - data.stats._bytesSentAudio = stat.bytesSent; - } - } - } else if (stat.type == "remote-candidate") { - candidates[stat.id] = stat; - if (stat.candidateType != "relay") { - ipleakingAllowedRemote = true; - } - } else if (stat.type == "local-candidate") { - candidates[stat.id] = stat; - if (stat.candidateType != "relay") { - ipleakingAllowedLocal = true; - } - } else if (stat.type == "candidate-pair" && stat.nominated) { - if (!nominatedCandidate) { - nominatedCandidate = stat; - } else if (nominatedCandidate.priority < stat.priority) { - nominatedCandidate = stat; - } - } else if ("mimeType" in stat && "type" in stat && stat.type == "codec") { - if (stat.mimeType.includes("video")) { - data.stats.video_codec = stat.mimeType.split("video/")[1]; - } else if (stat.mimeType.includes("audio")) { - data.stats.audio_codec = stat.mimeType.split("audio/")[1]; - if (stat.clockRate) { - data.stats.audio_clockRate = stat.clockRate; - if (stat.channels) { - data.stats.audio_clockRate += " / " + stat.channels; - } - } else if (stat.sdpFmtpLine) { - data.stats.fmtp = stat.sdpFmtpLine; - } - } - } - return; - }); - if (nominatedCandidate) { - if ("availableOutgoingBitrate" in nominatedCandidate) { - data.stats.available_outgoing_bitrate_kbps = parseInt(nominatedCandidate.availableOutgoingBitrate / 1024); - if (session.maxBandwidth !== false) { - session.limitMaxBandwidth(data.stats.available_outgoing_bitrate_kbps, session.pcs[UUID], false); - } - } - if ("totalRoundTripTime" in nominatedCandidate) { - if ("responsesReceived" in nominatedCandidate) { - data.stats.average_roundTripTime_ms = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000); - } - } - } - if (nominatedCandidate && nominatedCandidate.remoteCandidateId) { - if (candidates[nominatedCandidate.remoteCandidateId]) { - var candidate = candidates[nominatedCandidate.remoteCandidateId]; - if ("candidateType" in candidate) { - data.stats.candidateType_remote = candidate.candidateType; - if (candidate.candidateType === "relay") { - if ("ip" in candidate) { - data.stats.remote_relay_IP = candidate.ip; - } - if ("relayProtocol" in candidate) { - data.stats.remote_relay_protocol = candidate.relayProtocol; - } - data.stats.remote_ip_blocking = !ipleakingAllowedRemote; - } else { - try { - delete data.stats.remote_relay_IP; - delete data.stats.remote_relay_protocol; - delete data.stats.remote_ip_blocking; - } catch (e) { } - } - } - } - } - if (nominatedCandidate && nominatedCandidate.localCandidateId) { - if (candidates[nominatedCandidate.localCandidateId]) { - var candidate = candidates[nominatedCandidate.localCandidateId]; - if ("candidateType" in candidate) { - data.stats.candidateType_local = candidate.candidateType; - if (candidate.candidateType === "relay") { - if ("ip" in candidate) { - data.stats.local_relay_IP = candidate.ip; - } - if ("relayProtocol" in candidate) { - data.stats.local_relay_protocol = candidate.relayProtocol; - } - data.stats.local_ip_blocking = !ipleakingAllowedLocal; - } else { - try { - delete data.stats.local_relay_IP; - delete data.stats.local_relay_protocol; - delete data.stats.local_ip_blocking; - } catch (e) { } - } - } - } - } - return; - }); - }, - 0, - data - ); - } catch (e) { - errorlog(e); - } - }); - - for (var uuid in session.pcs) { - if (!session.pcs[uuid].stats) { - continue; - } - - var atot = 0; - var senders = getSenders2(uuid); // for any connected peer, update the video they have if connected with a video already. - senders.forEach(sender => { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track && sender.track.kind == "video" && sender.track.enabled) { - totalVideo += 1; - } else if (sender.track && sender.track.kind == "audio" && sender.track.enabled && !session.muted) { - atot = 1; - } - }); - totalAudio += atot; - - if ("scene" in session.pcs[uuid]) { - if (session.pcs[uuid].scene !== false) { - totalScenes += 1; - } - } - - if ("video_bitrate_kbps" in session.pcs[uuid].stats) { - totalBitrate += session.pcs[uuid].stats.video_bitrate_kbps || 0; - } - if ("audio_bitrate_kbps" in session.pcs[uuid].stats) { - totalBitrate += session.pcs[uuid].stats.audio_bitrate_kbps || 0; - } - if ("total_sending_bitrate_kbps" in session.pcs[uuid].stats) { - totalBitrate2 += session.pcs[uuid].stats.total_sending_bitrate_kbps || 0; - } - - if ("quality_limitation_reason" in session.pcs[uuid].stats) { - if (session.pcs[uuid].stats.quality_limitation_reason == "cpu") { - cpuLimited = true; - } - } - - if ("nacks_per_second" in session.pcs[uuid].stats) { - nackRate += session.pcs[uuid].stats.nacks_per_second; - totalStreams += 1; - } - - if (uuid in session.rpcs) { - if (session.pcs[uuid].stats.label) { - session.pcs[uuid].stats.label = session.rpcs[uuid].label; - } - if (session.pcs[uuid].stats.streamID) { - session.pcs[uuid].stats.streamID = session.rpcs[uuid].streamID; - } - } - - var screenTracksIds = []; - if (session.screenStream !== false) { - // null if already used. false if never used. - session.screenStream.getTracks().forEach(trk => { - screenTracksIds.push(trk.id); - }); - } - - setTimeout( - function (UUID) { - if (!session.pcs[UUID]) { - return; - } - - if (session.pcs[UUID].realUUID && session.pcs[session.pcs[UUID].realUUID]) { - var thisIsAlt = true; - var node = session.pcs[session.pcs[UUID].realUUID]; - } else { - var thisIsAlt = false; - var node = session.pcs[UUID]; - } - - node.getStats().then(function (stats) { - if (!(UUID in session.pcs)) { - return; - } - - if ("audio_bitrate_kbps" in session.pcs[UUID].stats) { - session.pcs[UUID].stats.audio_bitrate_kbps = 0; - } - - var nominatedCandidate = false; - var candidates = {}; - var ipleakingAllowedRemote = false; - var ipleakingAllowedLocal = false; - - var statObject = []; - var altStreamList = {}; - stats.forEach(stat => { - statObject.push(stat); - if (screenTracksIds.includes(stat.trackIdentifier)) { - altStreamList[stat.id] = stat.trackIdentifier; - } - }); - - statObject.forEach(stat => { - if (stat.id && stat.id.startsWith("DEPRECATED_")) { - return; - } - if (stat.type == "transport") { - if ("bytesSent" in stat) { - if ("_bytesSent" in session.pcs[UUID].stats) { - if (session.pcs[UUID].stats._timestamp3) { - if (stat.timestamp) { - session.pcs[UUID].stats.total_sending_bitrate_kbps = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSent)) / (stat.timestamp - session.pcs[UUID].stats._timestamp3)); - } - } - } - session.pcs[UUID].stats._bytesSent = stat.bytesSent; - } - if ("timestamp" in stat) { - session.pcs[UUID].stats._timestamp3 = stat.timestamp; - } - } else if (stat.type == "outbound-rtp") { - if (thisIsAlt && stat.mediaSourceId && !altStreamList[stat.mediaSourceId]) { - // this isn't an alt stream, but we are in alt mode - return; - } else if (!thisIsAlt && stat.mediaSourceId && altStreamList[stat.mediaSourceId]) { - // this is an alt stream, but we are not in alt mode - return; - } - if (stat.kind == "video") { - if ("framesPerSecond" in stat) { - session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond; - } else if ("frameHeight" in stat) { - if ("framesEncoded" in stat && stat.timestamp) { - var lastFramesEncoded = 0; - var lastTimestamp = 0; - try { - lastFramesEncoded = session.pcs[UUID].stats._framesEncoded; - lastTimestamp = session.pcs[UUID].stats._timestamp; - } catch (e) { } - session.pcs[UUID].stats._FPS = parseInt((10 * (stat.framesEncoded - lastFramesEncoded)) / (stat.timestamp - lastTimestamp)) / 10; - session.pcs[UUID].stats._framesEncoded = stat.framesEncoded; - session.pcs[UUID].stats._timestamp = stat.timestamp; - session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + session.pcs[UUID].stats._FPS; - } else { - session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight; - } - } - var miniInfo = {}; - var sendMini = false; - if ("encoderImplementation" in stat) { - session.pcs[UUID].stats.video_encoder = stat.encoderImplementation; - if (stat.encoderImplementation == "ExternalEncoder") { - session.pcs[UUID].stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly - if (session.pcs[UUID].encoder !== true) { - session.pcs[UUID].encoder = true; - miniInfo.hw_enc = true; - sendMini = true; - } - } else if (stat.encoderImplementation == "MediaFoundationVideoEncodeAccelerator") { - session.pcs[UUID].stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly - if (session.pcs[UUID].encoder !== true) { - session.pcs[UUID].encoder = true; - miniInfo.hw_enc = true; - sendMini = true; - } - } else { - if (session.pcs[UUID].encoder === true) { - session.pcs[UUID].encoder = false; - miniInfo.hw_enc = false; - sendMini = true; - } - } - } - if ("qualityLimitationReason" in stat) { - if (session.pcs[UUID].stats.quality_limitation_reason) { - if (session.pcs[UUID].stats.quality_limitation_reason !== stat.qualityLimitationReason) { - try { - sendMini = true; - miniInfo.qlr = stat.qualityLimitationReason; - } catch (e) { - warnlog(e); - } - } - } - session.pcs[UUID].stats.quality_limitation_reason = stat.qualityLimitationReason; - } - if (sendMini) { - session.sendMessage({ miniInfo: miniInfo }, UUID); - } - if ("bytesSent" in stat) { - if ("_bytesSentVideo" in session.pcs[UUID].stats) { - if (session.pcs[UUID].stats._timestamp1) { - if (stat.timestamp) { - session.pcs[UUID].stats.video_bitrate_kbps = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSentVideo)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1)); - } - } - } - session.pcs[UUID].stats._bytesSentVideo = stat.bytesSent; - } - if ("nackCount" in stat) { - if ("_nackCount" in session.pcs[UUID].stats) { - if (session.pcs[UUID].stats._timestamp1) { - if (stat.timestamp) { - session.pcs[UUID].stats.nacks_per_second = parseInt((10000 * (stat.nackCount - session.pcs[UUID].stats._nackCount)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1)) / 10; - } - } - } - } - if ("retransmittedBytesSent" in stat) { - if ("_retransmittedBytesSent" in session.pcs[UUID].stats) { - if (session.pcs[UUID].stats._timestamp1) { - if (stat.timestamp) { - session.pcs[UUID].stats.retransmitted_kbps = parseInt((8 * (stat.retransmittedBytesSent - session.pcs[UUID].stats._retransmittedBytesSent)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1)); - } - } - } - } - if ("nackCount" in stat) { - session.pcs[UUID].stats._nackCount = stat.nackCount; - } - if ("retransmittedBytesSent" in stat) { - session.pcs[UUID].stats._retransmittedBytesSent = stat.retransmittedBytesSent; - } - if ("timestamp" in stat) { - session.pcs[UUID].stats._timestamp1 = stat.timestamp; - } - if ("pliCount" in stat) { - session.pcs[UUID].stats.total_pli_count = stat.pliCount; - } - if ("keyFramesEncoded" in stat) { - session.pcs[UUID].stats.total_key_frames_encoded = stat.keyFramesEncoded; - } - } else if (stat.kind == "audio") { - if ("bytesSent" in stat) { - if (session.pcs[UUID].stats._bytesSentAudio) { - if (session.pcs[UUID].stats._timestamp2) { - if (stat.timestamp) { - if ("audio_bitrate_kbps" in session.pcs[UUID].stats) { - session.pcs[UUID].stats.audio_bitrate_kbps += parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSentAudio)) / (stat.timestamp - session.pcs[UUID].stats._timestamp2)); - } else { - session.pcs[UUID].stats.audio_bitrate_kbps = 0; - } - } - } - } - } - if ("timestamp" in stat) { - session.pcs[UUID].stats._timestamp2 = stat.timestamp; - } - if ("bytesSent" in stat) { - session.pcs[UUID].stats._bytesSentAudio = stat.bytesSent; - } - } - } else if (stat.type == "remote-candidate") { - candidates[stat.id] = stat; - if (stat.candidateType != "relay") { - ipleakingAllowedRemote = true; - } - } else if (stat.type == "local-candidate") { - candidates[stat.id] = stat; - if (stat.candidateType != "relay") { - ipleakingAllowedLocal = true; - } - } else if (stat.type == "candidate-pair" && stat.nominated) { - if (!nominatedCandidate) { - nominatedCandidate = stat; - } else if (nominatedCandidate.priority < stat.priority) { - nominatedCandidate = stat; - } - } else if ("mimeType" in stat && "type" in stat && stat.type == "codec") { - if (stat.mimeType.includes("video")) { - session.pcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1]; - } else if (stat.mimeType.includes("audio")) { - session.pcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1]; - if (stat.clockRate) { - session.pcs[UUID].stats.audio_clockRate = stat.clockRate; - if (stat.channels) { - session.pcs[UUID].stats.audio_clockRate += " / " + stat.channels; - } - } else if (stat.sdpFmtpLine) { - session.pcs[UUID].stats.fmtp = stat.sdpFmtpLine; - } - } - } - return; - }); - - if (nominatedCandidate) { - if ("availableOutgoingBitrate" in nominatedCandidate) { - session.pcs[UUID].stats.available_outgoing_bitrate_kbps = parseInt(nominatedCandidate.availableOutgoingBitrate / 1024); - if (session.maxBandwidth !== false) { - session.limitMaxBandwidth(session.pcs[UUID].stats.available_outgoing_bitrate_kbps, session.pcs[UUID], false); - } - } - if ("totalRoundTripTime" in nominatedCandidate) { - if ("responsesReceived" in nominatedCandidate) { - session.pcs[UUID].stats.average_roundTripTime_ms = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000); - } - } - } - if (nominatedCandidate && nominatedCandidate.remoteCandidateId) { - if (candidates[nominatedCandidate.remoteCandidateId]) { - var candidate = candidates[nominatedCandidate.remoteCandidateId]; - if ("candidateType" in candidate) { - session.pcs[UUID].stats.candidateType_remote = candidate.candidateType; - if (candidate.candidateType === "relay") { - if ("ip" in candidate) { - session.pcs[UUID].stats.remote_relay_IP = candidate.ip; - } - if ("relayProtocol" in candidate) { - session.pcs[UUID].stats.remote_relay_protocol = candidate.relayProtocol; - } - session.pcs[UUID].stats.remote_ip_blocking = !ipleakingAllowedRemote; - } else { - try { - delete session.pcs[UUID].stats.remote_relay_IP; - delete session.pcs[UUID].stats.remote_relay_protocol; - delete session.pcs[UUID].stats.remote_ip_blocking; - } catch (e) { } - } - } - } - } - if (nominatedCandidate && nominatedCandidate.localCandidateId) { - if (candidates[nominatedCandidate.localCandidateId]) { - var candidate = candidates[nominatedCandidate.localCandidateId]; - if ("candidateType" in candidate) { - session.pcs[UUID].stats.candidateType_local = candidate.candidateType; - if (candidate.candidateType === "relay") { - if ("ip" in candidate) { - session.pcs[UUID].stats.local_relay_IP = candidate.ip; - } - if ("relayProtocol" in candidate) { - session.pcs[UUID].stats.local_relay_protocol = candidate.relayProtocol; - } - session.pcs[UUID].stats.local_ip_blocking = !ipleakingAllowedLocal; - } else { - try { - delete session.pcs[UUID].stats.local_relay_IP; - delete session.pcs[UUID].stats.local_relay_protocol; - delete session.pcs[UUID].stats.local_ip_blocking; - } catch (e) { } - } - } - } - } - - return; - }); - }, - 0, - uuid - ); - } - - try { - var totalCon = Object.keys(session.pcs).length || 0; - var headerStats = "🔗 "; - headerStats += totalCon; - if (meshcastActive) { - if (totalAudio) { - headerStats += ", 👂 " + totalAudio; - } - if (totalVideo) { - headerStats += ", 👀 " + totalVideo; - } - headerStats += ", 📡Broadcast"; - } else { - headerStats += ", 👂 " + totalAudio; - headerStats += ", 👀 " + totalVideo; - } - if (session.roomid) { - headerStats += ", 🎬 " + totalScenes + ""; - } - - var changed = false; - if (!session.info.out) { - session.info.out = {}; - session.info.out.v = totalVideo; - session.info.out.a = totalAudio; - session.info.out.c = totalCon; - session.info.out.s = totalScenes; - changed = true; - } else { - if (session.info.out.a !== totalAudio) { - session.info.out.a = totalAudio; - // changed = true; // I'm not sending this data, so why bother - } - if (session.info.out.v !== totalVideo) { - session.info.out.v = totalAudio; - //changed = true; // I'm not sending this data, so why bother - } - if (session.info.out.c !== totalCon) { - if (session.info.out.c) { - changed = true; // update if I'm not the first one - } - session.info.out.c = totalCon; - } - if (session.info.out.s !== totalScenes) { - if (session.info.out.s) { - changed = true; // update if I'm not the first one - } - session.info.out.s = totalScenes; - } - } - } catch (e) { } - //session.info.out = {}; - - var uploadQuality = nackRate / totalStreams || 0; - if (totalStreams === 0) { - uploadQuality = "title='Connection seems good' style='color: transparent; text-shadow: 0 0 0 #4d9bff;'"; - } else if (uploadQuality === 0) { - uploadQuality = "title='Connection seems good' style='color: transparent; text-shadow: 0 0 0 #0F0;'"; - } else if (uploadQuality <= 1) { - uploadQuality = "title='Mild connection issues' style='color: transparent; text-shadow: 0 0 0 yellow;'"; - } else if (uploadQuality <= 5) { - uploadQuality = "title='Moderate connection issues' style='color: transparent; text-shadow: 0 0 0 orange;'"; - } else { - uploadQuality = "title='Severe connection issues' style='color: transparent; text-shadow: 0 0 0 #F00;'"; - } - - if (Firefox && totalBitrate === 0 && totalBitrate2 === 0) { - // does not support the current stats system - } else if (totalBitrate > totalBitrate2) { - headerStats += ", 🔺 " + Math.round(totalBitrate / 10.24) / 100 + "-Mbps"; - } else if (totalBitrate2 > 1000) { - headerStats += ", 🔺 " + Math.round(totalBitrate2 / 10.24) / 100 + "-Mbps"; - } else { - headerStats += ", 🔺 " + totalBitrate2 + "-kbps"; - } - - if (session.director || !session.roomid) { - // show stats if the director or if not in a group room - if (cpuLimited) { - headerStats += ", 🔥 CPU Overloaded"; - } - //if (relayUsed){ - // headerStats += " 💸"; - //} - - directorGraphStats(); - } - - var miniInfo = {}; - if (changed) { - miniInfo.out = {}; - miniInfo.out.c = session.info.out.c; - } - - if (session.cpuLimited !== cpuLimited) { - session.cpuLimited = cpuLimited; - miniInfo.cpu = cpuLimited; - changed = true; - } - - if (changed) { - for (var uuid in session.pcs) { - session.sendMessage({ miniInfo: miniInfo }, uuid); // lets send it to everyone. - } - } - - if (document.getElementById("head5")) { - try { - if (Object.keys(session.pcs).length || meshcastActive) { - document.getElementById("head5").classList.remove("hidden"); - } - } catch (e) { } - document.getElementById("head5").innerHTML = headerStats; - //miniTranslate(document.getElementById("head5")); - document.getElementById("head5").onclick = function () { - var [menu, innerMenu] = statsMenuCreator(); - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - printMyStats(innerMenu); - }; - } -} - -function updateStats(obsvc = false) { - if (document.getElementById("previewWebcam")) { - var ele = document.getElementById("previewWebcam"); - var wcs = "webcamstats"; - } else if (document.getElementById("videosource")) { - var ele = document.getElementById("videosource"); - var wcs = "webcamstats3"; - } else { - return; - } - - try { - getById(wcs).innerHTML = ""; - ele.srcObject.getVideoTracks().forEach(function (track) { - if (obsvc && parseInt(track.getSettings().frameRate) == 30) { - getById(wcs).innerHTML = "Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + " @ up to 60fps"; - } else { - var frameRateFPS = track.getSettings().frameRate; - if (frameRateFPS) { - getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + "@" + parseInt(frameRateFPS * 100) / 100.0 + "fps"; - } else { - getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0); - } - } - }); - } catch (e) { - errorlog(e); - } -} - -function toggleControlBar() { - if (!getById("controlButtons").classList.contains("hidden")) { - getById("controlButtons").dataset.enabled = true; - getById("controlButtons").classList.add("hidden"); - } else if (getById("controlButtons").dataset.enabled) { - getById("controlButtons").classList.remove("hidden"); - delete getById("controlButtons").dataset.enabled; - } -} - -function toggleMute(apply = false, event = false) { - // TODO: I need to have this be MUTE, toggle, with volume not touched. - - var mouseUp = null; - var touchEnd = null; - var timeStart = Date.now(); - if (event) { - mouseUp = document.onmouseup; - touchEnd = document.ontouchend; - document.onmouseup = function () { - document.onmouseup = mouseUp; - document.ontouchend = touchEnd; - if (Date.now() - timeStart < 500) { - return; - } else { - toggleMute(); - } - }; - document.ontouchend = function () { - document.onmouseup = mouseUp; - document.ontouchend = touchEnd; - if (Date.now() - timeStart < 300) { - return; - } else { - toggleMute(); - } - }; - } - - if (session.director) { - if (!session.directorEnabledPPT) { - log("Director doesn't have PPT enabled yet"); - // director has not enabled PTT yet. - return; - } - } - - if (apply) { - session.muted = !session.muted; // we flip here as we are going to flip again in a second. - } - //try{var ptt = getById("press2talk");} catch(e){var ptt=false;} - - if (session.muted == false) { - session.muted = true; - if (session.pendingMicRefreshTimeout) { - clearTimeout(session.pendingMicRefreshTimeout); - session.pendingMicRefreshTimeout = null; - } - getById("mutetoggle").className = "las la-microphone-slash toggleSize"; - if (!session.cleanOutput) { - getById("mutebutton").classList.add("red", "pulsate"); - getById("mutebutton").ariaPressed = "true"; - getById("header").classList.add("red"); - - if (session.localMuteElement) { - session.localMuteElement.style.display = "block"; - } - } - if (session.streamSrc) { - session.streamSrc.getAudioTracks().forEach(track => { - track.enabled = false; - }); - } - if ((window.obsstudio || session.mobile) && session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getAudioTracks().forEach(track => { - track.enabled = false; - }); - } - } else { - session.muted = false; - getById("mutetoggle").className = "las la-microphone toggleSize"; - if (!session.cleanOutput) { - getById("mutebutton").classList.remove("red", "pulsate"); - getById("mutebutton").ariaPressed = "false"; - getById("header").classList.remove("red"); - - if (session.localMuteElement) { - session.localMuteElement.style.display = "none"; - } - } - if (session.streamSrc) { - session.streamSrc.getAudioTracks().forEach(track => { - track.enabled = true; - }); - } - //if (session.mobile) { - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getAudioTracks().forEach(track => { - track.enabled = true; - }); - } - if (!apply && event && (iOS || iPad || SafariVersion)) { - if (session.pendingMicRefreshTimeout) { - clearTimeout(session.pendingMicRefreshTimeout); - } - session.pendingMicRefreshTimeout = setTimeout(() => { - session.pendingMicRefreshTimeout = null; - if (!session.muted) { - refreshMicrophoneDevice(); // refresh mic only if still unmuted - } - }, 150); - } - //} - - // toggleMute(false, event) - - //if (ptt){ - // ptt.innerHTML = "🔴 Push to Mute"; - //} - } - - try { - postMessageIframe(document.getElementById("screensharesource"), { mic: !session.muted }); - } catch (e) { } - - if (!apply) { - // only if they are changing states do we bother to spam. - var data = {}; - data.muteState = session.muted; - session.sendMessage(data); - log("SEND MUTE STATE TO PEERS"); - pokeIframeAPI("mic-mute-state", session.muted); - pokeAPI("muted", session.muted); - } -} - -function postMessageIframe(iFrameEle, message) { - // iframes seem to only have the contentWindow work on the last placed iframe object, so this checks the dom first. - if (iFrameEle && iFrameEle.nodeName == "IFRAME") { - try { - if (iFrameEle.id && document.getElementById(iFrameEle.id)) { - document.getElementById(iFrameEle.id).contentWindow.postMessage(message, "*"); - } else { - iFrameEle.contentWindow.postMessage(message, "*"); - } - } catch (e) { - errorlog(e); - } - } -} - -function toggleSpeakerMute(apply = false) { - if (session.ignoreNextSpeakerToggle) { - session.ignoreNextSpeakerToggle = false; - return; - } - - closeSpeakerVolumePanel(); - - // TODO: I need to have this be MUTE, toggle, with volume not touched. - - if (CtrlPressed) { - resetupAudioOut(); - } - - if (apply) { - session.speakerMuted = !session.speakerMuted; - } - if (session.speakerMuted == false) { - // mute output - session.speakerMuted = true; - getById("mutespeakertoggle").className = "las la-volume-mute toggleSize"; - if (!session.cleanOutput) { - getById("mutespeakerbutton").className = "float red"; - } - var sounds = document.getElementsByTagName("video"); - - if (iOS || iPad) { - for (var i = 0; i < sounds.length; ++i) { - if (sounds[i].id === "keepAlivePlayer") { - // we need to keep this unmuted - continue; - } - sounds[i].muted = !sounds[i].muted; - sounds[i].muted = session.speakerMuted; - } - } else { - for (var i = 0; i < sounds.length; ++i) { - if (sounds[i].id === "keepAlivePlayer") { - // we need to keep this unmuted - continue; - } - sounds[i].muted = session.speakerMuted; - } - } - } else { - session.speakerMuted = false; // unmute output - - getById("mutespeakertoggle").className = "las la-volume-up toggleSize"; - if (!session.cleanOutput) { - getById("mutespeakerbutton").className = "float"; - } - var sounds = document.getElementsByTagName("video"); - - if (iOS || iPad) { - // attempting to fix an iOS bug - for (var i = 0; i < sounds.length; ++i) { - sounds[i].muted = !sounds[i].muted; - if (sounds[i].id === "videosource") { - // don't unmute ourselves. feedback galore if so. - sounds[i].muted = true; - continue; - } else if (sounds[i].id === "previewWebcam") { - sounds[i].muted = true; - continue; - } else if (sounds[i].id === "screensharesource") { - sounds[i].muted = true; - continue; - } else if (sounds[i].id === "screenshare") { - // this is a webcam - sounds[i].muted = true; - continue; - } else { - sounds[i].muted = session.speakerMuted; - } - } - } else { - for (var i = 0; i < sounds.length; ++i) { - if (sounds[i].id === "videosource") { - // don't unmute ourselves. feedback galore if so. - continue; - } else if (sounds[i].id === "screensharesource") { - // don't unmute ourselves. feedback galore if so. - continue; - } else if (sounds[i].id === "previewWebcam") { - continue; - } else if (sounds[i].id === "screenshare") { - // this is a webm - continue; - } else { - sounds[i].muted = session.speakerMuted; - } - } - } - } - - for (var UUID in session.rpcs) { - applyMuteState(UUID); - postMessageIframe(session.rpcs[UUID].iframeEle, { mute: session.speakerMuted }); - } - - pokeIframeAPI("audio-mute-state", session.speakerMuted); - - if (!apply) { - pokeAPI("speakerMuted", session.speakerMuted); - } - - if (iOS || iPad) { - resetupAudioOut(); - } -} - -const SPEAKER_VOLUME_HOLD_DELAY = 400; -const SPEAKER_VOLUME_MIN_PERCENT = 1; -const SPEAKER_VOLUME_MAX_PERCENT = 100; -var speakerVolumeButton = null; -var speakerVolumePanelElement = null; -var speakerVolumeSliderElement = null; -var speakerVolumeValueElement = null; -var speakerVolumeHoldTimer = null; -var speakerVolumePanelVisible = false; -var speakerVolumeDocumentListenersActive = false; - -function clampSpeakerVolumePercent(percent) { - percent = parseInt(percent, 10); - if (isNaN(percent)) { - percent = SPEAKER_VOLUME_MAX_PERCENT; - } - if (percent < SPEAKER_VOLUME_MIN_PERCENT) { - percent = SPEAKER_VOLUME_MIN_PERCENT; - } - if (percent > SPEAKER_VOLUME_MAX_PERCENT) { - percent = SPEAKER_VOLUME_MAX_PERCENT; - } - return percent; -} - -function getCurrentSpeakerVolumePercent() { - if (typeof session.volume === "number" && !isNaN(session.volume)) { - return clampSpeakerVolumePercent(Math.round(session.volume * 100)); - } - return SPEAKER_VOLUME_MAX_PERCENT; -} - -function convertSpeakerPercentToVolume(percent) { - return clampSpeakerVolumePercent(percent) / 100; -} - -function updateSpeakerVolumeSliderUI(volume) { - if (!speakerVolumeSliderElement) { - return; - } - var effectiveVolume = typeof volume === "number" && !isNaN(volume) ? volume : 1; - if (effectiveVolume < SPEAKER_VOLUME_MIN_PERCENT / 100) { - effectiveVolume = SPEAKER_VOLUME_MIN_PERCENT / 100; - } - if (effectiveVolume > 1) { - effectiveVolume = 1; - } - var percent = clampSpeakerVolumePercent(Math.round(effectiveVolume * 100)); - speakerVolumeSliderElement.value = percent; - if (speakerVolumeValueElement) { - speakerVolumeValueElement.textContent = percent + "%"; - } -} - -function setSessionPlaybackVolume(volume, target) { - if (typeof volume !== "number" || isNaN(volume)) { - return; - } - if (volume > 1) { - volume = 1; - } - if (volume < 0) { - volume = 0; - } - - session.volume = volume; - - var applyToAll = !target || target === "*" || typeof target === "undefined"; - - if (applyToAll) { - if (session.videoElement && typeof session.videoElement.volume === "number") { - try { - session.videoElement.volume = volume; - } catch (e) { - errorlog(e); - } - } - try { - var mediaElements = document.querySelectorAll("video, audio"); - for (var i = 0; i < mediaElements.length; i++) { - var media = mediaElements[i]; - if (!media || typeof media.volume !== "number") { - continue; - } - if (media.dataset && media.dataset.keepVolume === "1") { - continue; - } - media.volume = volume; - } - } catch (e) { - errorlog(e); - } - if (session.screenShareElement && typeof session.screenShareElement.volume === "number") { - try { - session.screenShareElement.volume = volume; - } catch (e) { - errorlog(e); - } - } - } - - for (var UUID in session.rpcs) { - if (!Object.prototype.hasOwnProperty.call(session.rpcs, UUID)) { - continue; - } - try { - var peer = session.rpcs[UUID]; - if (!peer || !peer.videoElement) { - continue; - } - if (!applyToAll && target && target !== "*" && peer.streamID && peer.streamID !== target) { - continue; - } - peer.videoElement.volume = volume; - } catch (e) { - errorlog(e); - } - } - - updateSpeakerVolumeSliderUI(volume); -} - -function handleSpeakerVolumeSliderInput(event) { - if (!event || !event.target) { - return; - } - var percent = clampSpeakerVolumePercent(event.target.value); - var volume = convertSpeakerPercentToVolume(percent); - setSessionPlaybackVolume(volume, "*"); -} - -function clearSpeakerVolumeHoldTimer() { - if (speakerVolumeHoldTimer) { - clearTimeout(speakerVolumeHoldTimer); - speakerVolumeHoldTimer = null; - } -} - -function openSpeakerVolumePanel() { - clearSpeakerVolumeHoldTimer(); - if (!speakerVolumePanelElement || speakerVolumePanelVisible) { - return; - } - - updateSpeakerVolumeSliderUI(typeof session.volume === "number" ? session.volume : 1); - - speakerVolumePanelElement.classList.remove("hidden"); - speakerVolumePanelElement.setAttribute("aria-hidden", "false"); - speakerVolumePanelVisible = true; - - if (speakerVolumeSliderElement) { - try { - speakerVolumeSliderElement.focus({ preventScroll: true }); - } catch (e) { - try { - speakerVolumeSliderElement.focus(); - } catch (err) { } - } - } - - if (!speakerVolumeDocumentListenersActive) { - document.addEventListener("pointerdown", handleSpeakerVolumeDocumentPointerDown, true); - document.addEventListener("keydown", handleSpeakerVolumeKeydown, true); - speakerVolumeDocumentListenersActive = true; - } -} - -function closeSpeakerVolumePanel(options) { - clearSpeakerVolumeHoldTimer(); - - if (!speakerVolumePanelElement || !speakerVolumePanelVisible) { - if (speakerVolumePanelElement) { - speakerVolumePanelElement.setAttribute("aria-hidden", "true"); - } - if (speakerVolumeDocumentListenersActive) { - document.removeEventListener("pointerdown", handleSpeakerVolumeDocumentPointerDown, true); - document.removeEventListener("keydown", handleSpeakerVolumeKeydown, true); - speakerVolumeDocumentListenersActive = false; - } - if (!options || options.preserveToggleGuard !== true) { - session.ignoreNextSpeakerToggle = false; - } - speakerVolumePanelVisible = false; - return; - } - - speakerVolumePanelElement.classList.add("hidden"); - speakerVolumePanelElement.setAttribute("aria-hidden", "true"); - speakerVolumePanelVisible = false; - - if (speakerVolumeDocumentListenersActive) { - document.removeEventListener("pointerdown", handleSpeakerVolumeDocumentPointerDown, true); - document.removeEventListener("keydown", handleSpeakerVolumeKeydown, true); - speakerVolumeDocumentListenersActive = false; - } - - if (!options || options.preserveToggleGuard !== true) { - session.ignoreNextSpeakerToggle = false; - } -} - -function handleSpeakerVolumeDocumentPointerDown(event) { - if (!speakerVolumePanelVisible) { - return; - } - if (speakerVolumePanelElement && speakerVolumePanelElement.contains(event.target)) { - return; - } - if (speakerVolumeButton && speakerVolumeButton.contains(event.target)) { - return; - } - closeSpeakerVolumePanel(); -} - -function handleSpeakerVolumeKeydown(event) { - if (!speakerVolumePanelVisible) { - return; - } - if (event.key === "Escape" || event.key === "Esc") { - closeSpeakerVolumePanel(); - } -} - -function handleSpeakerButtonMouseDown(event) { - if (!event) { - return; - } - var target = event.target; - var panel = speakerVolumePanelElement; - if (!panel) { - panel = getById("speakerVolumePanel"); - } - if (panel && panel.contains(target)) { - event.stopPropagation(); - return; - } - event.preventDefault(); - event.stopPropagation(); -} - -function speakerButtonPointerDown(event) { - if (!speakerVolumeButton) { - return; - } - if (event && typeof event.stopPropagation === "function") { - event.stopPropagation(); - } - if (speakerVolumePanelVisible) { - session.ignoreNextSpeakerToggle = true; - closeSpeakerVolumePanel({ preserveToggleGuard: true }); - return; - } - if (event && event.pointerType === "mouse" && typeof event.button === "number" && event.button !== 0) { - return; - } - clearSpeakerVolumeHoldTimer(); - speakerVolumeHoldTimer = setTimeout(function () { - session.ignoreNextSpeakerToggle = true; - openSpeakerVolumePanel(); - }, SPEAKER_VOLUME_HOLD_DELAY); -} - -function speakerButtonPointerUp(event) { - if (event && typeof event.stopPropagation === "function") { - event.stopPropagation(); - } - clearSpeakerVolumeHoldTimer(); - if (speakerVolumePanelVisible) { - session.ignoreNextSpeakerToggle = true; - } -} - -function speakerButtonPointerLeave() { - clearSpeakerVolumeHoldTimer(); -} - -function initSpeakerVolumeControl() { - speakerVolumeButton = getById("mutespeakerbutton"); - speakerVolumePanelElement = getById("speakerVolumePanel"); - speakerVolumeSliderElement = getById("speakerVolumeSlider"); - speakerVolumeValueElement = getById("speakerVolumeValue"); - - if (!speakerVolumeButton || !speakerVolumePanelElement || !speakerVolumeSliderElement) { - return; - } - - var initialPercent = getCurrentSpeakerVolumePercent(); - speakerVolumeSliderElement.value = initialPercent; - if (speakerVolumeValueElement) { - speakerVolumeValueElement.textContent = initialPercent + "%"; - } - speakerVolumePanelElement.classList.add("hidden"); - speakerVolumePanelElement.setAttribute("aria-hidden", "true"); - speakerVolumePanelVisible = false; - - speakerVolumeButton.addEventListener("pointerdown", speakerButtonPointerDown); - speakerVolumeButton.addEventListener("pointerup", speakerButtonPointerUp); - speakerVolumeButton.addEventListener("pointerleave", speakerButtonPointerLeave); - speakerVolumeButton.addEventListener("pointercancel", speakerButtonPointerLeave); - - speakerVolumePanelElement.addEventListener("pointerdown", function (event) { - event.stopPropagation(); - }); - speakerVolumePanelElement.addEventListener("mousedown", function (event) { - event.stopPropagation(); - }); - speakerVolumePanelElement.addEventListener("touchstart", function (event) { - event.stopPropagation(); - }); - - speakerVolumeSliderElement.addEventListener("mousedown", function (event) { - event.stopPropagation(); - }); - speakerVolumeSliderElement.addEventListener("touchstart", function (event) { - event.stopPropagation(); - }); - - speakerVolumeSliderElement.addEventListener("input", handleSpeakerVolumeSliderInput); - speakerVolumeSliderElement.addEventListener("change", handleSpeakerVolumeSliderInput); -} - -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", initSpeakerVolumeControl); -} else { - initSpeakerVolumeControl(); -} - -function getPeerDisplayName(UUID, fallback = "Someone", preferLabel = true) { - if (!UUID || !session.rpcs || !(UUID in session.rpcs)) { - return fallback; - } - - let name = preferLabel - ? (session.rpcs[UUID].label || session.rpcs[UUID].streamID || fallback) - : (session.rpcs[UUID].streamID || session.rpcs[UUID].label || fallback); - if (!name) { - return fallback; - } - - if (typeof name === "string") { - name = sanitizeLabel(name); - } - - return name || fallback; -} - -const fileTransfers = {}; - -function toggleFileshare(UUID = false, event = null) { - if (session.cleanOputput) { return; } - - let string = ''; - if (UUID === false) { - string = 'Share a file with the group
    '; - } else if (session.directorList.indexOf(UUID) >= 0) { - string = `The director requested you share a file with them.
    `; - } else { - const requester = getPeerDisplayName(UUID); - string = `${requester} has requested you share a file with them.
    `; - } - - warnUser(string, false, false); - - updateFileShare(); -} - -const enhancedFileTransfers = { - transfers: {}, - - initTransfer(fileId, total) { - if (!this.transfers[fileId]) { - this.transfers[fileId] = { - progress: 0, - total, - speed: 0, - lastUpdate: Date.now(), - bytesLastSecond: 0, - downloaders: new Map() - }; - } - }, - - updateProgress(fileId, transferred, UUID) { - const transfer = this.transfers[fileId]; - if (!transfer) return; - - const now = Date.now(); - const timeDiff = (now - transfer.lastUpdate) / 1000; - - if (UUID && transfer.downloaders.get(UUID)) { - transfer.downloaders.set(UUID, { - progress: (transferred / transfer.total) * 100, - speed: timeDiff >= 1 ? Math.round((transferred - (transfer.downloaders.get(UUID).bytesTransferred || 0)) / timeDiff) : 0, - bytesTransferred: transferred, - lastUpdate: now - }); - } - - if (timeDiff >= 1) { - transfer.speed = Math.round((transferred - transfer.bytesLastSecond) / timeDiff); - transfer.lastUpdate = now; - transfer.bytesLastSecond = transferred; - } - - transfer.progress = (transferred / transfer.total) * 100; - updateFileShare(); - }, - - removeDownloader(fileId, UUID) { - const transfer = this.transfers[fileId]; - if (transfer) { - transfer.downloaders.delete(UUID); - updateFileShare(); - } - } -}; -let isFileManagerMinimized = false; - -function toggleFileManagerSize() { - isFileManagerMinimized = !isFileManagerMinimized; - updateFileShare(); -} -function updateFileShare() { - if (session.cleanOutput) return; - - const activeSharesDiv = document.getElementById('activeShares'); - activeSharesDiv.innerHTML = ''; - - const container = document.createElement('div'); - container.className = `file-manager${isFileManagerMinimized ? ' minimized' : ''}`; - - // Header - const header = document.createElement('div'); - header.className = 'file-manager-header'; - const headerControls = document.createElement('div'); - headerControls.className = 'header-controls'; - - header.innerHTML = `

    File Sharing

    `; - headerControls.innerHTML = ` - - - `; - header.appendChild(headerControls); - container.appendChild(header); - - if (!isFileManagerMinimized) { - // Hidden file input - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.id = 'fileInput'; - fileInput.style.display = 'none'; - fileInput.multiple = true; - fileInput.onchange = (e) => session.shareFile(e.target, false, e); - container.appendChild(fileInput); - - // File list - const fileList = document.createElement('div'); - fileList.className = 'file-list'; - - if (!(session.hostedFiles && session.hostedFiles.length)) { - fileList.innerHTML = '
    No files being shared
    '; - } else { - session.hostedFiles.forEach(file => { - const transfer = enhancedFileTransfers.transfers[file.id]; - - const fileItem = document.createElement('div'); - fileItem.className = 'file-item'; - - const fileInfo = document.createElement('div'); - fileInfo.className = 'file-info'; - fileInfo.innerHTML = ` - ${file.name} - ${formatFileSize(file.size)} - `; - - if (transfer && transfer.downloaders.size > 0) { - const downloadList = document.createElement('div'); - downloadList.className = 'transfer-info'; - - transfer.downloaders.forEach((data, UUID) => { - const transferItem = document.createElement('div'); - transferItem.className = 'transfer-item'; - transferItem.innerHTML = ` -
    - Downloading by: ${getLabelForUUID(UUID)} -
    -
    -
    - ${Math.round(data.progress)}% - ${formatSpeed(data.speed)} -
    - `; - downloadList.appendChild(transferItem); - }); - - fileInfo.appendChild(downloadList); - } - - const actions = document.createElement('div'); - actions.innerHTML = ` - - `; - - fileItem.appendChild(fileInfo); - fileItem.appendChild(actions); - fileList.appendChild(fileItem); - }); - } - - container.appendChild(fileList); - } - - activeSharesDiv.appendChild(container); -} -function getLabelForUUID(UUID) { - if (!UUID) return 'Unknown'; - - let label = UUID; - if (session.rpcs[UUID] && session.rpcs[UUID].label) { - label = sanitizeLabel(session.rpcs[UUID].label); - } else if (session.pcs[UUID] && session.pcs[UUID].label) { - label = sanitizeLabel(session.pcs[UUID].label); - } - - if (session.directorList.indexOf(UUID) >= 0) { - label = 'Director: ' + label; - } - - return label; -} -function formatFileSize(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; -} - -function formatSpeed(bytesPerSecond) { - return bytesPerSecond ? formatFileSize(bytesPerSecond) + '/s' : ''; -} - -function cancelFileTransfer(fileId) { - console.log(fileId); - - if (fileTransfers[fileId]) { - fileTransfers[fileId].channel.close(); - delete fileTransfers[fileId]; - } - // Remove the file from hostedFiles array - session.hostedFiles = session.hostedFiles.filter(file => file.id !== fileId); - updateFileShare(); // Refresh the file share display -} - -function truncateUrl(url) { - try { - const urlObj = new URL(url); - const path = urlObj.pathname.length > 15 ? - urlObj.pathname.substring(0, 15) + '...' : - urlObj.pathname; - return urlObj.hostname + path; - } catch (e) { - return url.length > 30 ? url.substring(0, 30) + '...' : url; - } -} - -session.sendFile = function (UUID, fileid) { - log("SENDING FILE: " + fileid + " " + UUID); - var fr = new FileReader(); - var fid = session.hostedFiles.findIndex(file => file.id === fileid); - - if (fid === -1) { - warnlog("requested file was not found"); - return; - } else if (session.hostedFiles[fid].state == 0) { - warnlog("requested file has been removed."); - return; - } else if (session.hostedFiles[fid].restricted && session.hostedFiles[fid].restricted !== UUID) { - warnlog("user didn't have access for this file."); - return; - } - - var chunksize = 16384; - var cid = 0; - var channelName = fileid; - if (channelName === "sendChannel") { - channelName = "sendChannel_" + session.generateStreamID(5); - } - - var transferchannel; - if (UUID in session.pcs) { - transferchannel = session.pcs[UUID].createDataChannel(channelName); - } else if (UUID in session.rpcs) { - transferchannel = session.rpcs[UUID].createDataChannel(channelName); - } else { - warnlog("UUID does not exist"); - return; - } - - transferchannel.binaryType = "arraybuffer"; - var chunk = session.hostedFiles[fid].file.slice(0, chunksize); // Use the stored File object - - fileTransfers[fileid] = { - channel: transferchannel, - progress: 0 - }; - - transferchannel.onopen = () => { - transferchannel.send(JSON.stringify({ type: "filetransfer", size: session.hostedFiles[fid].size, filename: session.hostedFiles[fid].name, id: session.hostedFiles[fid].id })); - fr.readAsArrayBuffer(chunk); - }; - - transferchannel.onclose = () => { - try { - var index = session.hostedTransfers.indexOf(transferchannel); - if (index > -1) { - session.hostedTransfers.splice(index, 1); - } - } catch (e) { - errorlog(e); - } - log("Transfer ended"); - delete fileTransfers[fileid]; - updateFileShare(); // Refresh the file share display - transferchannel = null; - enhancedFileTransfers.removeDownloader(fileid, UUID); - }; - - transferchannel.onmessage = event => { - // console.log(event.data); - }; - - session.hostedTransfers.push(transferchannel); - - fr.onload = function () { - if (session.hostedFiles[fid].state == 0) { - return; - } - var arrayBuffer = fr.result; - try { - transferchannel.send(arrayBuffer); - } catch (e) { - try { - transferchannel.close(); - } catch (e) { } - warnlog(e); - return; - } - cid += 1; - const progress = Math.min(100, Math.round((cid * chunksize / session.hostedFiles[fid].size) * 100)); - fileTransfers[fileid].progress = progress; - updateFileShare(); // Update progress display - - enhancedFileTransfers.updateProgress(fileid, cid * chunksize, UUID); - - if (cid * chunksize < session.hostedFiles[fid].size) { - try { - chunk = session.hostedFiles[fid].file.slice(cid * chunksize, (cid + 1) * chunksize); // Use the stored File object - fr.readAsArrayBuffer(chunk); - } catch (e) { - errorlog(e); - } - } else { - transferchannel.send("EOF1"); - transferchannel.close(); - } - }; -}; - -function toggleChat(event = null) { - const chatModule = document.getElementById('chatModule'); - - if (!chatModule.configured) { - setupAdjustableChat(); - } - - if (session.chat === false) { - session.chat = true; - chatModule.classList.remove("hidden"); - getById("chatInput").focus(); - getById("chatNotification").classList.remove("notification", "red"); - getById("chattoggle").classList.remove("pulsate"); - } else { - session.chat = false; - chatModule.classList.add("hidden"); - } - updateMessages(); -} - -function toggleDirectFeedback(event = null) { - const unmuteSelf = document.getElementById('unmuteSelf'); - unmuteSelf.classList.remove("hidden"); - - if (session.videoElement) { - session.videoElement.muted = session.videoElement.muted ? false : true; - - if (session.selfVolume) { - session.selfVolume = parseFloat(session.selfVolume); - if (session.selfVolume >= 1) { - session.videoElement.volume = Math.min(100, Math.max(1, session.selfVolume)) / 100; - session.selfVolume = null; - } else { - session.videoElement.volume = Math.min(1, Math.max(0, session.selfVolume)); - session.selfVolume = null; - } - } - } - - if (session.videoElement.muted) { - unmuteSelf.classList.remove("red", "pulsate"); - unmuteSelf.ariaPressed = "false"; - - } else { - unmuteSelf.classList.add("red", "pulsate"); - unmuteSelf.ariaPressed = "true"; - } -} - -function setupAdjustableChat() { - const chatModule = document.getElementById('chatModule'); - chatModule.configured = true; - - const chatBody = getById('chatBody'); - let isResizing = false; - let isDragging = false; - let startY, startHeight, startX, startTop, startLeft; - - function initResize(e) { - isResizing = true; - document.body.style.userSelect = 'none'; - startY = e.clientY; - startHeight = parseInt(window.getComputedStyle(chatBody).height, 10); - document.addEventListener('mousemove', resize); - document.addEventListener('mouseup', stopResize); - } - - function resize(e) { - if (isResizing) { - const maxHeight = window.innerHeight - chatModule.offsetTop - 20; // 20px buffer - const newHeight = Math.min(startHeight + (e.clientY - startY), maxHeight); - chatBody.style.height = `${Math.max(newHeight, 50)}px`; // Minimum height of 50px - keepInBounds(); - } - } - - function stopResize() { - isResizing = false; - document.removeEventListener('mousemove', resize); - document.removeEventListener('mouseup', stopResize); - document.body.style.userSelect = ''; - } - - function initDrag(e) { - isDragging = true; - document.body.style.userSelect = 'none'; - startX = e.clientX - chatModule.offsetLeft; - startY = e.clientY - chatModule.offsetTop; - document.addEventListener('mousemove', drag); - document.addEventListener('mouseup', stopDrag); - } - - function drag(e) { - if (isDragging) { - const newLeft = Math.min(Math.max(e.clientX - startX, 0), window.innerWidth - chatModule.offsetWidth); - const newTop = Math.min(Math.max(e.clientY - startY, 0), window.innerHeight - chatModule.offsetHeight); - chatModule.style.left = `${newLeft}px`; - chatModule.style.top = `${newTop}px`; - chatModule.style.bottom = 'auto'; - chatModule.style.right = 'auto'; - keepInBounds(); - } - } - - function stopDrag() { - isDragging = false; - document.removeEventListener('mousemove', drag); - document.removeEventListener('mouseup', stopDrag); - document.body.style.userSelect = ''; - } - - function keepInBounds() { - const rect = chatModule.getBoundingClientRect(); - - if (rect.right > window.innerWidth) { - chatModule.style.left = `${window.innerWidth - rect.width}px`; - } - if (rect.bottom > window.innerHeight) { - chatModule.style.top = `${window.innerHeight - rect.height}px`; - } - if (rect.left < 0) { - chatModule.style.left = '0px'; - } - if (rect.top < 0) { - chatModule.style.top = '0px'; - chatModule.style.bottom = 'auto'; - } - } - - chatModule.querySelector('.resizer').addEventListener('mousedown', initResize); - chatModule.querySelector('.chat-header').addEventListener('mousedown', initDrag); - - // Keep chat window in bounds when window is resized - window.addEventListener('resize', keepInBounds); - - // Initial positioning - keepInBounds(); -} - -function directorAdvanced(ele) { - var target = document.createElement("div"); - target.style = "position:absolute;float:left;width:270px;height:222px;background-color:#7E7E7E;"; - - var closeButton = document.createElement("button"); - closeButton.innerHTML = " close"; - closeButton.style.left = "5px"; - closeButton.style.position = "relative"; - closeButton.onclick = function () { - target.parentNode.removeChild(target); - }; - target.appendChild(closeButton); - - var someButton = document.createElement("button"); - someButton.innerHTML = " some action "; - someButton.style.left = "5px"; - someButton.style.position = "relative"; - someButton.onclick = function () { - var actionMsg = {}; - session.sendRequest(actionMsg, ele.dataset.UUID); - }; - target.appendChild(someButton); - - ele.parentNode.appendChild(target); -} - -function directorSendMessage(ele) { - var UUID = ele.dataset.UUID; - var target = document.querySelector("[data--u-u-i-d='" + UUID + "'][data-action-type='messaging-box']"); - if (!target) { - return; - } - - if (target.classList.contains("hidden")) { - target.classList.remove("hidden"); - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } else { - target.classList.add("hidden"); - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - return; - } - - var inputField = target.querySelector("[data-action-type='messaging-box-text']"); - if (inputField) { - inputField.focus(); - inputField.select(); - } - if ("overlay" in target) { - return; - } - - target.overlay = true; - - if (inputField) { - inputField.addEventListener("keydown", function (e) { - if (e.keyCode == 13) { - e.preventDefault(); - sendButton.click(); - } else if (e.keyCode == 27) { - e.preventDefault(); - inputField.value = ""; - target.parentNode.removeChild(target); - } - }); - } - - var sendButton = target.querySelector("[data-action-type='messaging-box-send']"); - if (sendButton) { - sendButton.onclick = function () { - var chatMsg = {}; - chatMsg.chat = inputField.value; - if (sendButton.parentNode.overlay) { - chatMsg.overlay = sendButton.parentNode.overlay; - } - session.sendRequest(chatMsg, ele.dataset.UUID); - inputField.value = ""; - //target.parentNode.removeChild(target); - }; - } - - var closeButton = target.querySelector("[data-action-type='messaging-box-close']"); - if (closeButton) { - closeButton.onclick = function () { - inputField.value = ""; - target.classList.add("hidden"); - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - }; - } - - var overlayMsg = target.querySelector("[data-action-type='messaging-box-toggle']"); - if (overlayMsg) { - overlayMsg.onclick = function (e) { - log(e.target.parentNode.parentNode); - if (e.target.parentNode.parentNode.overlay === true) { - e.target.parentNode.parentNode.overlay = false; - e.target.parentNode.innerHTML = ""; - } else { - e.target.parentNode.parentNode.overlay = true; - e.target.parentNode.innerHTML = ""; - } - }; - } -} - -function toggleAutoVideoMute() { - // for iOS devices, that tab out. - // document.visibilityState - if (!session.videoMuted && session.permaid !== false) { - var msg = {}; - msg.videoMuted = document.visibilityState === "hidden" || false; - //try { - session.sendMessage(msg); - //} catch(e){errorlog(e);} - pokeIframeAPI("video-mute-state", document.visibilityState); - } -} - -function toggleVideoMute(apply = false) { - // TODO: I need to have this be MUTE, toggle, with volume not touched. - if (apply) { - session.videoMuted = !session.videoMuted; - } - - if (!session.remoteVideoMuted) { - getById("head8").classList.add("hidden"); - } - - if (session.videoMuted == false) { - session.videoMuted = true; - getById("mutevideotoggle").className = "las la-video-slash toggleSize"; - if (!session.cleanOutput) { - getById("mutevideobutton").classList.add("red"); - getById("mutevideobutton").ariaPressed = "true"; - getById("header").classList.add("red"); - if (session.remoteVideoMuted) { - getById("head8").classList.remove("hidden"); - } - } - if (session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(track => { - track.enabled = false; - }); - } - } else if (session.remoteVideoMuted) { - // the director has muted this guest's video feed - session.videoMuted = false; // just setting it back to the pre-toggled state - getById("mutevideotoggle").className = "las la-video toggleSize"; - if (!session.cleanOutput) { - getById("head8").classList.remove("hidden"); - getById("header").classList.add("red"); - getById("mutevideobutton").classList.remove("red"); - getById("mutevideobutton").ariaPressed = "false"; - } - if (session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(track => { - track.enabled = false; - }); - } - } else { - session.videoMuted = false; - - getById("mutevideotoggle").className = "las la-video toggleSize"; - if (!session.cleanOutput) { - getById("mutevideobutton").classList.remove("red"); - getById("mutevideobutton").ariaPressed = "false"; - getById("header").classList.remove("red"); - } - if (session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(track => { - track.enabled = true; - }); - } - } - - if (session.avatar && session.avatar.ready && !apply) { - updateRenderOutpipe(); - if (session.videoMuted) { - var msg = {}; - msg.videoMuted = false; // doesn't matter the actual mute state; this is the avatar - session.sendMessage(msg); - } - } else if (!apply) { - var msg = {}; - msg.videoMuted = session.videoMuted; - session.sendMessage(msg); - } - - pokeIframeAPI("video-mute-state", session.videoMuted || session.remoteVideoMuted); - - if (!apply) { - pokeAPI("videoMuted", session.videoMuted || session.remoteVideoMuted); - } - - if (session.style && session.style == 1) { - if (!session.videoElement || session.videoElement.id !== "previewWebcam") { - updateMixer(); - } - } -} -var toggleSettingsState = false; -let settingsClickHandler = null; - -async function toggleSettings(forceShow = false) { - if (session.nosettings) return; - - const multiselectTrigger = getById("multiselect-trigger3"); - multiselectTrigger.dataset.state = "0"; - multiselectTrigger.classList.add("closed"); - multiselectTrigger.classList.remove("open"); - getById("chevarrow2").classList.add("bottom"); - - const popupSelector = getById("popupSelector"); - - // For forceShow, if already open just update devices - if (toggleSettingsState && forceShow) { - await enumerateDevices().then(gotDevices2); - return; - } - - if (popupSelector.style.display === "none") { - await showSettings(); - } else { - hideSettings(); - } - - pokeIframeAPI("settings-menu-state", toggleSettingsState); -} - -async function showSettings() { - const popupSelector = getById("popupSelector"); - const settingsButton = getById("settingsbutton"); - - updateConstraintSliders(); - - // Handler only closes the menu when clicking outside - settingsClickHandler = (e) => { - if (!popupSelector.contains(e.target) && !e.target.closest('#settingsbutton')) { - hideSettings(); - pokeIframeAPI("settings-menu-state", false); - } - }; - - setTimeout(() => { - document.addEventListener("click", settingsClickHandler); - }, 10); - - if (navigator.userAgent.indexOf("Chrome") !== -1) { - try { - const permissionResult = await navigator.permissions.query({ name: "camera" }); - if (permissionResult.state === "prompt") { - try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); - await enumerateDevices().then(gotDevices2); - stream.getTracks().forEach(track => track.stop()); - } catch { - await enumerateDevices().then(gotDevices2); - } - } else { - await enumerateDevices().then(gotDevices2); - } - } catch { - await enumerateDevices().then(gotDevices2); - } - } else { - await enumerateDevices().then(gotDevices2); - } - - popupSelector.style.display = "inline-block"; - settingsButton.classList.add("brown"); - settingsButton.ariaPressed = "true"; - - loadContentEffectsImages(); - - setTimeout(() => { - popupSelector.style.right = "0px"; - }, 1); - - toggleSettingsState = true; -} - -function hideSettings() { - const popupSelector = getById("popupSelector"); - const settingsButton = getById("settingsbutton"); - - if (settingsClickHandler) { - document.removeEventListener("click", settingsClickHandler); - settingsClickHandler = null; - } - - popupSelector.style.right = "-500px"; - settingsButton.classList.remove("brown"); - settingsButton.ariaPressed = "false"; - - setTimeout(() => { - popupSelector.style.display = "none"; - }, 200); - - toggleSettingsState = false; - getById("videoSettings3").style.display = "none"; -} - -function toggleFullscreenButtonSetting() { - var btn = getById("toggleFullscreenButton"); - var fullscreenPage = getById("fullscreenPage"); - - if (iOS || iPad) { - warnUser("Fullscreen is not supported on iOS/iPadOS"); - return; - } - - if (session.fullscreenButton) { - // Disable - session.fullscreenButton = false; - fullscreenPage.classList.add("hidden"); - document.documentElement.style.removeProperty("--full-screen-button"); - btn.innerText = "Enable"; - btn.classList.remove("selected"); - - // Exit fullscreen if currently in it - if (document.fullscreenElement) { - try { document.exitFullscreen(); } catch(e) {} - } - } else { - // Enable - session.fullscreenButton = true; - fullscreenPage.classList.remove("hidden"); - document.documentElement.style.setProperty("--full-screen-button", "none"); - btn.innerText = "Disable"; - btn.classList.add("selected"); - } - - try { - setStorage("fullscreenButtonSetting", session.fullscreenButton); - } catch(e) {} -} - -function togglePIPButtonSetting() { - var btn = getById("togglePIPButton"); - var pipPage = getById("PictureInPicturePage"); - - if (typeof documentPictureInPicture === "undefined") { - warnUser("Picture-in-Picture is not supported in this browser"); - return; - } - - if (pipPage.classList.contains("hidden")) { - // Enable - pipPage.classList.remove("hidden"); - btn.innerText = "Disable"; - btn.classList.add("selected"); - try { - setStorage("pipButtonSetting", true); - } catch(e) {} - } else { - // Disable - pipPage.classList.add("hidden"); - btn.innerText = "Enable"; - btn.classList.remove("selected"); - try { - setStorage("pipButtonSetting", false); - } catch(e) {} - } -} - -function initButtonToggleSettings() { - // Hide toggles on unsupported platforms - if (iOS || iPad) { - var fsContainer = getById("fullscreenToggleContainer"); - if (fsContainer) fsContainer.style.display = "none"; - } - - if (typeof documentPictureInPicture === "undefined") { - var pipContainer = getById("pipToggleContainer"); - if (pipContainer) pipContainer.style.display = "none"; - } - - // Restore saved settings (only if URL params didn't already set them) - try { - var savedFullscreen = getStorage("fullscreenButtonSetting"); - if (savedFullscreen === true && !session.fullscreenButton && !(iOS || iPad)) { - toggleFullscreenButtonSetting(); - } - - var savedPIP = getStorage("pipButtonSetting"); - var pipPage = getById("PictureInPicturePage"); - if (savedPIP === true && pipPage && pipPage.classList.contains("hidden") && typeof documentPictureInPicture !== "undefined") { - togglePIPButtonSetting(); - } - } catch(e) {} - - // Update button states to reflect current state - updateButtonToggleStates(); -} - -function updateButtonToggleStates() { - var fullscreenBtn = getById("toggleFullscreenButton"); - var pipBtn = getById("togglePIPButton"); - var fullscreenPage = getById("fullscreenPage"); - var pipPage = getById("PictureInPicturePage"); - - if (fullscreenBtn && fullscreenPage) { - if (!fullscreenPage.classList.contains("hidden")) { - fullscreenBtn.innerText = "Disable"; - fullscreenBtn.classList.add("selected"); - } else { - fullscreenBtn.innerText = "Enable"; - fullscreenBtn.classList.remove("selected"); - } - } - - if (pipBtn && pipPage) { - if (!pipPage.classList.contains("hidden")) { - pipBtn.innerText = "Disable"; - pipBtn.classList.add("selected"); - } else { - pipBtn.innerText = "Enable"; - pipBtn.classList.remove("selected"); - } - } -} - -let wakeLockObject = null; -let wakeLockReleaseHandler = null; -let wakeLockInteractionArmed = false; - -function armWakeLockOnInteraction() { - if (wakeLockInteractionArmed || !("wakeLock" in navigator)) { - return; - } - - wakeLockInteractionArmed = true; - const handler = () => { - document.removeEventListener("pointerdown", handler); - document.removeEventListener("keydown", handler); - wakeLockInteractionArmed = false; - acquireWakeLock(true); - }; - - document.addEventListener("pointerdown", handler, { once: true }); - document.addEventListener("keydown", handler, { once: true }); -} - -async function acquireWakeLock(fromUserGesture = false) { - if (!("wakeLock" in navigator)) { - warnlog("Wake Lock API is not supported in this browser"); - if (typeof session !== "undefined") { - session.forceLegacyWakeLock = true; - } - startLegacyKeepAliveLoop(); - return; - } - - try { - if (wakeLockObject && wakeLockReleaseHandler) { - try { - wakeLockObject.removeEventListener("release", wakeLockReleaseHandler); - } catch (e) { - errorlog(e); - } - } - - const lock = await navigator.wakeLock.request("screen"); - wakeLockObject = lock; - if (typeof session !== "undefined") { - session.wakeLockActive = true; - session.forceLegacyWakeLock = false; - } - - wakeLockReleaseHandler = () => { - wakeLockObject = null; - wakeLockReleaseHandler = null; - if (typeof session !== "undefined") { - session.wakeLockActive = false; - session.forceLegacyWakeLock = true; - } - startLegacyKeepAliveLoop(); - armWakeLockOnInteraction(); - }; - - wakeLockObject.addEventListener("release", wakeLockReleaseHandler); - removeLegacyKeepAlivePlayer(); - log("Wake Lock is active"); - } catch (err) { - if (typeof session !== "undefined") { - session.wakeLockActive = false; - session.forceLegacyWakeLock = true; - } - startLegacyKeepAliveLoop(); - if (!fromUserGesture) { - armWakeLockOnInteraction(); - } - errorlog(err); - } -} - -function handleVisibilityChangeWakeLock() { - if (document.visibilityState === "visible") { - acquireWakeLock(); - armWakeLockOnInteraction(); - } -} - -function releaseWakeLock() { - if (wakeLockObject) { - wakeLockObject - .release() - .then(() => { - wakeLockObject = null; - if (typeof session !== "undefined") { - session.wakeLockActive = false; - session.forceLegacyWakeLock = true; - } - wakeLockReleaseHandler = null; - startLegacyKeepAliveLoop(); - log("Wake Lock is released"); - }) - .catch(err => { - errorlog(err); - }); - } -} - -// QoS Report - sends anonymous connection quality data on hangup -function sendQosReport() { - if (!session.qosEnabled || !session.qosData || session.qosData.sent) return; - session.qosData.sent = true; // Prevent duplicate sends - - try { - var qd = session.qosData; - - // Helper functions - var avg = function(arr) { - if (!arr || !arr.length) return null; - return arr.reduce(function(a, b) { return a + b; }, 0) / arr.length; - }; - var max = function(arr) { - if (!arr || !arr.length) return null; - return Math.max.apply(null, arr); - }; - - // Determine browser (using existing detection) - var browser = "Unknown"; - var browserVersion = 0; - if (typeof Safari !== "undefined" && Safari) { - browser = "Safari"; - browserVersion = SafariVersion || 0; - } else if (typeof Firefox !== "undefined" && Firefox) { - browser = "Firefox"; - browserVersion = Firefox; - } else if (typeof ChromiumVersion !== "undefined" && ChromiumVersion) { - browser = "Chrome"; - browserVersion = ChromiumVersion; - } - - // Determine platform - var platform = "desktop"; - if (typeof iOS !== "undefined" && iOS) platform = "mobile"; - else if (typeof iPad !== "undefined" && iPad) platform = "tablet"; - else if (/Android/i.test(navigator.userAgent)) platform = "mobile"; - - // Determine connection type - var connectionType = "viewer"; - if (session.director) connectionType = "director"; - else if (session.streamSrc || session.videoElement) connectionType = "publisher"; - - // Get TURN server (already filtered to hostnames by client-side allowlist) - var turnServer = qd.turnServersUsed && qd.turnServersUsed.length ? qd.turnServersUsed[0] : null; - // Trim to 30 chars max (DB limit) - if (turnServer && turnServer.length > 30) turnServer = turnServer.substring(0, 30); - - // Get meshcast server if used (only if &meshcast enabled) - var meshcastServer = null; - if (session.meshcast && qd.meshcastServersUsed && qd.meshcastServersUsed.length > 0) { - meshcastServer = qd.meshcastServersUsed[0]; - } - - // For publishers: also collect stats from outbound connections (session.pcs) - // This supplements the inbound stats from processStats - if (session.pcs) { - for (var uuid in session.pcs) { - try { - var pcStats = session.pcs[uuid].stats; - if (!pcStats) continue; - - // Get transport type from publisher connection - if (pcStats.candidateType_local) { - if (pcStats.candidateType_local === "relay") { - qd.transportType = "turn"; - // TURN hostname already tracked in processPcsQosStats via allowlist - } else if (!qd.transportType || qd.transportType === "unknown") { - qd.transportType = "p2p"; - } - if (!qd.candidateTypesLocal.includes(pcStats.candidateType_local)) { - qd.candidateTypesLocal.push(pcStats.candidateType_local); - } - } - if (pcStats.candidateType_remote && !qd.candidateTypesRemote.includes(pcStats.candidateType_remote)) { - qd.candidateTypesRemote.push(pcStats.candidateType_remote); - } - - // Get RTT from publisher stats - if (pcStats.average_roundTripTime_ms) { - qd.rttSamples.push(pcStats.average_roundTripTime_ms); - } - - // Get resolution/codec from publisher stats - if (pcStats.resolution && !qd.lastResolution) { - qd.lastResolution = pcStats.resolution; - } - if (pcStats.encoder) { - qd.lastVideoCodec = pcStats.encoder; - } - } catch (e) { } - } - } - - // Set transport type for WHIP/WHEP if not already set - if (!qd.transportType || qd.transportType === "unknown") { - if (session.whipOut) qd.transportType = "whip"; - else if (session.whepIn || session.whepInput) qd.transportType = "whep"; - } - - // For non-meshcast WHIP/WHEP, mark as "private" (don't log actual endpoint) - if ((qd.transportType === "whip" || qd.transportType === "whep") && - (!qd.meshcastServersUsed || qd.meshcastServersUsed.length === 0)) { - meshcastServer = "private"; - } - - // Build payload - NO room IDs, stream IDs, or passwords - var payload = { - // Session - sessionDuration: Math.round((Date.now() - qd.startTime) / 1000), - connectionType: connectionType, - - // Client (privacy-safe) - browser: browser, - browserVersion: browserVersion, - platform: platform, - - // Transport - transportType: qd.transportType || "unknown", - turnServer: turnServer, - meshcastServer: meshcastServer, - wssSuccess: qd.wssSuccess, - candidateLocal: qd.candidateTypesLocal.length > 0 ? qd.candidateTypesLocal[0] : null, - candidateRemote: qd.candidateTypesRemote.length > 0 ? qd.candidateTypesRemote[0] : null, - - // Quality - connectionSuccess: qd.connectionSuccesses > 0 || qd.rttSamples.length > 0, - connectionFailures: qd.connectionFailures, - iceRestarts: qd.iceRestarts, - - // Packet loss - avgPacketLossVideo: avg(qd.packetLossVideoSamples) !== null ? Math.round(avg(qd.packetLossVideoSamples) * 100) / 100 : null, - avgPacketLossAudio: avg(qd.packetLossAudioSamples) !== null ? Math.round(avg(qd.packetLossAudioSamples) * 100) / 100 : null, - maxPacketLossVideo: max(qd.packetLossVideoSamples) !== null ? Math.round(max(qd.packetLossVideoSamples) * 100) / 100 : null, - - // Latency - avgRtt: avg(qd.rttSamples) !== null ? Math.round(avg(qd.rttSamples)) : null, - maxRtt: max(qd.rttSamples) !== null ? Math.round(max(qd.rttSamples)) : null, - avgJitter: avg(qd.jitterSamples) !== null ? Math.round(avg(qd.jitterSamples)) : null, - - // Media - videoCodec: qd.lastVideoCodec ? qd.lastVideoCodec.replace("video/", "") : null, - audioCodec: qd.lastAudioCodec ? qd.lastAudioCodec.replace("audio/", "") : null, - avgVideoBitrate: avg(qd.bitrateSamples) !== null ? Math.round(avg(qd.bitrateSamples)) : null, - maxResolution: qd.lastResolution, - - // Errors (last 10, sanitized to remove private data) - errors: (typeof errorReport !== "undefined" && errorReport && errorReport.length > 0) ? - errorReport.slice(-10).map(function(e) { - var msg = String(e.error || e || ""); - // Strip private/personal data from error messages - msg = msg - // Remove full URLs and URL parameters - .replace(/https?:\/\/[^\s"'<>)]+/gi, "[URL]") - .replace(/wss?:\/\/[^\s"'<>)]+/gi, "[WSS]") - // Remove UUIDs (various formats) - .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]") - .replace(/[0-9a-f]{32,}/gi, "[HASH]") - // Remove stream IDs, view IDs, push IDs - .replace(/(streamID|stream_id|streamid|sid|push|view|scene)[=:]["']?[a-zA-Z0-9_-]{3,30}["']?/gi, "$1=[REDACTED]") - // Remove room IDs - .replace(/(room|roomid|room_id)[=:]["']?[a-zA-Z0-9_-]{3,30}["']?/gi, "$1=[REDACTED]") - // Remove passwords and hashes - .replace(/(password|pass|pwd|hash|salt|key|token|auth)[=:]["']?[^\s"'&]{1,50}["']?/gi, "$1=[REDACTED]") - // Remove IP addresses (IPv4 and IPv6) - .replace(/\b(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?\b/g, "[IP]") - .replace(/\b([0-9a-f]{1,4}:){2,7}[0-9a-f]{1,4}\b/gi, "[IPv6]") - // Remove base64-ish strings that might be tokens/credentials - .replace(/[A-Za-z0-9+/=]{40,}/g, "[TOKEN]") - // Remove email addresses - .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, "[EMAIL]") - // Remove query string parameters - .replace(/\?[^\s"'<>]+/g, "?[PARAMS]"); - return { - msg: msg.substring(0, 200), - line: e.line || 0, - time: e.time ? parseInt(e.time) : 0 - }; - }) : null - }; - - // Send using sendBeacon for reliability during page unload - var blob = new Blob([JSON.stringify(payload)], { type: "application/json" }); - navigator.sendBeacon("https://qos.vdo.ninja/v1/report", blob); - log("QoS report sent"); - } catch (e) { - warnlog("QoS report error: " + e); - } -} - -// For view/scene links without explicit hangup - send QoS on page unload -// Only triggers if there are no active push connections (publisher links have explicit hangup) -if (typeof window !== "undefined") { - window.addEventListener("beforeunload", function() { - // Only send for viewers (no push) and if we haven't already sent - if (session && session.qosEnabled && session.qosData && !session.qosData.sent) { - // Check that we're not a publisher (publishers use explicit hangup) - if (!session.streamSrc && !session.videoElement) { - sendQosReport(); - } - } - }); - - // Also trigger on visibility change to hidden (tab close, navigate away) - document.addEventListener("visibilitychange", function() { - if (document.visibilityState === "hidden") { - if (session && session.qosEnabled && session.qosData && !session.qosData.sent) { - // Only for viewers - if (!session.streamSrc && !session.videoElement) { - sendQosReport(); - } - } - } - }); -} - -session.hangup = function (reload = false, estop = false) { - // Send QoS report on hangup - try { - sendQosReport(); - } catch (e) { warnlog(e); } - - try { - window.removeEventListener("beforeunload", confirmUnload); - } catch (e) { } - - // Clean up auto-end timer if it exists - if (session.autoEndTimer) { - clearTimeout(session.autoEndTimer); - session.autoEndTimer = null; - } - if (session.autoEndInterval) { - clearInterval(session.autoEndInterval); - session.autoEndInterval = null; - } - try { - const countdown = document.getElementById("autoEndCountdown"); - if (countdown) { - countdown.remove(); - } - } catch (e) { } - - try { - if (estop) { - recordLocalVideo("estop"); - } - } catch (e) { } - try { - if (estop) { - recordLocalVideo("estop", false, false, true); // screen share - } - } catch (e) { } - session.taintedSession = true; - warnlog("hanging up"); - - try { - recordLocalVideo("stop"); - } catch (e) { } - try { - recordLocalVideo("stop", false, false, true); // screen share - } catch (e) { } - - try { - transferList.forEach(file => { - if (file.writer) { - file.writer.close(); - } - if (file.videoElement && file.videoElement.stopWriter) { - file.videoElement.stopWriter(true); // estop - } - }); - } catch (e) { - errorlog(e); - } - - try { - var msg = {}; - msg.videoMuted = true; // might not trigger - msg.bye = true; - session.sendMessage(msg); // make sure the remote video goes black. - } catch (e) { } - - try { - session.ws.close(); - } catch (e) { } - - try { - if (session.canvasSource && session.canvasSource.srcObject) { - session.canvasSource.srcObject.getTracks().forEach(function (track) { - session.canvasSource.srcObject.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getTracks().forEach(function (track) { - session.videoElement.srcObject.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } - if (session.streamSrc) { - session.streamSrc.getTracks().forEach(function (track) { - session.streamSrc.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } - if (session.streamSrcClone) { - session.streamSrcClone.getTracks().forEach(function (track) { - session.streamSrcClone.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } - if (session.screenStream) { - session.screenStream.getTracks().forEach(function (track) { - session.screenStream.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } - } catch (e) { - errorlog(e); - } - try { - for (i in session.rpcs) { - try { - if (session.rpcs[i].videoElement) { - if (session.rpcs[i].videoElement.recording) { - recordLocalVideo("stop", null, session.rpcs[i].videoElement); - } - } - } catch (e) { } - log("closing rpc due to hangup event"); - session.closeRPC(i, true); - } - - for (i in session.pcs) { - log("closing 5"); - session.closePC(i); - } - } catch (e) { - errorlog(e); - } - - for (var sid in session.watchTimeoutList) { - clearTimeout(session.watchTimeoutList[sid]); - } - - if (session.whipOut && session.whipOut.deleteme) { - session.whipOut.deleteme(); - } - - if (DebugLog && errorReport) { - downloadLogs(); - } - - if (session.popupChat) { - if (!session.popupChat.closed) { - session.popupChat.close(); - session.popupChat = null; - } - } - - releaseWakeLock(); - - if (reload) { - reloadRequested(); - warnlog("Reloading? uh oh. Why didn't it?"); - return; - } else { - setTimeout(function () { - for (i in session) { - try { - delete session[i]; - } catch (e) { } - } - delete session; - }, 1200); - - hangupComplete(); - log("HANG UP COMPLETE"); - } -}; - -function hangup(showhangup = true) { - // TODO: I need to have this be MUTE, toggle, with volume not touched. - if (session.hostedTransfers.length) { - confirmAlt("There are still file transfer in progress\nAre you sure you wish to exit?").then(res => { - if (res) { - try { - if (showhangup) { - document.getElementById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML; - } else { - document.getElementById("main").innerHTML = ""; - document.getElementById("hangupTemplate").innerHTML = ""; - } - } catch (e) { } - - setTimeout(function () { - session.hangup(); - }, 0); - } - }); - } else { - try { - if (showhangup) { - document.getElementById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML; - } else { - document.getElementById("main").innerHTML = ""; - document.getElementById("hangupTemplate").innerHTML = ""; - } - } catch (e) { } - - setTimeout(function () { - session.hangup(); - }, 0); - } -} - -function hangup2() { - session.hangupDirector(); - getById("miniPerformer").innerHTML = ""; - getById("press2talk").dataset.enabled = false; - getById("screensharebutton").classList.add("hidden"); - getById("screenshare2button").classList.add("hidden"); - getById("screenshare3button").classList.add("hidden"); - getById("settingsbutton").classList.add("hidden"); - getById("mutebutton").classList.add("hidden"); - getById("hangupbutton2").classList.add("hidden"); - //getById("chatbutton").classList.remove("hidden"); - getById("controlButtons").classList.remove("hidden"); - //getById("mutespeakerbutton").classList.add("hidden"); - getById("mutevideobutton").classList.add("hidden"); - - getById("screensharebutton").classList.remove("green"); - getById("screensharebutton").ariaPressed = "false"; - - if (!session.showDirector) { - getById("miniPerformer").innerHTML = ''; - miniTranslate(getById("miniPerformer")); - } else { - getById("miniPerformer").innerHTML = ''; - } - getById("miniPerformer").className = ""; -} - -function hangupComplete() { - try { - getById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML; - - } catch (e) { } - - updateMixerRun = function () { }; - - pokeIframeAPI("hungup", true); // don't use Hangup, as that's an action. - pokeAPI("hangup", true); - - if (session.redirectHangup) { - setTimeout(function (href) { - window.location.href = href; - }, session.redirectHangupTimer || 0, session.redirectHangup); - } -} - -function reloadRequested() { - pokeIframeAPI("reloading", true); - window.removeEventListener("beforeunload", confirmUnload); // clear the confirm on reload - location.reload(); // the main reload function call -} -function confirmUnload(event) { - if (!session.noExitPrompt && !session.cleanOutput && session.scene === false && (session.seeding || session.roomid !== false || session.permaid !== false || session.director)) { - (event || window.event).returnValue = "Are you sure you want to exit?"; //Gecko + IE - return "Are you sure you want to exit?"; - } else { - return undefined; // ADDED OCT 29th; get rid of popup. Just close the socket connection if the user is refreshing the page. It's one or the other. - } -} - -function gobackSlide() { - var data = {}; - data.data = [176, 110, 10]; - sendRawMIDI(data); - try { - pokeIframeAPI("back-slide", true); - } catch (e) { } -} - -function nextSlide() { - var data = {}; - data.data = [176, 110, 11]; - sendRawMIDI(data); - - try { - pokeIframeAPI("next-slide", true); - } catch (e) { } -} - -function raisehand() { - if (session.raisehands !== 2 && session.directorUUID == false) { - // fine - log("no director in room yet"); - return false; - } - - var data = {}; - var handstate = false; - - log(data); - if (getById("raisehandbutton").dataset.raised == "0") { - getById("raisehandbutton").dataset.raised = "1"; - getById("raisehandbutton").classList.add("raisedHand"); - data.chat = "Raised hand"; - handstate = true; - log("hand raised"); - } else { - log("hand lowered"); - getById("raisehandbutton").dataset.raised = "0"; - getById("raisehandbutton").classList.remove("raisedHand"); - data.chat = "Lowered hand"; - handstate = false; - } - - if (session.raisehands == 2) { - session.sendMessage(data); - } else { - for (var i = 0; i < session.directorList.length; i++) { - data.UUID = session.directorList[i]; - session.sendMessage(data, data.UUID); - } - } - - try { - pokeIframeAPI("hand", handstate); - } catch (e) { } - - return handstate; -} - -function lowerhand() { - log("hand lowered"); - getById("raisehandbutton").dataset.raised = "0"; - getById("raisehandbutton").classList.remove("raisedHand"); - pokeIframeAPI("hand", false); - return false; -} - -var previousRoom = ""; -var stillNeedRoom = true; -var transferCancelled = false; -var armedTransfer = false; -var transferSettings = {}; - -async function directMigrate(ele, event, room = false) { - // everyone in the room will hangup this guest also? I like that idea. What about the STREAM ID? I suppose we don't kick out if the viewID matches. - log("directMigrate"); - if (room) { - var migrateRoom = room; - } else if (event === false) { - if (previousRoom === null) { - // user cancelled in previous callback - ele.innerHTML = ' transfer'; - miniTranslate(ele); - //ele.style.backgroundColor = null; - ele.classList.remove("armed"); - return false; - } - if (transferCancelled === true) { - ele.innerHTML = ' transfer'; - miniTranslate(ele); - //ele.style.backgroundColor = null; - ele.classList.remove("armed"); - return false; - } - var migrateRoom = previousRoom; - } else if (event.ctrlKey || event.metaKey) { - ele.innerHTML = ' armed'; - miniTranslate(ele); - ele.classList.add("armed"); - //ele.style.backgroundColor = "#BF3F3F"; - transferCancelled = false; - //armedTransfer=true; - Callbacks.push([directMigrate, ele, stillNeedRoom]); - stillNeedRoom = false; - log("Migrate queued"); - return true; - // } else if (armedTransfer){ - //migrateRoom = sanitizeRoomName(previousRoom); - } else { - if (armedTransfer !== false && previousRoom !== "") { - var migrateRoom = sanitizeRoomName(previousRoom); - } else { - var broadcastMode = null; - if ("broadcast" in transferSettings) { - broadcastMode = transferSettings.broadcast; - } else if (session.rpcs[ele.dataset.UUID] && session.rpcs[ele.dataset.UUID].stats.info && "broadcast_mode" in session.rpcs[ele.dataset.UUID].stats.info) { - broadcastMode = session.rpcs[ele.dataset.UUID].stats.info.broadcast_mode; - } else if (session.broadcastTransfer !== null) { - broadcastMode = session.broadcastTransfer; - } - - var queuedMode = null; - if ("queue" in transferSettings) { - queuedMode = transferSettings.queue; - } else if (session.queueTransfer) { - queuedMode = session.queueTransfer; - } - - var updateurl = null; - if ("updateurl" in transferSettings) { - updateurl = transferSettings.updateurl; - } - window.focus(); - - var response = await promptTransfer(previousRoom, broadcastMode, updateurl, queuedMode); - var migrateRoom = response.roomid; - if (migrateRoom !== null) { - transferSettings = response; - } - } - stillNeedRoom = true; - if (migrateRoom === null) { - // user cancelled - ele.innerHTML = ' transfer'; - miniTranslate(ele); - //ele.style.backgroundColor = null; - ele.classList.remove("armed"); - transferCancelled = true; - return false; - } - try { - migrateRoom = sanitizeRoomName(migrateRoom); - previousRoom = migrateRoom; - } catch (e) { } - } - ele.innerHTML = ' transfer'; - miniTranslate(ele); - //ele.style.backgroundColor = null; - ele.classList.remove("armed"); - - if (migrateRoom) { - previousRoom = migrateRoom; - session.directMigrateIssue(migrateRoom, transferSettings, ele.dataset.UUID); - return true; - } -} - -var stillNeedHangupTarget = 1; -async function directHangup(ele, event) { - // everyone in the room will hangup this guest? I like that idea. - var confirmHangup = false; - var blockUser = false; - - if (event == false) { - // Multi-user armed hangup mode - if (stillNeedHangupTarget === 1) { - window.focus(); - confirmHangup = confirm(getTranslation("confirm-disconnect-users")); - stillNeedHangupTarget = confirmHangup; - } else { - confirmHangup = stillNeedHangupTarget; - } - } else if (event === true) { - confirmHangup = true; - } else if (event.ctrlKey || event.metaKey) { - ele.innerHTML = ' ARMED'; - miniTranslate(ele); - ele.classList.add("armed"); - //ele.style.backgroundColor = "#BF3F3F"; - stillNeedHangupTarget = 1; - Callbacks.push([directHangup, ele, false]); - log("Hangup queued"); - return; - } else { - // Single user hangup - show dialog with block option - window.focus(); - var result = await confirmHangupWithBlock(getTranslation("confirm-disconnect-user")); - confirmHangup = result.confirmed; - blockUser = result.block; - } - - if (confirmHangup) { - var msg = {}; - msg.hangup = true; - - // If director chose to block, just set the flag - guest will use their own room info - if (blockUser) { - msg.block = true; - } - - log(msg); - log(ele.dataset.UUID); - var targetUUID = ele.dataset.UUID; - session.sendRequest(msg, targetUUID); - pokeIframeAPI("hungup", "directing", targetUUID); - //session.anysend(msg); // send to everyone in the room, so they know if they are on air or not. - - // Delayed fallback: if the hangup message never arrives (dead connection), - // clean up locally after 4 seconds to prevent stuck control boxes for co-directors - if (session.rpcs[targetUUID]) { - var sessionAtHangup = session.rpcs[targetUUID].session; // Capture session ID to verify it's the same connection - var fallbackTimeout = setTimeout(function(uuid, origSession) { - try { - // Only close if it's still the same session (not a reconnect or re-request) - if (!(uuid in session.rpcs) || session.rpcs[uuid].session !== origSession) { - return; - } - // Skip if connection AND data channel are healthy - message should have been delivered - var rpc = session.rpcs[uuid]; - var connState = rpc.connectionState || "unknown"; - var channelOpen = rpc.receiveChannel && rpc.receiveChannel.readyState === "open"; - if (connState === "connected" && channelOpen) { - warnlog("Hangup fallback: skipping - connection and channel healthy"); - return; - } - warnlog("Hangup fallback: cleaning up (conn: " + connState + ", channel: " + (channelOpen ? "open" : "closed") + ")"); - session.closeRPC(uuid, true); - } catch (e) { - warnlog(e); - } - }, 4000, targetUUID, sessionAtHangup); - // Store timeout so closeRPC can cancel it if cleanup happens normally - session.rpcs[targetUUID].hangupFallbackTimeout = fallbackTimeout; - } - - return true; - } else { - ele.innerHTML = ' Hangup'; - miniTranslate(ele); - //ele.style.backgroundColor = null; - ele.classList.remove("armed"); - return false; - } -} - -function getAutoAssignChannel() { - // Returns channel number (1-8) to assign based on session.autochannels config - if (!session.autochannels || !session.autochannels.length) return false; - - // Build usage map: channel -> count of guests using it - var usage = {}; - session.autochannels.forEach(function(ch) { usage[ch] = 0; }); - - // Count current assignments from sceneAudioChannel buttons in guest containers - var buttons = document.querySelectorAll('#guestFeeds [data-action-type="sceneAudioChannel"][data-state="1"]'); - buttons.forEach(function(btn) { - var ch = parseInt(btn.dataset.channel); - if (ch in usage) { - usage[ch]++; - } - }); - - if (session.autochannelmode === "roundrobin") { - // Pick next in sequence, wrap around - var ch = session.autochannels[session.autochannelIndex % session.autochannels.length]; - session.autochannelIndex++; - return ch; - } else { - // "leastused" mode: pick channel with fewest guests (enables stacking) - var minCount = Infinity; - var bestChannel = session.autochannels[0]; - for (var i = 0; i < session.autochannels.length; i++) { - var ch = session.autochannels[i]; - if (usage[ch] < minCount) { - minCount = usage[ch]; - bestChannel = ch; - } - } - return bestChannel; - } -} - -function autoAssignAudioChannel(UUID) { - // Auto-assign a newly joined guest to an audio channel based on session.autochannels config - if (!session.autochannels) return; - - var channel = false; - - // Check if guest has a preferred channel and it's in the allowed list - if (session.rpcs[UUID] && session.rpcs[UUID].preferChannel) { - var preferred = session.rpcs[UUID].preferChannel; - if (session.autochannels.includes(preferred)) { - channel = preferred; - log("Using guest's preferred channel C" + channel); - } - } - - // Fall back to auto-assignment if no valid preferred channel - if (!channel) { - channel = getAutoAssignChannel(); - } - if (!channel) return; - - // Find the guest's container - var container = getById("container_" + UUID); - if (!container) return; - - // Find the channel button in the guest's container - var btn = container.querySelector('[data-action-type="sceneAudioChannel"][data-channel="' + channel + '"]'); - if (!btn) return; - - // Set button state (skip C4 warning dialog since user configured allowed channels) - btn.dataset.state = "1"; - btn.classList.add("pressed"); - btn.ariaPressed = "true"; - - // Build and send message to scene viewers - var msg = {}; - msg.audioOutputChannel = channel; - msg.sid = session.rpcs[UUID].streamID; - - for (var uuid in session.pcs) { - if (session.pcs[uuid].scene !== false) { - session.sendMessage(msg, uuid); - } - } - - // Sync to co-directors - syncDirectorState(btn); - - log("Auto-assigned " + session.rpcs[UUID].streamID + " to channel C" + channel); -} - -async function directAudioChannel(ele, event, director = false) { - var UUID = ele.dataset.UUID; - var channel = parseInt(ele.dataset.channel); - var added = false; - - if (!(event.ctrlKey || event.metaKey)) { - if (ele.dataset.state == "1") { - ele.dataset.state = "0"; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } else { - if (channel == 4) { - if (event) { - let ret = await confirmAlt("⚠️Warning! Channel 4 should be avoided.\n\nChannel 4 in OBS is used for low-frequency audio and may distort the audio if you record to it.\n\nDo you wish to proceed?", false); - if (!ret) { - return; - } - } - } - ele.dataset.state = "1" - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - added = true; - } - } else if (ele.dataset.state == "1") { - added = true; - } - - if (director) { - - if (added) { - document.querySelectorAll('#controls_director [data-action-type="sceneAudioChannel"][data-state="1"]:not([data-channel="' + channel + '"])').forEach(el => { - el.dataset.state = "0"; - el.classList.remove("pressed"); - el.ariaPressed = "false"; - }); - } else { - document.querySelectorAll('#controls_director [data-action-type="sceneAudioChannel"][data-state="1"][data-channel]').forEach(el => { - el.dataset.state = "0"; - el.classList.remove("pressed"); - el.ariaPressed = "false"; - }); - channel = false; - } - - } else { - - if (added) { - document.querySelectorAll('[data-sid][data--u-u-i-d="' + UUID + '"][data-action-type="sceneAudioChannel"][data-state="1"]:not([data-channel="' + channel + '"])').forEach(el => { - el.dataset.state = "0"; - el.classList.remove("pressed"); - el.ariaPressed = "false"; - }); - } else { - document.querySelectorAll('[data-sid][data--u-u-i-d="' + UUID + '"][data-channel][data-action-type="sceneAudioChannel"][data-state="1"]').forEach(el => { - el.dataset.state = "0"; - el.classList.remove("pressed"); - el.ariaPressed = "false"; - }); - channel = false; - } - } - - var msg = {}; - - msg.audioOutputChannel = channel; - if (director) { - msg.sid = session.streamID; - } else { - msg.sid = ele.dataset.sid; - } - - for (var uuid in session.pcs) { - if (session.pcs[uuid].scene !== false) { - session.sendMessage(msg, uuid); - } - } - syncDirectorState(ele); - - if (channel) { - return true; - } else { - return false; - } -} - -function directEnable(ele, event, director = false) { - // A directing room only is controlled by the Director, with the exception of MUTE. - var scene = ele.dataset.scene; - if (!(event.ctrlKey || event.metaKey)) { - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - if (ele.children[1]) { - ele.children[1].innerHTML = "Add to Scene " + scene; - } - if (director) { - var cc = 0; - getById("container_director") - .querySelectorAll('[data-action-type="addToScene"]') - .forEach(ge => { - if (ge.value == 1) { - cc += 1; - } - }); - if (!cc) { - getById("container_director").style.backgroundColor = null; - getById("container_director").classList.remove("containerGreen"); - } - } else { - var cc = 0; - getById("container_" + ele.dataset.UUID) - .querySelectorAll('[data-action-type="addToScene"]') - .forEach(ge => { - if (ge.value == 1) { - cc += 1; - log("ge.value: '" + ge.value + "'"); - } else { - log("ge.value:--'" + ge.value + "'"); - } - }); - log(cc + " " + "container_" + ele.dataset.UUID); - if (!cc) { - getById("container_" + ele.dataset.UUID).style.backgroundColor = null; - getById("container_" + ele.dataset.UUID).classList.remove("containerGreen"); - } - } - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - if (ele.children[1]) { - ele.children[1].innerHTML = "Remove"; - } - if (director) { - getById("container_director").classList.add("containerGreen"); - } else { - getById("container_" + ele.dataset.UUID).classList.add("containerGreen"); - } - } - } - - var msg = {}; - - scene = scene + ""; - - msg.scene = scene; - msg.action = "display"; - msg.value = ele.value; - msg.target = ele.dataset.sid; - - try { - if (msg.value == 1) { - pokeIframeAPI("add-to-scene", scene, ele.dataset.UUID); - } else { - pokeIframeAPI("remove-from-scene", scene, ele.dataset.UUID); - } - } catch (e) { } - - //for (var uuid in session.pcs){ // removing this since it's obsolete at this point. - // if (session.pcs[uuid].stats.info && ("version" in session.pcs[uuid].stats.info) && (session.pcs[uuid].stats.info.version < 17.2)){ - //// msg.request = "sendroom"; - // session.sendMsg(msg); - // return; - // } - //} - for (var uuid in session.pcs) { - if (session.pcs[uuid].scene === scene) { - session.sendMessage(msg, uuid); - } - } - syncDirectorState(ele); - - if (msg.value) { - return true; - } else { - return false; - } -} - -function syncDirectorState(ele) { - //if (session.director){ // assumed director, since this is a directEnable sub-function - var msg = {}; - msg.directorState = getDetailedState(ele.dataset.sid); - - for (var uuid in session.pcs) { - if (session.pcs[uuid].coDirector) { - session.sendMessage(msg, uuid); - } - } - for (var i in session.directorList) { - var uuid = session.directorList[i]; - if (session.rpcs[uuid]) { - session.sendRequest(msg, uuid); - } - } - - pokeAPI("details", msg.directorState); -} - -function getQuickStats(sid = false) { - var stats = {}; - try { - stats.inbound = {}; - stats.outbound = {}; - - stats.streamID = session.streamID; - - if (session.whipOut && session.whipOut.stats) { - myStats.whip_outbound = session.whipOut.stats; - } - if (session.whepIn && session.whepIn.stats) { - myStats.whep_inbound = session.whepIn.stats; - } - - for (var i in session.rpcs) { - if (session.rpcs[i].streamID) { - stats.inbound[session.rpcs[i].streamID] = session.rpcs[i].stats; - } - } - for (var i in session.pcs) { - stats.outbound[i] = session.pcs[i].stats; - } - } catch (e) { } - if (sid) { - if (sid in stats.inbound) { - return stats.inbound[sid]; - } else { - return null; - } - } - return stats; -} - -function getDetailedState(sid = false) { - var streamList = {}; - var guestFeeds = document.getElementById("guestFeeds"); - - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].streamID) { - if (sid && sid !== session.rpcs[UUID].streamID) { - continue; - } - let item = {}; - item.streamID = session.rpcs[UUID].streamID; - item.label = session.rpcs[UUID].label; - item.group = session.rpcs[UUID].group; - - if (session.rpcs[UUID].stats && session.rpcs[UUID].stats.info) { - item.miscellaneous = session.rpcs[UUID].stats.info; - } - - try { - item.layout = session.rpcs[UUID].layout; - - if (session.director && session.slotmode) { - item.slot = getSlotState(UUID); - } else if (session.currentSlots) { - item.slot = Object.keys(session.currentSlots).find(key => session.currentSlots[key] === session.rpcs[UUID].streamID) || false; - } - - if (item.slot) { - item.slot = parseInt(item.slot); - } - - if (session.director) { - let featured = query("[data--u-u-i-d='" + UUID + "'][data-action-type='solo-video']"); - if (featured && parseInt(featured.value)) { - item.featured = true; - } else { - item.featured = false; - } - } else if (session.infocus && session.infocus === UUID) { - item.featured = true; - } else { - item.featured = false; - } - } catch (e) { - errorlog(e); - } - - item.iframeSrc = session.rpcs[UUID].iframeSrc; - item.localStream = false; - item.muted = session.rpcs[UUID].remoteMuteState; - item.videoMuted = session.rpcs[UUID].videoMuted; - try { - item.activeSpeaker = session.rpcs[UUID].activelySpeaking; - item.defaultSpeaker = session.rpcs[UUID].defaultSpeaker; - } catch (e) { - errorlog(e); - } - - item.videoVisible = session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.checkVisibility(); - if (session.rpcs[UUID].videoElement) { - item.videoVolume = session.rpcs[UUID].videoElement.volume; - } - item.iframeVisible = session.rpcs[UUID].iframeVisible && session.rpcs[UUID].iframeVisible.checkVisibility(); - - if (session.directorList.indexOf(UUID) >= 0) { - item.director = true; - } else { - item.director = false; - } - try { - if (session.director) { - if (guestFeeds) { - var lock = parseInt(document.getElementById("position_" + UUID).dataset.locked); - if (lock) { - item.position = lock; // probably should make a universal function to do this, for all lock requesting - } else { - var child = document.getElementById("container_" + UUID); - if (child) { - var parent = child.parentNode; - if (parent.id == "guestFeeds") { - item.position = Array.prototype.indexOf.call(parent.children, child) + 1; - } - } - } - } - var scenes = getById("container_" + UUID).querySelectorAll('[data-action-type="addToScene"][data-scene][data--u-u-i-d="' + UUID + '"]'); - var sceneState = {}; - for (var i = 0; i < scenes.length; i++) { - if (scenes[i].value == 1) { - sceneState[scenes[i].dataset.scene] = true; - } else { - sceneState[scenes[i].dataset.scene] = false; - } - } - item.scenes = sceneState; - - var others = getById("container_" + UUID).querySelectorAll('[data-action-type][data--u-u-i-d="' + UUID + '"]'); - var otherState = {}; - for (var i = 0; i < others.length; i++) { - if (!others[i] || !others[i].dataset) { - continue; - } - if (others[i].dataset.actionType === "solo-video" && - others[i].classList && - others[i].classList.contains("altpress")) { - otherState[others[i].dataset.actionType] = "alt"; - continue; - } else if (others[i].dataset.actionType === "remove-queue") { - if (others[i].classList.contains("hidden")) { - otherState[others[i].dataset.actionType] = false; - } else { - otherState[others[i].dataset.actionType] = true; - } - continue; - } else if (others[i].dataset.actionType === "hand-raised") { - if (others[i].classList.contains("hidden")) { - otherState[others[i].dataset.actionType] = false; - } else { - otherState[others[i].dataset.actionType] = true; - } - continue; - } - if ("scene" in others[i].dataset) { - continue; - } else if ("toggle-group" == others[i].dataset.actionType) { - continue; - } else if ("value" in others[i]) { - if (others[i].value !== "") { - otherState[others[i].dataset.actionType] = others[i].value; - } - } else { - try { - if (others[i].querySelector(".altpress")) { - otherState[others[i].dataset.actionType] = "alt"; - } else if (others[i].querySelector(".pressed")) { - otherState[others[i].dataset.actionType] = true; - } else if (others[i].dataset.actionType == "soloChat") { - otherState[others[i].dataset.actionType] = false; - } - } catch (e) { - errorlog(e); - } - } - } - item.others = otherState; - } - } catch (e) { } - - streamList[session.rpcs[UUID].streamID] = item; - } - } - - if (sid && sid !== session.streamID) { - return streamList; - } - - streamList[session.streamID] = {}; - - try { - if (session.director) { - var sceneState = {}; - var scenes = getById("container_director").querySelectorAll('[data-action-type="addToScene"][data-scene]'); - for (var i = 0; i < scenes.length; i++) { - if (scenes[i].value == 1) { - sceneState[scenes[i].dataset.scene] = true; - } else { - sceneState[scenes[i].dataset.scene] = false; - } - } - streamList[session.streamID].scenes = sceneState; - } - } catch (e) { } - - if (session.director) { - let featured = document.querySelector("#highlightDirector[data-action-type='solo-video'], #container_director [data-action-type='solo-video']"); - if (featured && parseInt(featured.value)) { - streamList[session.streamID].featured = true; - } else { - streamList[session.streamID].featured = false; - } - } else if (session.infocus && session.infocus === true) { - streamList[session.streamID].featured = true; - } else { - streamList[session.streamID].featured = false; - } - - streamList[session.streamID].label = session.label; - streamList[session.streamID].meta = session.meta; - streamList[session.streamID].group = session.group; - streamList[session.streamID].groupView = session.groupView; - streamList[session.streamID].scene = session.scene; - streamList[session.streamID].streamID = session.streamID; - streamList[session.streamID].iframeSrc = session.iframeSrc; - streamList[session.streamID].director = session.directorState; //session.director is what you want to be; session.directorState is what you are - streamList[session.streamID].localstream = true; // deprecated. - streamList[session.streamID].localStream = true; - streamList[session.streamID].seeding = session.seeding; - streamList[session.streamID].muted = session.muted; - streamList[session.streamID].videoMuted = session.videoMuted; - streamList[session.streamID].videoVisible = session.videoElement && session.videoElement.checkVisibility(); - streamList[session.streamID].speakerMuted = session.speakerMuted; - streamList[session.streamID].position = null; - streamList[session.streamID].meshcast = session.meshcast; - streamList[session.streamID].layout = session.layout; - - try { - if (session.streamID && session.slotmode) { - // Properly check for director's slots by looking directly at currentSlots - let directorSlot = false; - - // Look for the director's main stream in currentSlots - Object.entries(session.currentSlots).forEach(([slot, sid]) => { - if (sid === session.streamID) { - directorSlot = parseInt(slot); - } - }); - - // If the director has a slot assigned, use it - if (directorSlot) { - streamList[session.streamID].slot = directorSlot; - } else { - // No slot found for director - streamList[session.streamID].slot = false; - } - } - } catch (e) { - errorlog(e); - } - - if (session.info && session.info.out) { - streamList[session.streamID].outbound = session.info.out; - } - - if (session.showDirector && session.director) { - var child = document.getElementById("container_director"); - if (child) { - var parent = child.parentNode; - if (parent.id == "guestFeeds") { - streamList[session.streamID].position = Array.prototype.indexOf.call(parent.children, child) + 1; - } - } - } - - if (session.notifyScreenShare) { - streamList[session.streamID].screenSharing = session.screenShareState; - } else { - streamList[session.streamID].screenSharing = false; - } - - if (session.streamSrc) { - streamList[session.streamID].audioTrack = session.streamSrc.getAudioTracks().length !== 0; - streamList[session.streamID].videoTrack = session.streamSrc.getVideoTracks().length !== 0; - } else { - streamList[session.streamID].audioTrack = false; - streamList[session.streamID].videoTrack = false; - } - - return streamList; -} - -function getGuestList() { - var guestFeeds = document.getElementById("guestFeeds"); - if (!guestFeeds) { - return {}; - } - var streamList = {}; - for (var i = 0; i < guestFeeds.children.length; i++) { - try { - if (session.rpcs[guestFeeds.children[i].dataset.UUID]) { - streamList[i + 1 + ""] = { streamID: session.rpcs[guestFeeds.children[i].dataset.UUID].streamID, label: session.rpcs[guestFeeds.children[i].dataset.UUID].label || "" }; - } else if (guestFeeds.children[i].id == "container_director") { - streamList[i + 1 + ""] = { streamID: session.streamID, label: session.label || "" }; - } else if (guestFeeds.children[i].id == "container_screen_director") { - streamList[i + 1 + ""] = { streamID: session.streamID + ":s", label: session.screenShareLabel || "" }; - } - } catch (e) { - errorlog(e); - } - } - - return streamList; -} - -function syncOtherState(sid) { - if (!session.syncState) { - return; - } - if (!session.syncState[sid]) { - return; - } - - /* if (session.rpcs[ele.dataset.UUID].directorMutedState==1){ - pokeIframeAPI("director-mute-state", true, ele.dataset.UUID); - pokeAPI("directorMuted", true, session.rpcs[ele.dataset.UUID].streamID); - } else { - pokeIframeAPI("director-mute-state", false, ele.dataset.UUID); - pokeAPI("directorMuted", false, session.rpcs[ele.dataset.UUID].streamID); - } */ - - var others = session.syncState[sid].others; - - log(others); - - for (var other in others) { - if (other == "toggle-group") { - continue; - } - var ele = document.querySelector('[data-sid="' + sid + '"][data-action-type="' + other + '"]'); - if (ele) { - var state = others[other]; - if (state === "alt" && other === "solo-video") { - if ("value" in ele) { - ele.value = 1; - } - if (ele.nodeName && ele.nodeName.toLowerCase() == "input") { - try { - ele.checked = true; - } catch (e) { } - } - ele.classList.add("altpress"); - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - continue; - } else if (other === "remove-queue") { - if (state) { - ele.classList.remove("hidden"); - } else { - ele.classList.add("hidden"); - } - continue; - } else if (other === "hand-raised") { - if (state) { - ele.classList.remove("hidden"); - } else { - ele.classList.add("hidden"); - } - continue; - } else { - ele.classList.remove("altpress"); - } - if (state) { - if (!("value" in ele)) { - errorlog("NO DEFAULT VALUE IN SPECIFIED ELEMENT; guessing default: " + other); - ele.value = 0; - } - var changed = true; - if (ele.value == state) { - changed = false; - } - if (other == "mute-guest") { - if (changed) { - remoteMute(ele, false, true); - } - } else if (other == "hide-guest") { - if (changed) { - remoteHideVideo(ele, true, true); - } - } else if (other == "mute-video-guest") { - if (changed) { - remoteMuteVideo(ele, true, true); - } - } else { - ele.value = state; - - if (ele.nodeName.toLowerCase() == "input") { - ele.value = parseInt(state); - } else if (parseInt(state)) { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } else { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } - } - } - } - } - - var UUID = document.querySelector('[data-sid="' + sid + '"][data--u-u-i-d'); - if (UUID && UUID.dataset.UUID) { - UUID = UUID.dataset.UUID; - if (session.syncState[sid].group && session.rpcs[UUID]) { - session.rpcs[UUID].group = session.syncState[sid].group; - syncGroup(session.rpcs[UUID].group, UUID); - } - } -} - -function htmlToElement(html) { - var template = document.createElement("template"); - html = html.trim(); // Never return a text node of whitespace as the result - template.innerHTML = html; - return template.content.firstChild; -} - -function syncGroup(groups, UUID) { - if (!groups || typeof groups !== "object") { - errorlog("Group isn't an object"); - return; - } - groups.forEach(group => { - var ele = getById("container_" + UUID).querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]'); - if (!ele) { - var newGroup = htmlToElement('"); - - var added = false; - getById("container_" + UUID) - .querySelectorAll(".customGroup>[data-group]") - .forEach(ele => { - log(ele); - if (!added && ele.dataset.group > group + "") { - ele.parentNode.insertBefore(newGroup, ele); - added = true; - } - }); - if (!added) { - var newGroupCon = getById("container_" + UUID).querySelector(".customGroup"); - if (!newGroupCon) { - newGroupCon = document.createElement("div"); - newGroupCon.classList.add("customGroup"); - getById("container_" + UUID).appendChild(newGroupCon); - } - newGroupCon.appendChild(newGroup); - } - } - }); - - var elements = document.querySelectorAll('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group]'); - if (elements.length) { - for (var i = 0; i < elements.length; i++) { - if (session.rpcs[UUID].group.includes(elements[i].dataset.group)) { - elements[i].classList.add("pressed"); - elements[i].ariaPressed = "true"; - } else { - elements[i].classList.remove("pressed"); - elements[i].ariaPressed = "false"; - } - } - log("synced group"); - } else { - log("not syncing group buttons; don't exist"); - } -} - -function syncLabelState(sid) { - if (!session.syncState || !session.syncState[sid]) { - return; - } - var newLabel = session.syncState[sid].label; - // Find the UUID for this streamID - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].streamID === sid) { - // Update the RPC label - if (session.rpcs[UUID].label !== newLabel) { - session.rpcs[UUID].label = newLabel; - // Label came from director sync - mark it to prevent info message from overwriting - if (newLabel) { - session.rpcs[UUID].labelSetByDirector = true; - } else { - session.rpcs[UUID].labelSetByDirector = false; - } - // Update the UI label element - var labelEle = document.getElementById("label_" + UUID); - if (labelEle) { - if (newLabel) { - labelEle.innerText = newLabel; - labelEle.classList.remove("addALabel"); - } else if (session.directorUUID === UUID) { - miniTranslate(labelEle, "main-director"); - labelEle.classList.remove("addALabel"); - } else if (session.directorList.indexOf(UUID) >= 0) { - miniTranslate(labelEle, "co-director"); - labelEle.classList.remove("addALabel"); - } else { - miniTranslate(labelEle, "add-a-label"); - labelEle.classList.add("addALabel"); - } - } - log("synced label for " + sid + ": " + newLabel); - } - break; - } - } -} - -function syncSceneState(sid) { - if (!session.syncState) { - return; - } - if (!session.syncState[sid]) { - return; - } - var scenes = session.syncState[sid].scenes || []; - for (var scene in scenes) { - try { - var ele = document.querySelector('[data-sid="' + sid + '"][data-action-type="addToScene"][data-scene="' + scene + '"]'); - if (ele) { - if (scenes[scene]) { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - getById("container_" + ele.dataset.UUID).classList.add("containerGreen"); - if (ele.children[1]) { - ele.children[1].innerHTML = "Remove"; - } - } else { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - if (ele.children[1]) { - ele.children[1].innerHTML = "Add to Scene " + scene; - } - } - } - } catch (e) { } - } -} - -function issueLayout(scene = false, UUID = false) { - // A directing room only is controlled by the Director, with the exception of MUTE. - log("issueLayout() called"); - - var msg = {}; - msg.layout = session.layout; - msg.layout_array = session.layout_array; - - //try { - // pokeIframeAPI("layout", {layout:layout, scene:scene}); - //} catch(e){} - - /* session.layout = { - "stevetestA": { - x:0, - y:0, - w:40, - h:40, - z:0, - c:false - - }, - "stevetestB": { - x:50, - y:50, - w:40, - h:40, - z:1, - c:true - } - }; */ - - if (UUID) { - if (session.pcs[UUID] && scene !== false && session.pcs[UUID].scene === scene + "" && !session.pcs[UUID].solo && session.pcs[UUID].layout) { - // scene specified - session.sendMessage(msg, UUID); - session.pcs[UUID].layoutState = normalizeLayoutStateValue(session.layout); - } else if (session.pcs[UUID] && session.pcs[UUID].layout && !session.pcs[UUID].solo) { - // no scene targetted - session.sendMessage(msg, UUID); - session.pcs[UUID].layoutState = normalizeLayoutStateValue(session.layout); - log("broadcast"); - } - } else { - for (var uuid in session.pcs) { - if (scene !== false && session.pcs[uuid].scene === scene + "" && !session.pcs[uuid].solo && session.pcs[uuid].layout) { - session.sendMessage(msg, uuid); - session.pcs[uuid].layoutState = normalizeLayoutStateValue(session.layout); - } else if (session.pcs[uuid].layout && !session.pcs[uuid].solo) { - session.sendMessage(msg, uuid); - session.pcs[uuid].layoutState = normalizeLayoutStateValue(session.layout); - log("broadcast"); - } - } - } -} - -async function issueLayoutOBS(data) { - // A directing room only is controlled by the Director, with the exception of MUTE. - - var layout = data.layout || false; - var scene = data.scene || false; - var UUID = data.UUID || false; - var obsCommand = data.obsCommand || false; - const normalizedLayoutState = normalizeLayoutStateValue(layout); - - log("issueLayoutOBS() called"); - var msg = {}; - msg.layout = layout; - msg.obsCommand = obsCommand; - - if (data.remote) { - msg.remote = data.remote; - } else { - msg.remote = session.remote || true; - } - msg = await session.encodeRemote(msg); - - if (UUID) { - try { - log("CONTROL STATE" + session.pcs[UUID].obsState.details.controlLevel); - } catch (e) { } - - if (session.pcs[UUID] && scene !== false && session.pcs[UUID].scene === scene + "") { - if (!session.pcs[UUID].solo) { - session.sendMessage(msg, UUID); - session.pcs[UUID].layoutState = normalizedLayoutState; - } - } else if (session.pcs[UUID] && session.pcs[UUID].layout) { - session.sendMessage(msg, UUID); - session.pcs[UUID].layoutState = normalizedLayoutState; - log("broadcast"); - } - } else { - for (var uuid in session.pcs) { - try { - log("CONTROL STATE" + session.pcs[UUID].obsState.details.controlLevel); - } catch (e) { } - - if (scene !== false && session.pcs[uuid].scene === scene + "") { - if (!session.pcs[uuid].solo) { - session.sendMessage(msg, uuid); - session.pcs[uuid].layoutState = normalizedLayoutState; - } - } else if (session.pcs[uuid].layout) { - session.sendMessage(msg, uuid); - session.pcs[uuid].layoutState = normalizedLayoutState; - log("broadcast"); - } - } - } -} - -var previousURL = ""; -var stillNeedURL = true; -var reloadCancelled = false; -var armedReload = false; - -async function directPageReload(ele, event) { - log("URL Page reload"); - if (event === false) { - if (previousURL === null) { - // user cancelled in previous callback - ele.innerHTML = ' change URL'; - miniTranslate(ele); - ele.classList.remove("armed"); // ele.style.backgroundColor = null; - return; - } - if (reloadCancelled === true) { - ele.innerHTML = ' change URL'; - miniTranslate(ele); - ele.classList.remove("armed"); - //ele.style.backgroundColor = null; - return; - } - reloadURL = previousURL; - } else if (event.ctrlKey || event.metaKey) { - ele.innerHTML = ' armed'; - miniTranslate(ele); - ele.classList.add("armed"); - //ele.style.backgroundColor = "#BF3F3F"; - reloadCancelled = false; - armedReload = true; - Callbacks.push([directPageReload, ele, stillNeedURL]); - stillNeedURL = false; - log("URL update queued"); - return; - } else if (armedReload) { - reloadURL = previousURL; - } else { - window.focus(); - var reloadURL = await promptAlt(getTranslation("transfer-guest-to-url"), false, false, previousURL); - stillNeedURL = true; - if (reloadURL === null) { - // user cancelled - ele.innerHTML = ' change URL'; - miniTranslate(ele); - ele.classList.remove("armed"); - //ele.style.backgroundColor = null; - reloadCancelled = true; - return; - } - try { - previousURL = reloadURL; - } catch (e) { } - } - ele.innerHTML = ' change URL'; - miniTranslate(ele); - ele.classList.remove("armed"); //ele.style.backgroundColor = null; - - if (reloadURL) { - previousURL = reloadURL; - - var msg = {}; - msg.changeURL = reloadURL; - if (ele.dataset.UUID in session.rpcs) { - session.rpcs[ele.dataset.UUID].receiveChannel.send(JSON.stringify(msg)); - } - } -} - -async function directTimer(ele, event = false, manualSetTime = false) { - // A directing room only is controlled by the Director, with the exception of MUTE. - log("directTimer"); - var msg = {}; - ele.classList.remove("blue"); - ele.classList.remove("red"); - if (!event || !(event.ctrlKey || event.metaKey)) { - if (ele.value == 0 || ele.value == 2) { - if (manualSetTime !== false) { - var getTime = parseFloat(manualSetTime) || 0; - } else { - var getTime = await promptAlt("Time to set count down timer", false, false, parseInt(getById("overlayClockContainer").dataset.initial), true); - } - if (getTime === null) { - return; - } - getById("overlayClockContainer").dataset.initial = parseInt(getTime); - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.classList.remove("red"); - msg.setClock = getTime; - msg.showClock = true; - msg.startClock = true; - - ele.innerHTML = ' Remove Timer'; - } else if (ele.value == 3) { - ele.value = 1; - msg.resumeClock = true; - ele.classList.add("red"); - } else { - ele.value = 2; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - msg.stopClock = true; - msg.hideClock = true; - ele.innerHTML = ' Create Timer'; - } - //miniTranslate(ele); - } else if (event.ctrlKey || event.metaKey) { - if (ele.value == 1) { - ele.value = 3; - msg.pauseClock = true; - ele.classList.add("blue"); - } else if (ele.value == 3) { - ele.value = 1; - msg.resumeClock = true; - ele.classList.add("red"); - } - } - - if (!session.director) { - return; - } - - if (ele.dataset.UUID) { - if (session.sendRequest(msg, ele.dataset.UUID)) { - return true; - } - } else { - if (session.sendRequest(msg)) { - return true; - } - } - return false; -} -function formatTime24h(date, clock24 = true) { - let hours = date.getHours(); - let minutes = date.getMinutes(); - - // Ensure hours are always 0-23 - hours = hours % 24; - - // Pad single digit hours and minutes with leading zeros - hours = hours.toString().padStart(2, '0'); - minutes = minutes.toString().padStart(2, '0'); - - if (clock24) { - // 24-hour format - return `${hours}:${minutes}`; - } else { - // 12-hour format - let period = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12 || 12; // Convert 0 to 12 for midnight - return `${hours}:${minutes} ${period}`; - } -} - -function toggleClock(clock24 = session.clock24) { - if (session.showTime === false) { - return; - } - if (session.showTime) { - clearInterval(session.showTime); - session.showTime = null; - var clock = getById("overlayClock2"); - clock.ctx = null; - clock.canvas = null; - - if (document.pictureInPictureElement && clock.video) { - if (document.pictureInPictureElement == clock.video) { - document.exitPictureInPicture(); - pokeIframeAPI("picture-in-picture", false); - } - clock.video.remove; - } - clock.video = null; - clock.innerHTML = ""; - getById("overlayClockContainer2").classList.add("hidden"); - } else { - var time = new Date(); - - var clock = getById("overlayClock2"); - if (clock.ctx) { - clock.ctx.beginPath(); - clock.ctx.rect(0, 0, 230, 40); - clock.ctx.fillStyle = "#000"; - clock.ctx.fill(); - clock.ctx.fillStyle = "#FFF"; - clock.ctx.font = "50px monospace"; - clock.ctx.textAlign = "center"; - clock.ctx.fillText(formatTime24h(time, clock24), 115, 37); - } else { - clock.innerHTML = formatTime24h(time, clock24); - } - - session.showTime = setInterval(function () { - var time = new Date(); - - var clock = getById("overlayClock2"); - if (clock.ctx) { - clock.ctx.beginPath(); - clock.ctx.rect(0, 0, 230, 40); - clock.ctx.fillStyle = "#000"; - clock.ctx.fill(); - clock.ctx.fillStyle = "#FFF"; - clock.ctx.font = "50px monospace"; - clock.ctx.textAlign = "center"; - clock.ctx.fillText(formatTime24h(time, clock24), 115, 37); - } else { - getById("overlayClock2").innerHTML = formatTime24h(time, clock24); - } - }, 2000); - getById("overlayClockContainer2").classList.remove("hidden"); - } - return; -} - -async function directRoomClock(ele, event = false) { - if (ele.active) { - ele.active = false; - session.showRoomTime = false; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } else { - ele.active = true; - session.showRoomTime = true; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } - - if (session.showTime !== false) { - if (ele.active && !session.showTime) { - toggleClock(); - } else if (!ele.active && session.showTime) { - toggleClock(); - } - } - var msg = {}; - if (session.clock24 !== null) { - msg.clock24 = session.clock24; - } - msg.showTime = session.showRoomTime; - session.sendRequest(msg); -} -async function directRoomTimer(ele, event = false, preSetTime = false) { - // A directing room only is controlled by the Director, with the exception of MUTE. - log("directGlobalRoomTimer"); - var msg = {}; - ele.classList.remove("blue"); - ele.classList.remove("red"); - - getById("overlayClockContainer").style.fontSize = "50px"; - - if (!event || !(event.ctrlKey || event.metaKey || event.altKey)) { - if (ele.value == 0 || ele.value == 2) { - if (preSetTime !== false) { - var getTime = preSetTime; - } else { - var getTime = await promptAlt("Time to set count down timer", false, false, parseInt(getById("overlayClockContainer").dataset.initial), true); - } - if (getTime === null) { - return; - } - getTime = parseInt(getTime); - getById("overlayClockContainer").dataset.initial = getTime; - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.classList.remove("red"); - - session.roomTimer = Date.now() / 1000 + getTime; - session.roomTimerGlobal = false; - - msg.setClock = getTime; - setClock(getTime); - msg.showClock = true; - showClock(); - msg.startClock = true; - startClock(); - ele.innerHTML = ' Remove Timer'; - } else if (ele.value == 3) { - ele.value = 1; - msg.resumeClock = true; - resumeClock(); // this needed to be removed, right? - if (!session.roomTimer) { - session.roomTimer = false; - } else if (session.roomTimer > 0) { - session.roomTimer = false; - } else { - session.roomTimer = Date.now() / 1000 - session.roomTimer; - } - ele.innerHTML = ' Remove Timer'; - ele.classList.add("red"); - } else { - ele.value = 2; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - session.roomTimer = false; - msg.stopClock = true; - stopClock(); - msg.hideClock = true; - hideClock(); - ele.innerHTML = ' Create Timer'; - } - //miniTranslate(ele); - } else if (event.ctrlKey || event.metaKey || event.altKey) { - if (ele.value == 1) { - ele.value = 3; - msg.pauseClock = true; - pauseClock(); - if (!session.roomTimer) { - session.roomTimer = false; - } else if (session.roomTimer < Date.now() / 1000) { - session.roomTimer = false; - } else { - session.roomTimer = Date.now() / 1000 - session.roomTimer; - } - ele.innerHTML = ' Resume Timer'; - ele.classList.add("blue"); - } else if (ele.value == 3) { - ele.value = 1; - msg.resumeClock = true; - resumeClock(); - if (!session.roomTimer) { - session.roomTimer = false; - } else if (session.roomTimer > 0) { - session.roomTimer = false; - } else { - session.roomTimer = Date.now() / 1000 - session.roomTimer; - } - ele.innerHTML = ' Remove Timer'; - ele.classList.add("red"); - - } else if (event.altKey && ele.dataset.actionType && !ele.dataset.UUID && (ele.dataset.actionType == "create-timer-global")) { - if (preSetTime !== false) { - var getTime = preSetTime; - } else { - var getTime = await promptAlt("Time to set count down timer .\n(This alt-timer will show in scenes-also)", false, false, parseInt(getById("overlayClockContainer").dataset.initial), true); - } - if (getTime === null) { - return; - } - getTime = parseInt(getTime); - getById("overlayClockContainer").dataset.initial = getTime; - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.classList.remove("red"); - - session.roomTimer = Date.now() / 1000 + getTime; - session.roomTimerGlobal = true; - - msg.setClock = getTime; - setClock(getTime); - msg.showClock = true; - showClock(); - msg.startClock = true; - startClock(); - ele.innerHTML = ' Remove Global Timer'; - } - } - if (!session.director) { - return; - } - if (ele.dataset.UUID) { - session.sendRequest(msg, ele.dataset.UUID); - } else if (session.roomTimerGlobal) { - session.sendPeers(msg); - } else { - session.sendRequest(msg); - } -} - -function updateRemoteTimerButton(UUID, currentTime) { - var elements = document.querySelectorAll('[data-action-type="create-timer"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - if (elements[0].value != 2) { - var time = parseInt(currentTime) || 0; - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - elements[0].value = 1; - if (time < 0) { - time = time * -1; - var minutes = Math.floor(time / 60); - var seconds = time - minutes * 60; - elements[0].classList.add("red"); - elements[0].innerHTML = ' -' + minutes + "m : " + zpadTime(seconds) + "s"; - } else { - var minutes = Math.floor(time / 60); - var seconds = time - minutes * 60; - elements[0].classList.remove("red"); - elements[0].innerHTML = ' ' + minutes + "m : " + zpadTime(seconds) + "s"; - } - } else { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - elements[0].classList.remove("red"); - elements[0].innerHTML = ' Create Timer'; - } - } -} - -function directMute(ele, event = false) { - // A directing room only is controlled by the Director, with the exception of MUTE. - log("mute 2"); - - if (!event || !(event.ctrlKey || event.metaKey)) { - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - miniTranslate(ele, "mute-scene"); - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - miniTranslate(ele, "unmute"); - } - } - var msg = {}; - msg.scene = true; - msg.action = "mute"; - msg.value = ele.value; - msg.target = ele.dataset.sid; - - log(msg); - log("ele:"); - log(ele); - //for (var uuid in session.pcs){ // obsolete at this point; v22 - // if (session.pcs[uuid].stats.info && ("version" in session.pcs[uuid].stats.info) && (session.pcs[uuid].stats.info.version < 17.2)){ - // msg.request = "sendroom"; - // session.sendMsg(msg); - // return; - // } - //} - - for (var uuid in session.pcs) { - if (session.pcs[uuid].scene !== false) { - // send to all scenes (but scene = 0) - session.sendMessage(msg, uuid); - } - } - - syncDirectorState(ele); - - if (msg.value) { - return true; - } else { - return false; - } -} - -function requestFileUpload(ele) { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.disabled = true; - ele.innerHTML = ' Requesting..'; - setTimeout( - function (ele) { - try { - ele.innerHTML = ' Request File'; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - ele.disabled = false; - } catch (e) { } - }, - 15000, - ele - ); - var msg = { - requestUpload: true, - UUID: ele.dataset.UUID - }; - session.sendRequest(msg, ele.dataset.UUID); -} - -function remoteSpeakerMute(ele, event = false) { - log("speaker mute"); - if (!event || !(event.ctrlKey || event.metaKey)) { - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - ele.innerHTML = ' Deafen'; - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.innerHTML = ' Undeafen'; - } - miniTranslate(ele); - } - - var msg = {}; - if (ele.value == 0) { - msg.speakerMute = false; - } else { - msg.speakerMute = true; - } - msg.UUID = ele.dataset.UUID; - session.sendRequest(msg, ele.dataset.UUID); - syncDirectorState(ele); - - errorlog(msg); - - return msg.speakerMute; -} - -function updateRemoteSpeakerMute(UUID) { - var ele = document.querySelectorAll('[data-action-type="toggle-remote-speaker"][data--u-u-i-d="' + UUID + '"]'); - if (ele[0]) { - ele[0].classList.add("pressed"); - ele[0].ariaPressed = "true"; - ele[0].value = 1; - ele[0].innerHTML = ' undeafen'; - miniTranslate(ele[0]); - } - return true; -} - -function updateRemoteDisplayMute(UUID, blind = true) { - var ele = document.querySelectorAll('[data-action-type="toggle-remote-display"][data--u-u-i-d="' + UUID + '"]'); - if (ele[0]) { - if (blind) { - ele[0].classList.add("pressed"); - ele[0].ariaPressed = "true"; - ele[0].value = 1; - ele[0].innerHTML = ' unblind'; - miniTranslate(ele[0]); - return true; - } else { - ele[0].classList.remove("pressed"); - ele[0].ariaPressed = "false"; - ele[0].value = 0; - ele[0].innerHTML = ' blind'; - miniTranslate(ele[0]); - return false; - } - } - return false; -} - -function blindAllGuests(ele, event = false) { - if (!session.director) { - if (!session.cleanOutput) { - warnUser("Only a director can mute other guests"); - } - return; - } // only a director can use this button. - - log("blind all display mute"); - if (!event || !(event.ctrlKey || event.metaKey)) { - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - ele.classList.remove("red"); - ele.innerHTML = ''; - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.classList.add("red"); - ele.innerHTML = ''; - } - } - - var msg = {}; - if (ele.value == 0) { - msg.displayMute = false; - session.directorBlindAllGuests = false; - } else { - msg.displayMute = true; - session.directorBlindAllGuests = true; - } - for (var UUID in session.rpcs) { - // doesn't include scenes, as they don't publiish and this is rpcs - if (session.directorList.indexOf(UUID) >= 0) { - continue; - } // don't try to mute other directors - try { - session.sendRequest(msg, UUID); - updateRemoteDisplayMute(UUID, msg.displayMute); - } catch (e) { - errorlog(e); - } - } - syncDirectorState(ele); - return msg.displayMute; -} - -function remoteDisplayMute(ele, event = false) { - log("display mute"); - if (!event || !(event.ctrlKey || event.metaKey)) { - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - ele.innerHTML = ' Blind'; - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.innerHTML = ' Unblind'; - } - miniTranslate(ele); - } - - var msg = {}; - if (ele.value == 0) { - msg.displayMute = false; - } else { - msg.displayMute = true; - } - msg.UUID = ele.dataset.UUID; - session.sendRequest(msg, ele.dataset.UUID); - syncDirectorState(ele); - return msg.displayMute; -} - -function remoteLowerhands(UUID) { - var msg = {}; - msg.lowerhand = true; - msg.UUID = UUID; - session.sendRequest(msg, UUID); - - try { - getById("hands_" + UUID).classList.add("hidden"); - session.rpcs[UUID].remoteRaisedHandElement.classList.add("hidden"); - } catch (e) { } - - // Sync hand-lowered state to co-directors (only main director syncs) - if (session.directorState !== false) { - try { - if (session.rpcs[UUID] && session.rpcs[UUID].streamID) { - var ele = { dataset: { sid: session.rpcs[UUID].streamID } }; - syncDirectorState(ele); - } - } catch (e) { errorlog(e); } - } - - return true; -} - -function remoteMute(ele, event = false, skipSend = false) { - log("mute"); - var val = parseInt(ele.value) || 0; - if (!event || !(event.ctrlKey || event.metaKey)) { - if (val == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - //ele.innerHTML = ' mute'; - miniTranslate(ele, "mute"); - //ele.innerHTML += getTranslation("mute"); - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - //ele.innerHTML = ' unmute'; - miniTranslate(ele, "unmute"); - } - } - - try { - session.rpcs[ele.dataset.UUID].directorMutedState = ele.value; - var volume = session.rpcs[ele.dataset.UUID].directorVolumeState; - } catch (e) { - errorlog(e); - var volume = 100; - } - - if (!skipSend) { - var msg = {}; - if (val == 1) { - msg.volume = volume; - } else { - msg.volume = 0; - } - msg.UUID = ele.dataset.UUID; - session.sendRequest(msg, ele.dataset.UUID); - syncDirectorState(ele); - log(msg); - } - - if (session.rpcs[ele.dataset.UUID].directorMutedState == 1) { - pokeIframeAPI("director-mute-state", true, ele.dataset.UUID); - pokeAPI("directorMuted", true, session.rpcs[ele.dataset.UUID].streamID); - } else { - pokeIframeAPI("director-mute-state", false, ele.dataset.UUID); - pokeAPI("directorMuted", false, session.rpcs[ele.dataset.UUID].streamID); - } - - if (val) { - return true; - } else { - return false; - } -} - -function toggleQualityGear3() { - toggle(document.getElementById("videoSettings3"), (inline = false)); - if (getById("gear_webcam3").style.display === "inline-block") { - var videoSelect = document.querySelector("select#videoSource3").options; - var obscam = false; - log(videoSelect[videoSelect.selectedIndex].text); - if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) { - // OBS Virtualcam - obscam = true; - } else if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) { - // OBS Virtualcam - obscam = true; - } - - updateStats(obscam); - } -} - -function remoteHideVideo(ele, event = false, skipSend = false) { - log("video hide"); - - if (!event || event.ctrlKey || event.metaKey) { - //ele.children[1].innerHTML = getTranslation("armed"); - miniTranslate(ele.children[1], "armed"); - //ele.style.backgroundColor = "#BF3F3F"; - ele.classList.add("armed"); - Callbacks.push([remoteHideVideo, ele, false]); - log("video queued"); - return; - } else { - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - ele.innerHTML = ' Hide'; - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.innerHTML = ' Unhide'; - } - miniTranslate(ele); - ele.classList.remove("armed"); //ele.style.backgroundColor = null; - } - - var msg = {}; - if (ele.value == 0) { - msg.directVideoMuted = false; - } else { - msg.directVideoMuted = true; - } - - if (!skipSend) { - for (var i in session.pcs) { - msg.target = ele.dataset.UUID; - - if (i === msg.target) { - msg.target = true; - } - try { - session.pcs[i].sendChannel.send(JSON.stringify(msg)); - } catch (e) { } - } - syncDirectorState(ele); - } - - pokeIframeAPI("director-video-hide-state", msg.directVideoMuted, ele.dataset.UUID); - pokeAPI("directorVideoHide", msg.directVideoMuted, session.rpcs[ele.dataset.UUID].streamID); - - return msg.directVideoMuted; -} - -function remoteMuteVideo(ele, event = false, skipSend = false) { - log("video mute"); - - if (!event || event.ctrlKey || event.metaKey) { - //ele.children[1].innerHTML = getTranslation("armed"); - miniTranslate(ele.children[1], "armed"); - ele.classList.add("armed"); //ele.style.backgroundColor = "#BF3F3F"; - Callbacks.push([remoteMuteVideo, ele, false]); - log("video queued"); - return; - } else { - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - ele.innerHTML = ' Video off'; - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.innerHTML = ' Video on'; - } - miniTranslate(ele); - ele.classList.remove("armed"); - } - - var msg = {}; - if (ele.value == 0) { - msg.remoteVideoMuted = false; - } else { - msg.remoteVideoMuted = true; - } - - if (!skipSend) { - session.sendRequest(msg, ele.dataset.UUID); - syncDirectorState(ele); - } - - pokeIframeAPI("remote-video-mute-state", msg.remoteVideoMuted, ele.dataset.UUID); - pokeAPI("remoteVideoMuted", msg.remoteVideoMuted, session.rpcs[ele.dataset.UUID].streamID); - - return msg.remoteVideoMuted; -} -function updateDirectorVideoHide(UUID) { - var ele = document.querySelectorAll('[data-action-type="hide-guest"][data--u-u-i-d="' + UUID + '"]'); - if (ele[0]) { - ele[0].value = 1; - ele[0].classList.add("pressed"); - ele[0].ariaPressed = "true"; - ele[0].innerHTML = ' Unhide'; - miniTranslate(ele[0]); - } - return true; -} -function updateDirectorVideoMute(UUID) { - var ele = document.querySelectorAll('[data-action-type="mute-video-guest"][data--u-u-i-d="' + UUID + '"]'); - if (ele[0]) { - ele[0].value = 1; - ele[0].classList.add("pressed"); - ele[0].ariaPressed = "true"; - ele[0].innerHTML = ' Video on'; - miniTranslate(ele[0]); - } - return true; -} - -function directVolume(ele) { - // NOT USED ANYMORE - log("volume"); - var msg = {}; - msg.scene = true; - msg.action = "volume"; - msg.target = ele.dataset.sid; // i want to focus on the STREAM ID, not the UUID... - msg.value = ele.value; - - //for (var uuid in session.pcs){ - // if (session.pcs[uuid].stats.info && ("version" in session.pcs[uuid].stats.info) && (session.pcs[uuid].stats.info.version < 17.2)){ - // msg.request = "sendroom"; - // session.sendMsg(msg); - // return; - // } - //} - - for (var uuid in session.pcs) { - if (session.pcs[uuid].scene !== false) { - // send to all scenes (but scene = 0) - session.sendMessage(msg, uuid); - } - } - - syncDirectorState(ele); - return msg.value; -} - -function applyMuteState(UUID) { - // this is the mute state of PLAYBACK audio; not the microphone or outbound. - if (!(UUID in session.rpcs)) { - return "UUID not found"; - } - - var muteOutcome = session.rpcs[UUID].mutedState || session.rpcs[UUID].mutedStateMixer || session.rpcs[UUID].mutedStateScene || session.speakerMuted || session.rpcs[UUID].bandwidthMuted; - - if (session.pauseInvisible) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.isInvisible) { - muteOutcome = true; - } - } - - if (!muteOutcome && session.noaudio !== false) { - if (session.noaudio === true) { - muteOutcome = true; - } else if (session.noaudio.length) { - if (("streamID" in session.rpcs[UUID]) && session.rpcs[UUID].streamID && !session.noaudio.includes(session.rpcs[UUID].streamID)) { - muteOutcome = true; - } - } else { - muteOutcome = true; - } - } else if (!muteOutcome && session.excludeaudio) { - if (("streamID" in session.rpcs[UUID]) && session.rpcs[UUID].streamID && session.excludeaudio.includes(session.rpcs[UUID].streamID)) { - muteOutcome = true; - } - } - - if (session.rpcs[UUID].videoElement) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.usermuted === 1) { - return "usermuted 1"; - } - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.usermuted === 2) { - return "usermuted 2"; - } - session.rpcs[UUID].videoElement.muted = muteOutcome; - } - - // session.scene - return muteOutcome; -} - -function checkMuteState(UUID) { - // this is the mute state of PLAYBACK audio; not the microphone or outbound. - if (!(UUID in session.rpcs)) { - return false; - } - if (session.pauseInvisible) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.isInvisible) { - return true; - } - } - return session.rpcs[UUID].mutedState || session.rpcs[UUID].mutedStateMixer || session.rpcs[UUID].mutedStateScene || session.speakerMuted || session.rpcs[UUID].bandwidthMuted; -} - -var volumeLUT = [0, 1, 2, 2.4, 2.7, 3, 3.4, 3.7, 4, 4.5, 4.8, 5, 5.6, 6, 6.4, 6.8, 7, 7.7, 8, 8.6, 9, 9.5, 10, 10.4, 10.9, 11, 12, 12.5, 13, 13.6, 14, 14.7, 15, 15.6, 16, 17, 17.7, 18, 19, 19.7, 20, 21, 21.8, 22, 23, 24, 24.7, 25, 26, 27, 28, 28.5, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 48, 49, 51, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 67, 68, 70, 72, 74, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 100, 102, 104, 107, 109, 112, 114, 117, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 156, 159, 163, 166, 170, 174, 177, 181, 185, 189, 193, 198, 202, 206, 211, 215, 220, 225, 230, 234, 240, 245, 250, 255, 261, 266, 272, 278, 284, 290, 296, 302, 308, 315, 322, 328, 335, 342, 350, 357, 364, 372, 380, 388, 396, 404, 413, 421, 430, 439, 448, 458, 467, 477, 487, 497, 507, 518, 528, 539, 551, 562, 574, 586, 598, 610, 623, 635, 649, 662, 676, 690, 704, 718, 733, 748, 764, 779, 795, 812, 828]; - -function initAudioButtons(audioGain, UUID) { - if (audioGain === 0) { - var ele = document.querySelector('[data-action-type="mute-guest"][data--u-u-i-d="' + UUID + '"]'); - if (ele) { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - miniTranslate(ele.children[1], "unmute"); - session.rpcs[UUID].directorMutedState = 1; - } - pokeIframeAPI("director-mute-state", true, UUID); - } else { - var ele = document.querySelector('[data-action-type="volume"][data--u-u-i-d="' + UUID + '"]'); - if (ele) { - if (audioGain == 100) { - ele.value = audioGain; - } else { - audioGain = parseInt(audioGain) || 0; - ele.value = 200; - for (var i = 1; i <= 200; i++) { - if (volumeLUT[i] >= audioGain) { - ele.value = i; - break; - } - } - } - session.rpcs[UUID].directorVolumeState = audioGain; - remoteVolumeUI(ele); - } - } -} -function remoteVolumeUI(ele) { - var value = ele.value; - value = volumeLUT[parseInt(value)]; - - ele.nextElementSibling.innerHTML = value + "%"; - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - remoteVolume(ele); - } - //setVolumeColor(ele); - return value; -} - -function remoteVolume(ele) { - // A directing room only is controlled by the Director, with the exception of MUTE. - log("volume: " + session.rpcs[ele.dataset.UUID].directorMutedState); - var msg = {}; - var muted = session.rpcs[ele.dataset.UUID].directorMutedState; - - var value = ele.value; - value = volumeLUT[parseInt(value)]; - - //log(ele); - if (muted == true) { - // 1 is a string, not an int, so == and not ===. this happens in a few places :/ - session.rpcs[ele.dataset.UUID].directorVolumeState = value; - } else { - session.rpcs[ele.dataset.UUID].directorVolumeState = value; - msg.volume = value; - msg.UUID = ele.dataset.UUID; - session.sendRequest(msg, ele.dataset.UUID); - } - - //const minLog = Math.log10(0.01); // Log10 of minimum gain - // const maxLog = Math.log10(10); // Log10 of maximum gain - // const normalizedValue = (msg.volume / 75) * (maxLog - minLog) + minLog; - // const gainValue = Math.pow(10, normalizedValue); - //console.log(gainValue*100); - - pokeIframeAPI("director-volume-state", value, ele.dataset.UUID); - - syncDirectorState(ele); - return value; -} - -/* function setVolumeColor(ele){ - var vol1 = 200-parseInt(ele.value); - if (vol1<0){vol1=0}; - ele.style.backgroundColor = "hsl("+vol1+", 100%, 50%)"; -} */ - -function clearDirectorSettings() { - // make sure to wipe the director's room settings if creating a new room. - //console.warn("Clearing"); - removeStorage("directorCustomize"); - removeStorage("directorWebsiteShare"); -} - -function saveDirectorSettings() { - //console.warn("Saving"); - var settings = {}; - - if (getById("customizeLinks").classList.contains("hidden")) { - settings.customizeLinks = true; - } - - var customizeLinks1 = getById("customizeLinks1").querySelectorAll("input"); - settings.customizeLinks1 = {}; - for (var i = 0; i < customizeLinks1.length; i++) { - settings.customizeLinks1[customizeLinks1[i].dataset.param] = customizeLinks1[i].checked; - } - - var customizeLinks3 = getById("customizeLinks3").querySelectorAll("input"); - settings.customizeLinks3 = {}; - for (var i = 0; i < customizeLinks3.length; i++) { - settings.customizeLinks3[customizeLinks3[i].dataset.param] = customizeLinks3[i].checked; - } - - var directorLinks1 = getById("directorLinks1").querySelectorAll("input"); - settings.directorLinks1 = {}; - for (var i = 0; i < directorLinks1.length; i++) { - settings.directorLinks1[directorLinks1[i].dataset.param] = directorLinks1[i].checked; - } - - var directorLinks2 = getById("directorLinks2").querySelectorAll("input"); - settings.directorLinks2 = {}; - for (var i = 0; i < directorLinks2.length; i++) { - settings.directorLinks2[directorLinks2[i].dataset.param] = directorLinks2[i].checked; - } - setStorage("directorCustomize", settings); -} - -function loadDirectorSettings() { - //console.warn("LOAD DIRECTOR SETTING"); - var settings = getStorage("directorCustomize"); - if (!settings || typeof settings !== "object") { - return; - } - - if (settings.customizeLinks && !session.cleanDirector && !session.cleanOutput) { - try { - hideDirectorinvites(getById("directorLinksButton"), false); - } catch (e) { - errorlog(e); - } - } - - if (settings.customizeLinks1) { - var customizeLinks1 = getById("customizeLinks1"); - Object.keys(settings.customizeLinks1).forEach((key, index) => { - try { - if (settings.customizeLinks1[key]) { - customizeLinks1.querySelector('[data-param="' + key + '"]').checked = settings.customizeLinks1[key]; - customizeLinks1.querySelector('[data-param="' + key + '"]').onchange(); - } - } catch (e) { - errorlog(e); - } - }); - } - - if (settings.customizeLinks3) { - var customizeLinks3 = getById("customizeLinks3"); - Object.keys(settings.customizeLinks3).forEach((key, index) => { - try { - if (settings.customizeLinks3[key]) { - customizeLinks3.querySelector('[data-param="' + key + '"]').checked = settings.customizeLinks3[key]; - customizeLinks3.querySelector('[data-param="' + key + '"]').onchange(); - } - } catch (e) { - errorlog(e); - } - }); - } - - if (settings.directorLinks1) { - var directorLinks1 = getById("directorLinks1"); - Object.keys(settings.directorLinks1).forEach((key, index) => { - try { - if (key in settings.directorLinks1) { - directorLinks1.querySelector('[data-param="' + key + '"]').checked = settings.directorLinks1[key]; - directorLinks1.querySelector('[data-param="' + key + '"]').onchange(); - } - } catch (e) { - errorlog("key :" + key); - errorlog(e); - } - }); - } - - if (settings.directorLinks2) { - var directorLinks2 = getById("directorLinks2"); - Object.keys(settings.directorLinks2).forEach((key, index) => { - try { - if (key in settings.directorLinks2) { - directorLinks2.querySelector('[data-param="' + key + '"]').checked = settings.directorLinks2[key]; - directorLinks2.querySelector('[data-param="' + key + '"]').onchange(); - } - } catch (e) { - errorlog("key :" + key); - errorlog(e); - } - }); - } -} - -function sendChat(chatmessage = "hi", UUID = false, overlay = false) { - // A directing room only is controlled by the Director, with the exception of MUTE. - log("Chat message"); - var msg = {}; - msg.chat = chatmessage; - msg.overlay = overlay; - session.sendPeers(msg, UUID); - return true; -} - -// ===================== -// TIPPING FUNCTIONALITY -// ===================== - -// Initialize default tip settings -if (typeof session.receiveTips === 'undefined') session.receiveTips = false; -if (typeof session.tipId === 'undefined') session.tipId = null; -if (typeof session.tipsId === 'undefined') session.tipsId = null; // Overlay token for SSE -if (typeof session.tipServer === 'undefined') session.tipServer = "https://ninjabacker.com"; -if (typeof session.tipAmounts === 'undefined') session.tipAmounts = [5, 10, 25, 50, 100]; -if (typeof session.tipCurrency === 'undefined') session.tipCurrency = "USD"; -if (typeof session.tipEventSource === 'undefined') session.tipEventSource = null; -if (typeof session.tipStripe === 'undefined') session.tipStripe = null; - -// Cache for performer validation results -var tipPerformerCache = {}; - -// Validate that a performer has completed Stripe setup and can receive tips -async function validateTipPerformer(tipId, tipServer) { - if (!tipId) return false; - - tipServer = tipServer || session.tipServer || "https://ninjabacker.com"; - var cacheKey = tipServer + "/" + tipId; - - // Return cached result if available - if (cacheKey in tipPerformerCache) { - return tipPerformerCache[cacheKey]; - } - - try { - var response = await fetch(tipServer + "/v1/performer/" + tipId); - var isValid = response.ok; // 200 = performer exists and has charges_enabled - tipPerformerCache[cacheKey] = isValid; - return isValid; - } catch(e) { - // Network error - assume not valid, don't cache to allow retry - return false; - } -} - -// Add tip icon overlay to video container (two-way opt-in system) -async function addTipIconToVideo(UUID) { - if (!session.showTips || session.cleanOutput) return; - - var peer = session.rpcs[UUID] || session.pcs[UUID]; - if (!peer || !peer.acceptsTips) return; - - // Build tip page URL for QR code - only if performer has a registered tipId - var tipServer = peer.tipServer || session.tipServer || "https://ninjabacker.com"; - var tipId = peer.tipId; // Must be explicitly set - don't fall back to UUID - - // If no tipId, performer hasn't set up tipping properly - don't show icon - if (!tipId) return; - - // Validate performer has completed Stripe setup before showing tip icon - var isValidPerformer = await validateTipPerformer(tipId, tipServer); - if (!isValidPerformer) return; - - // Find video container - try different naming patterns - var videoContainer = document.getElementById("videoContainer_" + UUID); - var videoElement = document.getElementById("videosource_" + UUID); - if (!videoContainer) { - // Try finding the video element's parent - if (videoElement && videoElement.parentElement) { - videoContainer = videoElement.parentElement; - } - } - - // If container doesn't exist yet, try again later - if (!videoContainer) { - setTimeout(function() { - addTipIconToVideo(UUID); - }, 1000); - return; - } - - // Don't add duplicate icons - if (videoContainer.querySelector(".tipIconOverlay")) return; - - // Determine if we're in OBS or a scene/view link (not a guest/publisher) - var isOBS = !!window.obsstudio; - var isSceneOrView = session.scene !== false || session.view; // scene link or view parameter - - // QR code shown only if: - // 1. User explicitly set &tipqrsize, OR - // 2. In OBS studio, OR - // 3. It's a scene/view link (not a guest publisher page) - var showQRCode = false; - if (session.tipQRSize && session.tipQRSize !== 150) { // User explicitly set size - showQRCode = true; - } else if (isOBS || isSceneOrView) { - showQRCode = !session.noTipQR; // Can be disabled with ¬ipqr - } - - // Check video display size - only show QR if video is large enough (min 640x360) - if (showQRCode) { - var videoWidth = videoContainer.offsetWidth || (videoElement ? videoElement.offsetWidth : 0); - var videoHeight = videoContainer.offsetHeight || (videoElement ? videoElement.offsetHeight : 0); - var minWidthForQR = 640; - var minHeightForQR = 360; - if (videoWidth < minWidthForQR || videoHeight < minHeightForQR) { - showQRCode = false; - } - } - - var tipPageUrl = tipServer + "/" + (tipId || ""); - var qrSize = Math.max(session.tipQRSize || 150, 100); // Minimum 100px for scanability - - // Create container for heart + label + QR - var tipOverlay = document.createElement("div"); - tipOverlay.className = "tipIconOverlay"; - if (!showQRCode) { - tipOverlay.classList.add("noQR"); - } - if (isOBS) { - tipOverlay.classList.add("obsMode"); - } - tipOverlay.title = "Send a tip"; - tipOverlay.dataset.UUID = UUID; - - // Heart icon with dollar sign - var heartIcon = document.createElement("div"); - heartIcon.className = "tipHeart"; - heartIcon.innerHTML = '$'; - tipOverlay.appendChild(heartIcon); - - // "Send a Tip" label - var tipLabel = document.createElement("div"); - tipLabel.className = "tipLabel"; - tipLabel.textContent = "Send a Tip"; - tipOverlay.appendChild(tipLabel); - - // Only add QR code if appropriate - if (showQRCode) { - var qrContainer = document.createElement("div"); - qrContainer.className = "tipQR"; - qrContainer.style.width = qrSize + "px"; - qrContainer.style.height = qrSize + "px"; - - // Generate styled QR code - generateStyledTipQR(qrContainer, tipPageUrl, qrSize); - tipOverlay.appendChild(qrContainer); - } - - // Click handler - tipOverlay.onclick = function(e) { - e.stopPropagation(); - if (typeof openTipModal === 'function') { - openTipModal(this.dataset.UUID); - } - }; - - videoContainer.appendChild(tipOverlay); - - // Start animation based on mode - if (showQRCode && isOBS) { - // OBS mode: QR code with occasional "Send a Tip" text - startTipQRAnimation(tipOverlay, true); - } else if (showQRCode) { - // Scene/view mode: QR code with occasional "Send a Tip" text - startTipQRAnimation(tipOverlay, false); - } else { - // Guest mode: Heart with occasional "Send a Tip" label - startTipLabelAnimation(tipOverlay); - } -} - -// Animate between heart/label and QR code -// OBS mode: Show QR most of the time, occasionally show "Send a Tip" label -// Scene mode: Show QR periodically (every 45 seconds for 8 seconds) -function startTipQRAnimation(tipOverlay, isOBS) { - if (isOBS) { - // OBS: Start with QR showing, periodically show "Send a Tip" label - tipOverlay.classList.add("showQR"); - - function showLabel() { - tipOverlay.classList.remove("showQR"); - tipOverlay.classList.add("showLabel"); - // Show label for 6 seconds - setTimeout(function() { - if (tipOverlay && tipOverlay.parentElement) { - tipOverlay.classList.remove("showLabel"); - tipOverlay.classList.add("showQR"); - } - }, 6000); - } - - // Show label every 60 seconds - tipOverlay.qrInterval = setInterval(function() { - if (tipOverlay && tipOverlay.parentElement) { - showLabel(); - } - }, 60000); - - // First label after 20 seconds - setTimeout(function() { - if (tipOverlay && tipOverlay.parentElement) { - showLabel(); - } - }, 20000); - } else { - // Scene/view mode: Show heart normally, QR periodically - function showQR() { - tipOverlay.classList.add("showQR"); - // Show QR for 8 seconds - setTimeout(function() { - if (tipOverlay && tipOverlay.parentElement) { - tipOverlay.classList.remove("showQR"); - } - }, 8000); - } - - // Show QR every 45 seconds - tipOverlay.qrInterval = setInterval(function() { - if (tipOverlay && tipOverlay.parentElement) { - showQR(); - } - }, 45000); - - // First QR after 15 seconds - setTimeout(function() { - if (tipOverlay && tipOverlay.parentElement) { - showQR(); - } - }, 15000); - } -} - -// Animate "Send a Tip" label for guest/publisher mode (no QR) -function startTipLabelAnimation(tipOverlay) { - function showLabel() { - tipOverlay.classList.add("showLabel"); - // Show label for 5 seconds - setTimeout(function() { - if (tipOverlay && tipOverlay.parentElement) { - tipOverlay.classList.remove("showLabel"); - } - }, 5000); - } - - // Show label every 45 seconds - tipOverlay.labelInterval = setInterval(function() { - if (tipOverlay && tipOverlay.parentElement) { - showLabel(); - } - }, 45000); - - // First label after 10 seconds - setTimeout(function() { - if (tipOverlay && tipOverlay.parentElement) { - showLabel(); - } - }, 10000); -} - -// Clean up tip icon animation when video removed -function removeTipIconFromVideo(UUID) { - var tipOverlay = document.querySelector('.tipIconOverlay[data-uuid="' + UUID + '"]'); - if (tipOverlay) { - if (tipOverlay.qrInterval) { - clearInterval(tipOverlay.qrInterval); - } - if (tipOverlay.labelInterval) { - clearInterval(tipOverlay.labelInterval); - } - tipOverlay.remove(); - } -} - -// Generate QR code using built-in thirdparty/qrcode.min.js library -var tipQRPendingContainers = []; // Queue for containers waiting for library - -function generateStyledTipQR(container, url, size) { - // Use existing QRCode library from thirdparty/qrcode.min.js - if (window.QRCode) { - createTipQR(container, url, size); - } else { - // Queue this container and load library - tipQRPendingContainers.push({ container: container, url: url, size: size }); - loadQR(function() { - // Process all pending containers - tipQRPendingContainers.forEach(function(item) { - createTipQR(item.container, item.url, item.size); - }); - tipQRPendingContainers = []; - }); - } -} - -function createTipQR(container, url, size) { - try { - // Create inner div for QR code - var qrDiv = document.createElement("div"); - qrDiv.style.cssText = "width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#fff;border-radius:4px;"; - container.appendChild(qrDiv); - - var qrcode = new QRCode(qrDiv, { - width: size - 8, - height: size - 8, - colorDark: "#e53935", // Red color to match heart theme - colorLight: "#FFFFFF", - correctLevel: QRCode.CorrectLevel.H // High error correction for video compression - }); - qrcode.makeCode(url); - - // Remove default title - qrDiv.title = ""; - setTimeout(function() { - qrDiv.title = ""; - // Style the generated image - var imgs = qrDiv.getElementsByTagName("img"); - if (imgs.length) { - imgs[0].style.cursor = "pointer"; - imgs[0].style.margin = "auto"; - imgs[0].style.borderRadius = "4px"; - } - var canvas = qrDiv.getElementsByTagName("canvas"); - if (canvas.length) { - canvas[0].style.borderRadius = "4px"; - } - }, 100); - } catch (e) { - errorlog("QR code error:", e); - } -} - -// Show onboarding modal for first-time tip setup -function showTipOnboardingModal() { - // Check if already seen - if (getStorage("tipOnboardingSeen")) return; - - var tipServer = session.tipServer || "https://ninjabacker.com"; - - // If user already has tipsId set, they're fully configured - skip onboarding - if (session.tipsId) { - setStorage("tipOnboardingSeen", "true", 9999); - return; - } - - var step2Content = - '

    2. Enter Your Username

    ' + - '

    After registering, enter your username below:

    ' + - '
    ' + - '' + - '' + - '
    ' + - '

    Enter your registered username to enable tipping.

    '; - - var modalHTML = - '
    ' + - '
    ' + - '×' + - '

    Tipping Setup

    ' + - '

    To receive tips, follow these steps:

    ' + - - '

    1. Register Your Account

    ' + - '

    Create a username and connect your Stripe account:

    ' + - '' + - 'Register & Create Username' + - '' + - - step2Content + - - '

    3. Viewer Setup

    ' + - '

    Viewers need &showtips in their URL to see tip buttons.

    ' + - - '

    4. QR Code Feature

    ' + - '

    A scannable QR code will appear on your video periodically. Use &notipqr to disable, or &tipqrsize=200 to resize.

    ' + - - '
    ' + - '' + - '' + - '
    ' + - '
    ' + - '
    ' + - '
    '; - - document.body.insertAdjacentHTML("beforeend", modalHTML); -} - -function closeTipOnboarding(permanent) { - if (permanent) { - setStorage("tipOnboardingSeen", "true", 9999); // Never show again - } else { - setStorage("tipOnboardingSeen", "true", 1); // Show again in 1 day - } - var modal = document.getElementById("tipOnboardingModal"); - var backdrop = document.getElementById("tipOnboardingBackdrop"); - if (modal) modal.remove(); - if (backdrop) backdrop.remove(); -} - -async function applyTipUsername() { - var input = document.getElementById("tipUsernameInput"); - if (!input) return; - - var username = input.value.trim().toLowerCase(); - if (!username) { - warnUser("Please enter a username"); - return; - } - - // Validate username format (alphanumeric, underscore, hyphen, 3-30 chars) - if (!/^[a-z0-9_-]{3,30}$/.test(username)) { - warnUser("Username must be 3-30 characters (letters, numbers, _ or -)"); - return; - } - - // Fetch performer info from API to get overlay token - var tipServer = session.tipServer || "https://ninjabacker.com"; - try { - var response = await fetch(tipServer + "/v1/performer/" + username); - if (!response.ok) { - warnUser("Username not found or not registered for tips"); - return; - } - var data = await response.json(); - if (!data.overlay_token) { - warnUser("Performer account not fully set up"); - return; - } - - // Build new URL with tipsid parameter (overlay token) - var url = new URL(window.location.href); - url.searchParams.delete("tip"); - url.searchParams.delete("tips"); - url.searchParams.delete("tipid"); - url.searchParams.set("tipsid", data.overlay_token); - - // Reload with new parameter - window.location.href = url.toString(); - } catch(e) { - errorlog("Failed to lookup performer:", e); - warnUser("Failed to verify username. Please try again."); - } -} - -// Get currency symbol helper -function getTipCurrencySymbol(currency) { - var symbols = { USD: "$", EUR: "\u20AC", GBP: "\u00A3", CAD: "C$", AUD: "A$", JPY: "\u00A5" }; - return symbols[currency] || currency + " "; -} - -// Show on-screen tip banner (performer only) -function showTipBanner(tipData) { - var currencySymbol = getTipCurrencySymbol(tipData.currency || "USD"); - var fromLabel = sanitizeLabel(tipData.fromLabel || tipData.from || "Anonymous"); - var amount = tipData.amount; - - // Create banner element - var banner = document.createElement("div"); - banner.className = "tipBanner"; - banner.innerHTML = currencySymbol + amount + " tip from " + fromLabel; - if (tipData.message) { - banner.innerHTML += '
    "' + sanitizeChat(tipData.message) + '"
    '; - } - - document.body.appendChild(banner); - - // Trigger animation - setTimeout(function() { - banner.classList.add("tipBannerShow"); - }, 10); - - // Remove after 5 seconds - setTimeout(function() { - banner.classList.remove("tipBannerShow"); - banner.classList.add("tipBannerHide"); - setTimeout(function() { - banner.remove(); - }, 500); // Wait for fade out animation - }, 5000); -} - -// Process incoming tip message -function processTipMessage(tipData, UUID) { - log("Tip received:", tipData); - - var currencySymbol = getTipCurrencySymbol(tipData.currency || "USD"); - var fromLabel = sanitizeLabel(tipData.fromLabel || tipData.from || "Anonymous"); - var message = tipData.message ? sanitizeChat(tipData.message) : ""; - - // Plain text version for notification - var notifyMsg = currencySymbol + tipData.amount + " tip from " + fromLabel; - if (message) { - notifyMsg += ': "' + message + '"'; - } - - var data = { - time: Date.now(), - type: "tip", - msg: notifyMsg, - label: "Tip" - }; - - messageList.push(data); - messageList = messageList.slice(-100); - - // Play notification sound - if (session.beepToNotify) { - playtone(); - showNotification("Tip received", notifyMsg); - } - - updateMessages(); - - // Show on-screen banner only for SSE-received tips (performer's own tips) - if (UUID === null) { - showTipBanner(tipData); - } - - // Chat notification (red dot) when chat is closed - if (session.chat == false) { - getById("chattoggle").className = "las la-comments toggleSize pulsate"; - getById("chatbutton").className = "float"; - - if (getById("chatNotification").value) { - getById("chatNotification").value = getById("chatNotification").value + 1; - } else { - getById("chatNotification").value = 1; - } - getById("chatNotification").classList.add("notification", "red"); - } - - // Broadcast to popout chat window - if (session.broadcastChannel !== false) { - session.broadcastChannel.postMessage(data); - } - - // Browser notification - if (Notification.permission === "granted") { - try { - new Notification("Tip Received!", { - body: notifyMsg, - icon: "./media/logo.png" - }); - } catch(e) {} - } - - // API callback - if (typeof pokeAPI === 'function') { - pokeAPI("tip", tipData); - } - - // Iframe postMessage - if (isIFrame && session.iframetarget) { - try { - parent.postMessage({ action: "tip", value: tipData }, session.iframetarget); - } catch(e) {} - } -} - -// Initialize SSE connection for tip notifications -function initTipNotifications() { - if (!session.receiveTips) return; - if (session.tipEventSource) return; // Already connected - - // Use tipsId (overlay token) for SSE subscription, fallback to streamID for legacy - var subscribeId = session.tipsId || session.streamID; - if (!subscribeId) return; - - var tipServer = session.tipServer || "https://ninjabacker.com"; - var sseURL = tipServer + "/v1/subscribe/" + subscribeId; - - try { - session.tipEventSource = new EventSource(sseURL); - - session.tipEventSource.onmessage = function(event) { - try { - var tipData = JSON.parse(event.data); - if (tipData.type === "tip") { - processTipMessage(tipData, null); - // Broadcast to room peers - broadcastTipReceived(tipData); - } - } catch(e) { - errorlog("Tip SSE parse error:", e); - } - }; - - session.tipEventSource.onerror = function(err) { - warnlog("Tip SSE connection error, will retry..."); - }; - - log("Tip notifications initialized for: " + subscribeId); - } catch(e) { - errorlog("Failed to initialize tip notifications:", e); - } -} - -// Fetch performer info (username) from tipsId token and set session.tipId -async function fetchPerformerFromToken() { - if (!session.tipsId) return; - if (session.tipId) return; // Already have username - - var tipServer = session.tipServer || "https://ninjabacker.com"; - try { - var response = await fetch(tipServer + "/v1/performer/" + session.tipsId); - if (response.ok) { - var data = await response.json(); - if (data.username) { - session.tipId = data.username; - log("Performer username from token: " + data.username); - } - } - } catch(e) { - errorlog("Failed to fetch performer from token:", e); - } -} - -// Close SSE connection -function closeTipNotifications() { - if (session.tipEventSource) { - session.tipEventSource.close(); - session.tipEventSource = null; - } -} - -// Broadcast tip received to all peers (so viewers see it too) -function broadcastTipReceived(tipData) { - var msg = { tip: tipData }; - session.sendPeers(msg); -} - -// Open tip modal for a peer -function openTipModal(UUID) { - var peer = session.rpcs[UUID] || session.pcs[UUID]; - if (!peer || !peer.acceptsTips) { - warnUser("This user does not accept tips"); - return; - } - - var peerLabel = sanitizeLabel(peer.tipId || peer.label || peer.streamID || "Performer"); - var amounts = peer.tipAmounts || session.tipAmounts || [5, 10, 25, 50, 100]; - var currency = peer.tipCurrency || session.tipCurrency || "USD"; - var currencySymbol = getTipCurrencySymbol(currency); - - var modalID = "tipModal_" + UUID; - var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; - - var amountButtons = amounts.map(function(amt) { - return ''; - }).join(''); - - var modalTemplate = - '
    ' + - '
    ' + - '' + - '
    ' + - '

    \uD83D\uDCB0 Send a tip to ' + peerLabel + '

    ' + - '
    ' + - '
    ' + amountButtons + '
    ' + - '
    ' + - '' + - '' + - '
    ' + - '
    ' + - '' + - '' + - '
    ' + - '
    ' + - '' + - '' + - '
    ' + - '
    ' + - '' + - '
    ' + - '
    ' + - '
    ' + - 'Powered by' + - '' + - '
    ' + - '
    ' + - 'Selected: ' + currencySymbol + '0' + - '
    ' + - '' + - '' + - '
    ' + - '
    '; - - document.body.insertAdjacentHTML("beforeend", modalTemplate); - - // Initialize Stripe Elements - initTipStripeElements(UUID, peer); -} - -// Load Stripe.js dynamically if not already loaded -function loadStripeJS() { - return new Promise(function(resolve, reject) { - if (typeof Stripe !== 'undefined') { - resolve(); - return; - } - var script = document.createElement('script'); - script.src = 'https://js.stripe.com/v3/'; - script.onload = resolve; - script.onerror = function() { reject(new Error('Failed to load Stripe.js')); }; - document.head.appendChild(script); - }); -} - -// Initialize Stripe Elements for tip modal -async function initTipStripeElements(UUID, peer) { - peer = peer || {}; - var tipServer = peer.tipServer || session.tipServer || "https://ninjabacker.com"; - - try { - // Load Stripe.js if needed - await loadStripeJS(); - - // Get performer-specific Stripe publishable key (supports test/live mode per account) - var performerId = peer.tipId || peer.tipsId || peer.streamID; - var stripeKey = null; - - if (performerId) { - try { - var perfResponse = await fetch(tipServer + "/v1/performer/" + performerId); - if (perfResponse.ok) { - var perfData = await perfResponse.json(); - stripeKey = perfData.stripePublishableKey; - } - } catch(e) { - // Fall back to config endpoint - } - } - - // Fallback to global config if performer-specific key not available - if (!stripeKey) { - var response = await fetch(tipServer + "/v1/config"); - var config = await response.json(); - stripeKey = config.stripePublishableKey; - } - - if (!stripeKey) { - document.getElementById("tipError_" + UUID).textContent = "Tipping not configured"; - document.getElementById("tipError_" + UUID).classList.remove("hidden"); - return; - } - - // Initialize Stripe with performer-specific key - var stripe = Stripe(stripeKey); - var elements = stripe.elements(); - - var cardElement = elements.create('card', { - hidePostalCode: true, - style: { - base: { - color: '#ffffff', - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - fontSize: '16px', - '::placeholder': { color: '#a0a0a0' } - } - } - }); - - cardElement.mount('#tipCardElement_' + UUID); - - // Store for later use - if (!window.tipStripeElements) window.tipStripeElements = {}; - window.tipStripeElements[UUID] = { stripe: stripe, cardElement: cardElement }; - - cardElement.on('change', function(event) { - var amount = window.tipStripeElements[UUID].selectedAmount || 0; - document.getElementById("tipConfirmBtn_" + UUID).disabled = !event.complete || amount <= 0; - }); - - } catch(e) { - errorlog("Failed to init Stripe:", e); - document.getElementById("tipError_" + UUID).textContent = "Failed to load payment form"; - document.getElementById("tipError_" + UUID).classList.remove("hidden"); - } -} - -// Select a predefined tip amount -function selectTipAmount(btn, UUID) { - document.querySelectorAll('#tipModal_' + UUID + ' .tipAmountBtn').forEach(function(b) { - b.classList.remove('selected'); - }); - btn.classList.add('selected'); - - var amount = parseFloat(btn.dataset.amount); - var peer = session.rpcs[UUID] || session.pcs[UUID]; - var currency = peer?.tipCurrency || session.tipCurrency || "USD"; - var currencySymbol = getTipCurrencySymbol(currency); - - document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + amount.toFixed(2); - document.getElementById('tipCustomInput_' + UUID).value = ""; - - if (window.tipStripeElements && window.tipStripeElements[UUID]) { - window.tipStripeElements[UUID].selectedAmount = amount; - } - - // Enable button if card is ready - checkTipButtonState(UUID); -} - -// Handle custom amount input -function customTipAmount(UUID, value) { - var amount = parseFloat(value); - var peer = session.rpcs[UUID] || session.pcs[UUID]; - var currency = peer?.tipCurrency || session.tipCurrency || "USD"; - var currencySymbol = getTipCurrencySymbol(currency); - - // Deselect preset buttons - document.querySelectorAll('#tipModal_' + UUID + ' .tipAmountBtn').forEach(function(b) { - b.classList.remove('selected'); - }); - - if (amount > 0) { - document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + amount.toFixed(2); - if (window.tipStripeElements && window.tipStripeElements[UUID]) { - window.tipStripeElements[UUID].selectedAmount = amount; - } - } else { - document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + "0"; - if (window.tipStripeElements && window.tipStripeElements[UUID]) { - window.tipStripeElements[UUID].selectedAmount = 0; - } - } - - checkTipButtonState(UUID); -} - -// Check if tip button should be enabled -function checkTipButtonState(UUID) { - if (!window.tipStripeElements || !window.tipStripeElements[UUID]) return; - var amount = window.tipStripeElements[UUID].selectedAmount || 0; - // Button state is also controlled by Stripe card element change event -} - -// Confirm and process tip payment -async function confirmTip(UUID) { - if (!window.tipStripeElements || !window.tipStripeElements[UUID]) return; - - var stripeData = window.tipStripeElements[UUID]; - var peer = session.rpcs[UUID] || session.pcs[UUID]; - - if (!peer || !stripeData.selectedAmount || stripeData.selectedAmount <= 0) { - return; - } - - var tipServer = peer.tipServer || session.tipServer || "https://ninjabacker.com"; - var amount = stripeData.selectedAmount; - var currency = peer.tipCurrency || session.tipCurrency || "USD"; - var tipperName = document.getElementById('tipName_' + UUID)?.value || "Anonymous"; - var message = document.getElementById('tipMessageInput_' + UUID)?.value || ""; - var performerUsername = peer.tipId || peer.streamID; - - var submitBtn = document.getElementById('tipConfirmBtn_' + UUID); - var errorEl = document.getElementById('tipError_' + UUID); - - submitBtn.disabled = true; - submitBtn.textContent = "Processing..."; - errorEl.classList.add('hidden'); - - try { - // Create PaymentIntent - var intentResponse = await fetch(tipServer + "/v1/tip/intent", { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - amount: amount, - currency: currency, - performerUsername: performerUsername, - tipperName: tipperName, - message: message - }) - }); - - if (!intentResponse.ok) { - var errData = await intentResponse.json(); - throw new Error(errData.error || 'Failed to create payment'); - } - - var intentData = await intentResponse.json(); - - // Confirm payment with Stripe - var result = await stripeData.stripe.confirmCardPayment(intentData.clientSecret, { - payment_method: { card: stripeData.cardElement } - }); - - if (result.error) { - throw new Error(result.error.message); - } - - if (result.paymentIntent.status === 'succeeded') { - // Confirm with backend - await fetch(tipServer + "/v1/tip/confirm", { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tipId: intentData.tipId, - paymentIntentId: result.paymentIntent.id - }) - }); - - // Success! - closeTipModal('tipModal_' + UUID, UUID); - warnUser("Tip sent successfully! Thank you!", 3000); - } - - } catch(e) { - errorlog("Tip payment error:", e); - errorEl.textContent = e.message; - errorEl.classList.remove('hidden'); - submitBtn.disabled = false; - submitBtn.textContent = "Send Tip"; - } -} - -// Close tip modal -function closeTipModal(modalID, UUID) { - var modal = document.getElementById(modalID); - if (modal) modal.remove(); - - // Cleanup Stripe elements - if (window.tipStripeElements && window.tipStripeElements[UUID]) { - if (window.tipStripeElements[UUID].cardElement) { - window.tipStripeElements[UUID].cardElement.destroy(); - } - delete window.tipStripeElements[UUID]; - } -} - -// ===================== -// END TIPPING FUNCTIONALITY -// ===================== - -var activatedStream = false; - -async function publishScreen() { - if (activatedStream == true) { - return; - } - activatedStream = true; - setTimeout(function () { - activatedStream = false; - }, 1000); - - formSubmitting = false; - - var quality = 0; - - if (document.getElementById("webcamquality2")) { - quality = parseInt(document.getElementById("webcamquality2").elements.namedItem("resolution2").value) || 0; - } - - session.quality_ss = quality; - - if (session.quality !== false) { - quality = session.quality; // override the user's setting - } - - if (session.screensharequality !== false) { - quality = session.screensharequality; - } - - var video = {}; - - if (quality == -1) { - // unlocked capture resolution - } else if (quality == -2) { - video.width = { - ideal: 3840 - }; - video.height = { - ideal: 2160 - }; - } else if (quality == -3) { - video.width = { - ideal: 2560 - }; - video.height = { - ideal: 1440 - }; - } else if (quality == 0) { - video.width = { - ideal: 1920 - }; - video.height = { - ideal: 1080 - }; - } else if (quality == 1) { - video.width = { - ideal: 1280 - }; - video.height = { - ideal: 720 - }; - } else if (quality == 2) { - video.width = { - ideal: 640 - }; - video.height = { - ideal: 360 - }; - } else if (quality >= 3) { - // lowest - video.width = { - ideal: 320 - }; - video.height = { - ideal: 180 - }; - } else { - video.width = { - min: 640 - }; - video.height = { - min: 360 - }; - } - - if (session.width) { - video.width = { - ideal: session.width - }; - } - if (session.height) { - video.height = { - ideal: session.height - }; - } - - var constraints = { - audio: { - echoCancellation: false, - autoGainControl: false, - noiseSuppression: false - }, - video: video - }; - - if (session.noiseSuppression === true) { - constraints.audio.noiseSuppression = true; // the defaults for screen publishing should be off. - } - if (session.autoGainControl === true) { - constraints.audio.autoGainControl = true; // the defaults for screen publishing should be off. - } - if (session.echoCancellation === true) { - constraints.audio.echoCancellation = true; // the defaults for screen publishing should be off. - } - if (session.voiceIsolation === true) { - constraint.audio.voiceIsolation = true; - } - try { - let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); - if (supportedConstraints.cursor) { - if (session.screensharecursor) { - constraints.video.cursor = ["always", "motion"]; - } else { - constraints.video.cursor = "never"; - } - } - if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) { - constraints.audio.suppressLocalAudioPlayback = true; - } - // - if (session.preferCurrentTab) { - constraints.preferCurrentTab = true; - } - if (session.selfBrowserSurface) { - constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include - } - if (session.surfaceSwitching) { - constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include - } - if (session.systemAudio) { - constraints.systemAudio = session.systemAudio; // exclude or include - } - if (session.displaySurface && supportedConstraints.displaySurface) { - constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser - } - } catch (e) { - warnlog("navigator.mediaDevices.getSupportedConstraints() not supported"); - } - - var overrideFramerate = false; - if (session.screensharefps !== false) { - constraints.video.frameRate = { - ideal: session.screensharefps, - max: session.screensharefps - }; - } else if (session.frameRate !== false && session.maxframeRate != false) { - overrideFramerate = session.frameRate; - constraints.video.frameRate = { - ideal: session.maxframeRate, - max: session.maxframeRate - }; - } else if (session.frameRate !== false) { - constraints.video.frameRate = session.frameRate; - } else if (session.maxframeRate != false) { - constraints.video.frameRate = { - ideal: session.maxframeRate, - max: session.maxframeRate - }; - } else { - constraints.video.frameRate = { - ideal: 60 - }; - } - - var outputSelect = getById("outputSourceScreenshare"); - - try { - session.sink = outputSelect.options[outputSelect.selectedIndex].value; // will probably fail on Safari. - log("Session Sink: " + session.sink); - saveSettings(); - } catch (e) { - warnlog(e); - } - - session.audioDevice = selectedScreenShareAudioDevices; - - return await publishScreen2(constraints, selectedScreenShareAudioDevices, true, overrideFramerate) - .then(res => { - if (res == false) { - return; - } // no screen selected - log("streamID is: " + session.streamID); - if (session.transcript) { - setTimeout(function () { - setupClosedCaptions(); - }, 1000); - } - - if (!session.cleanOutput && !session.cleanViewer) { - getById("mutebutton").classList.remove("hidden"); - getById("mutespeakerbutton").classList.remove("hidden"); - //getById("mutespeakerbutton").className="float"; - getById("chatbutton").className = "float"; - getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. - getById("mutevideobutton").className = "float"; - getById("hangupbutton").className = "float"; - if (session.showSettings) { - getById("settingsbutton").className = "float"; - } - if (session.raisehands) { - getById("raisehandbutton").className = "float"; - } - if (session.pptControls) { - getById("pptbackbutton").classList.remove("hidden"); - getById("pptnextbutton").classList.remove("hidden"); - } - if (session.recordLocal !== false) { - getById("recordLocalbutton").classList.remove("hidden"); - } - if (session.screensharebutton) { - getById("screensharebutton").className = "float"; - } - getById("controlButtons").classList.remove("hidden"); - // getById("legal").classList.remove("hidden"); - //getById("helpbutton").style.display = "inherit"; - //getById("reportbutton").style.display = ""; - } else if (session.cleanish && session.recordLocal !== false) { - getById("recordLocalbutton").classList.remove("hidden"); - getById("mutebutton").classList.add("hidden"); - getById("mutespeakerbutton").classList.add("hidden"); - getById("chatbutton").classList.add("hidden"); - getById("mutevideobutton").classList.add("hidden"); - getById("hangupbutton").classList.add("hidden"); - getById("hangupbutton2").classList.add("hidden"); - getById("controlButtons").classList.remove("hidden"); - getById("settingsbutton").classList.add("hidden"); - getById("screenshare2button").classList.add("hidden"); - getById("screensharebutton").classList.add("hidden"); - getById("screenshare3button").classList.add("hidden"); - getById("queuebutton").classList.add("hidden"); - } else { - getById("controlButtons").classList.add("hidden"); - } - - if (session.chatbutton === true) { - getById("chatbutton").classList.remove("hidden"); - getById("controlButtons").classList.remove("hidden"); - } else if (session.chatbutton === false) { - getById("chatbutton").classList.add("hidden"); - } - - if (session.screensharebutton === true) { - getById("controlButtons").classList.remove("hidden"); - getById("screensharebutton").className = "float"; - } - - if (session.hangupbutton === true) { - getById("controlButtons").classList.remove("hidden"); - getById("hangupbutton").className = "float"; - } - - getById("head1").className = "hidden"; - getById("head2").className = "hidden"; - - return res; - }) - .catch(() => { }); -} - -function getWidth() { - return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth, document.documentElement.clientWidth); -} - -function getHeight() { - return Math.max(document.documentElement.clientHeight); -} - -function updateForceRotate(skipLastBit = false) { - var capabilities = { facingMode: "unknown" }; - var FirefoxSucks = false; - - if (session.orientation) { - try { - var track = false; - if (session.streamSrc) { - var tracks = session.streamSrc.getVideoTracks(); - if (tracks.length) { - track = tracks[0]; - } - } - if (!track) { - return; - } - - const settings = track.getSettings(); - session.currentCameraConstraints = settings; - - if (screen && screen.orientation && screen.orientation.type) { - if (!screen.orientation.type.includes("portrait")) { - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - } else if (!window.matchMedia("(orientation: portrait)").matches) { - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - - session.forceRotate = 0; - - if (track.getCapabilities) { - capabilities = track.getCapabilities(); // firefox sucks? - - if ("width" in settings) { - if ("height" in settings) { - if (settings.width < settings.height) { - if (session.orientation == "landscape") { - if (capabilities) { - if (capabilities.facingMode == "environment") { - session.forceRotate = 270; - } else { - session.forceRotate = 90; - } - } - } else { - session.forceRotate = 0; - } - } else if (settings.width > settings.height) { - if (session.orientation == "portrait") { - if (capabilities) { - if (capabilities.facingMode == "environment") { - session.forceRotate = 90; - } else { - session.forceRotate = 270; - } - } - } else { - session.forceRotate = 0; - } - } else { - session.forceRotate = 0; - } - } else { - return; - } - } - } else if (Firefox && session.mobile) { - // firefox sucks... - try { - var vs1 = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3"); - if (vs1) { - vs1 = vs1.options[vs1.selectedIndex].textContent; - if (vs1.includes(" back")) { - capabilities = { facingMode: "environment" }; - FirefoxSucks = 2; - } else if (vs1.includes(" front")) { - capabilities = { facingMode: "user" }; - FirefoxSucks = 1; - } - } - getById("videosource").style.transform = ""; - if (session.orientation == "landscape") { - if (FirefoxSucks === 1) { - session.forceRotate = 90; - // video needs to be flipped - getById("videosource").style.transform = "rotate(90deg)"; - } else if (FirefoxSucks === 2) { - getById("videosource").style.transform = "rotate(270deg)"; - session.forceRotate = 270; - } - } - } catch (e) { - errorlog(e); - } - } else { - return; - } - - var msg = {}; - msg.rotate_video = 0; - if (session.forceRotate !== false) { - if (session.rotate) { - msg.rotate_video = session.forceRotate + parseInt(session.rotate); - } else { - msg.rotate_video = session.forceRotate; - } - } else { - msg.rotate_video = parseInt(session.rotate) || 0; - } - - if (msg.rotate_video && msg.rotate_video >= 360) { - msg.rotate_video -= 360; - } - - for (var UUID in session.pcs) { - try { - if (session.pcs[UUID].rotation != msg.rotate_video) { - // 0 == false will skip I think - session.pcs[UUID].rotation = msg.rotate_video; - session.sendMessage(msg, UUID); - //log("sending updated rotation info"); - } - } catch (e) { - errorlog(e); - if (session.pcs[UUID].startTime + 100000 < Date.now()) { - warnlog("RTC Connection seems to be dead or not yet open? 8"); - } else { - log("RTC Connection seems to be dead or not yet open? 8"); - } - } - } - } catch (e) { - errorlog(e); - } - - if (!(Firefox && session.mobile)) { - updateForceRotatedCSS(); - } - } else if (Firefox && session.mobile) { - try { - var vs1 = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3"); - if (vs1) { - vs1 = vs1.options[vs1.selectedIndex].textContent; - if (vs1.includes(" back")) { - FirefoxSucks = 2; - } else if (vs1.includes(" front")) { - FirefoxSucks = 1; - } - } - - if (screen && screen.orientation && screen.orientation.type) { - if (screen.orientation.type.includes("portrait")) { - session.forceRotate = 0; - } else if (screen.orientation.type.includes("landscape")) { - if (FirefoxSucks === 1) { - session.forceRotate = 90; - } else if (FirefoxSucks === 2) { - session.forceRotate = 270; - } - } - } else if (window.matchMedia("(orientation: portrait)").matches) { - // legacy support; it seems to update late, 100ms or so after screen.orientation, so lets not use it - session.forceRotate = 0; - } else if (window.matchMedia("(orientation: landscape)").matches) { - if (FirefoxSucks === 1) { - session.forceRotate = 90; - } else if (FirefoxSucks === 2) { - session.forceRotate = 270; - } - } - - var msg = {}; - msg.rotate_video = 0; - if (session.forceRotate !== false) { - if (session.rotate) { - msg.rotate_video = session.forceRotate + parseInt(session.rotate); - } else { - msg.rotate_video = session.forceRotate; - } - if (msg.rotate_video && msg.rotate_video >= 360) { - msg.rotate_video -= 360; - } - //warnlog("FIREFOX MOBILE ONLY ROTATE: "+msg.rotate_video); - //session.sendMessage(msg); - } else { - msg.rotate_video = parseInt(session.rotate) || 0; - - if (msg.rotate_video && msg.rotate_video >= 360) { - msg.rotate_video -= 360; - } - //warnlog("FIREFOX MOBILE ONLY ROTATE: "+msg.rotate_video); - } - for (var UUID in session.pcs) { - try { - if (session.pcs[UUID].rotation != msg.rotate_video) { - session.pcs[UUID].rotation = msg.rotate_video; - session.sendMessage(msg, UUID); - //log("sending updated rotation info"); - } - } catch (e) { - errorlog(e); - if (session.pcs[UUID].startTime + 100000 < Date.now()) { - warnlog("RTC Connection seems to be dead or not yet open? 8"); - } else { - log("RTC Connection seems to be dead or not yet open? 8"); - } - } - } - } catch (e) { - errorlog(e); - } - } else { - var msg = {}; - msg.rotate_video = 0; - if (session.forceRotate !== false) { - if (session.rotate) { - msg.rotate_video = session.forceRotate + parseInt(session.rotate); - } else { - msg.rotate_video = session.forceRotate; - } - } else { - msg.rotate_video = parseInt(session.rotate) || 0; - } - - if (msg.rotate_video && msg.rotate_video >= 360) { - msg.rotate_video -= 360; - } - - for (var UUID in session.pcs) { - try { - if (session.pcs[UUID].rotation != msg.rotate_video) { - session.pcs[UUID].rotation = msg.rotate_video; - session.sendMessage(msg, UUID); - //log("sending updated rotation info"); - } - } catch (e) { - errorlog(e); - if (session.pcs[UUID].startTime + 100000 < Date.now()) { - warnlog("RTC Connection seems to be dead or not yet open? 8"); - } else { - log("RTC Connection seems to be dead or not yet open? 8"); - } - } - } - } - if (!skipLastBit) { - applyMirror(session.mirrorExclude); - session.setResolution(); // probably only triggers with mobile devices? - } -} - -function updateForceRotatedCSS(rotateThis = session.forceRotate) { - if (rotateThis == 270) { - document.body.setAttribute("style", "transform: rotate(270deg);position: absolute;top: 100vh;left: 0;height: 100vw;width: 100vh;transform-origin: 0 0;"); - document.body.dataset.rotated = "1"; - } else if (rotateThis == 90) { - document.body.setAttribute("style", "transform: rotate(90deg);position: absolute;top: 0;left: 100vw;height: 100vw;width: 100vh;transform-origin: 0 0;"); - document.body.dataset.rotated = "1"; - } else if (rotateThis == 180) { - document.body.setAttribute("style", "transform: rotate(180deg);position: absolute;top: 100vh;left: 100vw;height: 100vh;width: 100vw;transform-origin: 0 0;"); - document.body.dataset.rotated = ""; - } else { - document.body.setAttribute("style", ""); - document.body.dataset.rotated = ""; - } -} - -async function joinDataMode() { - // join the room, but without publishing anything. - await session.connect(); - if (session.roomid) { - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - joinRoom(session.roomid); - } else if (session.view) { - window.onresize = updateMixer; - play(); - if (session.permaid !== false) { - session.postPublish(); - } - } else if (session.permaid !== false) { - session.postPublish(); - } -} - -function publishWebcam(btn = false, miconly = false) { - if (btn) { - if (btn.dataset.ready == "false") { - warnlog("Clicked too quickly; button not enabled yet"); - return; - } - - if (getById("passwordBasicInput").value.length) { - session.password = getById("passwordBasicInput").value; - session.password = sanitizePassword(session.password); - if (session.password.length == 0) { - session.password = false; - } else { - session.defaultPassword = false; - if (urlParams.has("pass")) { - updateURL("pass=" + session.password); - } else if (urlParams.has("pw")) { - updateURL("pw=" + session.password); - } else if (urlParams.has("p")) { - updateURL("p=" + session.password); - } else { - updateURL("password=" + session.password); - } - } - } - } - - if (activatedStream == true) { - return; - } - activatedStream = true; - log("PRESSED PUBLISH WEBCAM!!"); - - formSubmitting = false; - window.scrollTo(0, 0); // iOS has a nasty habit of overriding the CSS when changing camaera selections, so this addresses that. - - getById("head2").className = "hidden"; - - if (session.mobile && !session.roomid && session.permaid === false) { - if (!getById("rememberStreamID").classList.contains("hidden")) { - if (getById("rememberStreamIDcheck").checked) { - session.streamID = getStorage("permaid") || session.streamID; - setStorage("permaid", session.streamID, 99999); // ~ 13 months? - setStorage("rememberStreamIDmobile", "true", 99999); - } else { - removeStorage("permaid"); - setStorage("rememberStreamIDmobile", "false", 99999); - } - } - } - - if (session.roomid !== false) { - // they are in a room or a faux room - - window.onresize = updateMixer; - window.onorientationchange = function () { - if (Firefox) { - updateForceRotate(true); - } - setTimeout(async function () { - if (session.forceAspectRatio) { - await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - } - if (session.effect && session.effect === "7") { - digitalZoom(); // true needed to restart or start - } - updateForceRotate(); - updateMixer(); - }, 200); - }; - - if (session.roomid === "" && (!session.view || session.view === "")) { - // no room, no viewing, viewing disabled - if (session.manual === null) { - session.manual = session.manual === null ? true : session.manual; - } - if (!session.cleanOutput) { - var showReshare = getStorage("showReshare"); - if (showReshare) { - generateHash(session.streamID + session.salt + "bca321", 4) - .then(function (hash) { - // million to one error. - if (showReshare === hash) { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - } else if (session.permaid === null) { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - } - }) - .catch(errorlog); - } - } - } else { - log("ROOM ID ENABLED"); - log("Update Mixer Event on REsize SET"); - getById("main").style.overflow = "hidden"; - //session.cbr=0; // we're just going to override it - - if (session.stereo == 5) { - if (session.roomid === "") { - session.stereo = 1; - } else { - session.stereo = 3; - } - } - joinRoom(session.roomid); - if (session.roomid !== "") { - if (!session.cleanOutput) { - // Check if user joined via text input (casual user) - if (sessionStorage.getItem("jvi")) { - sessionStorage.removeItem("jvi"); - if (!urlParams.has("push") && !urlParams.has("id") && !urlParams.has("permaid") - && !urlParams.has("perma") && !urlParams.has("sticky")) { - // Show invite header INSTEAD of "You are in room" - var inviteURL = location.protocol + "//" + location.host + location.pathname + "?room=" + session.roomid; - var urlPW = urlParams.get("password") || urlParams.get("pass") || urlParams.get("pw") || urlParams.get("p"); - if (urlPW === "false" || urlPW === "0" || urlPW === "off") { - // Password explicitly disabled - inviteURL += "&password=false"; - getById("inviteLinkURL").href = inviteURL; - getById("inviteLinkURL").innerText = inviteURL; - getById("head9").classList.remove("hidden"); - } else if (urlPW) { - // Actual password in URL - generate hash - generateHash(session.password + session.salt, 4).then(function(hash) { - inviteURL += "&hash=" + hash; - getById("inviteLinkURL").href = inviteURL; - getById("inviteLinkURL").innerText = inviteURL; - getById("head9").classList.remove("hidden"); - }); - } else { - // No password in URL - use default, no param needed - getById("inviteLinkURL").href = inviteURL; - getById("inviteLinkURL").innerText = inviteURL; - getById("head9").classList.remove("hidden"); - } - } else { - getById("head2").className = ""; - } - } else { - getById("head2").className = ""; - } - } - } - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - } - } else { - // they are not in a room or faux room - if (session.manual === null) { - session.manual = session.manual === null ? true : session.manual; - } - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - getById("logoname").style.display = "none"; - generateHash(session.streamID + session.salt + "bca321", 4) - .then(function (hash) { - // million to one error. - setStorage("showReshare", hash, 24 * 30); - }) - .catch(errorlog); - } - - log("streamID is: " + session.streamID); - getById("head1").className = "hidden"; - - if (!session.cleanOutput) { - getById("mutebutton").classList.remove("hidden"); - getById("mutespeakerbutton").classList.remove("hidden"); - //getById("mutespeakerbutton").className="float"; - getById("chatbutton").className = "float"; - getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. - getById("mutevideobutton").className = "float"; - getById("hangupbutton").className = "float"; - if (session.showSettings) { - getById("settingsbutton").className = "float"; - } - if (session.raisehands) { - getById("raisehandbutton").className = "float"; - } - if (session.pptControls) { - getById("pptbackbutton").classList.remove("hidden"); - getById("pptnextbutton").classList.remove("hidden"); - } - if (session.recordLocal !== false) { - getById("recordLocalbutton").classList.remove("hidden"); - } - if (session.screensharebutton) { - if (session.roomid) { - if (session.screenshareType === 3) { - getById("screenshare3button").className = "float"; - getById("screensharebutton").className = "float hidden"; - getById("screenshare2button").className = "float hidden"; - } else if (session.screenshareType === 1) { - getById("screensharebutton").className = "float"; - getById("screenshare3button").className = "float hidden"; - getById("screenshare2button").className = "float hidden"; - } else if (session.screenshareType === 2) { - getById("screenshare2button").className = "float"; - getById("screensharebutton").className = "float hidden"; - getById("screenshare3button").className = "float hidden"; - } else { - getById("screenshare3button").className = "float"; - getById("screensharebutton").className = "float hidden"; - getById("screenshare2button").className = "float hidden"; - } - } else { - getById("screensharebutton").className = "float"; - getById("screenshare2button").className = "float hidden"; - getById("screenshare3button").className = "float hidden"; - } - } - getById("controlButtons").classList.remove("hidden"); - // getById("legal").classList.remove("hidden"); - //getById("helpbutton").style.display = "inherit"; - //getById("reportbutton").style.display = ""; - } else if (session.cleanish && session.recordLocal !== false) { - getById("recordLocalbutton").classList.remove("hidden"); - getById("mutebutton").classList.add("hidden"); - getById("mutespeakerbutton").classList.add("hidden"); - getById("chatbutton").classList.add("hidden"); - getById("mutevideobutton").classList.add("hidden"); - getById("hangupbutton").classList.add("hidden"); - getById("hangupbutton2").classList.add("hidden"); - getById("controlButtons").classList.remove("hidden"); - getById("settingsbutton").classList.add("hidden"); - getById("screenshare2button").classList.add("hidden"); - getById("screensharebutton").classList.add("hidden"); - getById("queuebutton").classList.add("hidden"); - } else { - getById("controlButtons").classList.add("hidden"); - } - - if (session.chatbutton === true) { - getById("chatbutton").classList.remove("hidden"); - getById("controlButtons").classList.remove("hidden"); - } else if (session.chatbutton === false) { - getById("chatbutton").classList.add("hidden"); - } - - updatePushId(); - - if (session.dataMode) { - // skip the media stuff. - errorlog("this shoulnd't happen.."); - session.postPublish(); - return; - } - - if (!session.streamSrc) { - checkBasicStreamsExist(); // create srcObject + videoElement - } - - if (session.mobile && session.streamSrc && needsLegacyWakeLock()) { - if (Firefox) { - startLegacyKeepAliveLoop(true); - } else if (!session.avatar) { - try { - if (session.streamSrc.getVideoTracks && !session.streamSrc.getVideoTracks().length) { - startLegacyKeepAliveLoop(); - } - } catch (e) { - errorlog(e); - } - } - } - - session.publishStream(getById("previewWebcam")); // calls session.postPublish at the end. -} - -function createYoutubeLink(vidid) { - return "https://www.youtube.com/embed/" + vidid + "?modestbranding=1&playsinline=1&enablejsapi=1&autoplay=1"; -} -function parseURL4Iframe(iframeURL) { - if (iframeURL == "") { - iframeURL = "./"; - } - if (iframeURL === session.iframeSrc) { - return iframeURL; - } - - if (!iframeURL.startsWith("https://") && !iframeURL.startsWith("http://")) { - if (iframeURL.includes(".") && !iframeURL.startsWith("./") && !iframeURL.startsWith("/")) { - iframeURL = "https://" + iframeURL; - } - } - - if (iframeURL.startsWith("http://") && !electronApi && (location.hostname !== "insecure.vdo.ninja")) { - try { - iframeURL = "https://" + iframeURL.split("http://")[1]; - } catch (e) { - errorlog(e); - } - } - - if (iframeURL.startsWith("https://") || iframeURL.startsWith("http://")) { - var domain; - try { - domain = new URL(iframeURL); - domain = domain.hostname; - } catch (e) { - errorlog(e); - return iframeURL; - } - - if (domain == "youtu.be") { - iframeURL = iframeURL.replace("youtu.be/", "youtube.com/watch?v="); - } - - if (domain == "youtu.be" || domain == "www.youtube.com" || domain == "youtube.com") { - if (iframeURL.includes("/v/")) { - var vidMatch = iframeURL.match(/\/v\/([^\/\?#]+)/); - if (vidMatch && vidMatch[1] && vidMatch[1].length == 11) { - return createYoutubeLink(vidMatch[1]); - } - } - - var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; - var match = iframeURL.match(regExp); - var vidid = match && match[7] && match[7].length == 11 ? match[7] : false; - - if (iframeURL.includes("/live_chat")) { - if (!iframeURL.includes("&embed_domain=")) { - iframeURL += "&embed_domain=" + location.hostname; - } - return iframeURL; - } - - if (vidid) { - iframeURL = createYoutubeLink(vidid); - } else { - iframeURL = iframeURL.replace("playlist?list=", "embed/videoseries?list="); - var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(videoseries\?))\??list?=?([^#&?]*).*/; - var match = iframeURL.match(regExp); - var plid = match && match[7] && match[7].length == 34 ? match[7] : false; - if (plid) { - iframeURL = "https://www.youtube.com/embed/videoseries?list=" + plid + "&autoplay=1&modestbranding=1&playsinline=1&enablejsapi=1"; - } - } - } else if (domain == "twitch.tv" || domain == "www.twitch.tv") { - if (iframeURL.includes("twitch.tv/popout/")) { - iframeURL = iframeURL.replace("/popout/", "/embed/"); - iframeURL = iframeURL.replace("?popout=", "?parent=" + location.hostname); - iframeURL = iframeURL.replace("?popout", "?parent=" + location.hostname); - iframeURL = iframeURL.replace("&popout=", "?parent=" + location.hostname); - iframeURL = iframeURL.replace("&popout", "?parent=" + location.hostname); - if (iframeURL.includes("darkpopout=")) { - iframeURL = iframeURL.replace("?darkpopout=", "?darkpopout=&parent=" + location.hostname); - } else { - iframeURL = iframeURL.replace("?darkpopout", "?darkpopout&parent=" + location.hostname); - } - } else { - var vidid = iframeURL.split("/").pop().split("#")[0].split("?")[0]; - if (vidid) { - iframeURL = "https://player.twitch.tv/?channel=" + vidid + "&parent=" + location.hostname; - } - } - } else if (domain == "www.vimeo.com" || domain == "vimeo.com") { - iframeURL = iframeURL.replace("//vimeo.com/", "//player.vimeo.com/video/"); - iframeURL = iframeURL.replace("//www.vimeo.com/", "//player.vimeo.com/video/"); - } else if (domain.endsWith(".tiktok.com") || domain == "tiktok.com") { - var split = iframeURL.split("/video/"); - if (split.length > 1) { - split = split[1].split("/")[0].split("?")[0].split("#")[0]; - iframeURL = "https://www.tiktok.com/embed/v2/" + split; - } - } - } - - return iframeURL; -} - -function soloLinkGenerator(streamID, scene = true) { - var codecGroupFlag = ""; - if (session.codecGroupFlag) { - codecGroupFlag = session.codecGroupFlag; - } - if (session.bitrateGroupFlag) { - codecGroupFlag += session.bitrateGroupFlag; - } - - var wss = ""; - if (session.wssSetViaUrl) { - if (session.customWSS && (session.customWSS !== true)) { - wss = "&pie=" + session.customWSS; - } else if (session.customWSS == true) { - wss = "&wss=" + session.wss; - } else { - wss = "&wss2=" + session.wss; - } - } - - var passAdd2 = ""; - if (session.password) { - if (session.defaultPassword === false) { - passAdd2 = "&password=" + session.password; - } - } - - if (session.token) { - passAdd2 += "&token=" + session.token; - } - - // Add auth parameters if in auth mode - var authParams = ""; - if (session.authMode) { - // For view links, we need a universal token that bypasses auth - if (session.universalViewToken) { - authParams = "&universaltoken=" + session.universalViewToken; - } else { - // Fallback: include auth flag so viewer knows auth is required - authParams = "&auth=true"; - } - } - - if (scene) { - return "https://" + location.host + location.pathname + "?view=" + streamID + "&solo" + codecGroupFlag + "&room=" + session.roomid + passAdd2 + authParams + wss + soloLinkAppended; - } else { - return "https://" + location.host + location.pathname + "?view=" + streamID + codecGroupFlag + passAdd2 + authParams + wss + soloLinkAppended; - } -} - -function YoutubeAPI(iframe, func, args) { - // playVideo, pauseVideo, stopVideo - if (!(iframe && iframe.contentWindow)) { - return; - } - try { - iframe.contentWindow.postMessage( - JSON.stringify({ - event: "command", - func: func, - args: args || [], - id: iframe.id || "unknown" - }), - "*" - ); - } catch (e) { } -} - -function YoutubeListen(iframe_id) { - var iframe = document.getElementById(iframe_id); - if (!iframe) { - return; - } - if (iframe.loadedYoutubeListen) { - return; - } - try { - iframe.contentWindow.postMessage(JSON.stringify({ event: "listening", id: iframe_id }), "*"); // - } catch (e) { } - setTimeout( - function (iframe_id) { - YoutubeListen(iframe_id); - }, - 1000, - iframe_id - ); -} - -function processYoutubeEvent(e) { - if (!(e.type && e.type === "message")) { - return; - } - try { - var data = JSON.parse(e.data); - if ("id" in data) { - var iframe = document.getElementById(data.id); - if (!iframe) { - return; - } - if (!iframe.loadedYoutubeListen) { - iframe.loadedYoutubeListen = true; - } - } - if (!("mediaReferenceTime" in data.info)) { - return; - } - } catch (e) { - return; - } - - log(e); - - if (iframe.id == "iframe_source") { - if (!session.iframeEle.sendOnNewConnect) { - session.iframeEle.sendOnNewConnect = {}; - session.iframeEle.sendOnNewConnect.ifs = {}; - session.iframeEle.sendOnNewConnect.ifs.t = null; - session.iframeEle.sendOnNewConnect.ifs.v = null; - session.iframeEle.sendOnNewConnect.ifs.s = null; - session.iframeEle.sendOnNewConnect.ifs.r = null; - } - - try { - var msg = {}; - msg.ifs = {}; - - try { - msg.ifs.t = parseFloat(data.info.mediaReferenceTime + 0.01) || 0; - session.iframeEle.sendOnNewConnect.ifs.t = msg.ifs.t; - } catch (e) { - return; - } - - if ("playerState" in data.info) { - msg.ifs.s = parseInt(data.info.playerState); - - if (msg.ifs.s == -1) { - msg.ifs.s = 0; - } - if (msg.ifs.s == 2) { - if (session.iframeEle.sendOnNewConnect.ifs.s == 3) { - delete msg.ifs.s; - } else { - msg.ifs.s = 3; - } - } - if (msg.ifs.s && session.iframeEle.sendOnNewConnect.ifs.s != msg.ifs.s) { - session.iframeEle.sendOnNewConnect.ifs.s = msg.ifs.s; - } else { - //delete(msg.ifs.s); - } - - if ("videoData" in data.info) { - if (session.iframeEle.sendOnNewConnect.ifs.v != data.info.videoData.video_id) { - session.iframeEle.sendOnNewConnect.ifs.v = data.info.videoData.video_id; - msg.ifs.v = data.info.videoData.video_id; - var vidSrc = createYoutubeLink(msg.ifs.v); - if (vidSrc !== session.iframeSrc) { - session.iframeSrc = vidSrc; - var data = {}; - data.iframeSrc = session.iframeSrc; - if (parseInt(msg.ifs.t) > 1) { - data.iframeSrc += "&start=" + parseInt(Math.ceil(msg.ifs.t)) + ""; - } - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowIframe === true) { - session.sendMessage(data, UUID); - } - } - return; - } - } - } - // we will still be sending the msg data if available. - } else if ("videoData" in data.info) { - if (session.iframeEle.sendOnNewConnect.ifs.v != data.info.videoData.video_id) { - msg.ifs.v = data.info.videoData.video_id; - session.iframeEle.sendOnNewConnect.ifs.v = msg.ifs.v; - var vidSrc = createYoutubeLink(msg.ifs.v); - if (vidSrc !== session.iframeSrc) { - session.iframeSrc = vidSrc; - var data = {}; - data.iframeSrc = session.iframeSrc; - if (parseInt(msg.ifs.t) > 1) { - data.iframeSrc += "&start=" + parseInt(Math.ceil(msg.ifs.t)) + ""; - } - if (session.iframeEle.sendOnNewConnect.ifs.s == "1") { - data.iframeSrc += "&autoplay=1"; - } else { - data.iframeSrc += "&autoplay=0"; - } - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowIframe === true) { - session.sendMessage(data, UUID); - } - } - return; - } - } - } else { - if ("playbackRate" in data.info) { - msg.ifs.r = parseFloat(data.info.playbackRate); - if (session.iframeEle.sendOnNewConnect.ifs.r != msg.ifs.r) { - session.iframeEle.sendOnNewConnect.ifs.r = msg.ifs.r; - } else { - delete msg.ifs.r; - } - } - if (session.iframeEle.sendOnNewConnect.ifs.s == 1) { - if ("t" in msg.ifs) { - delete msg.ifs.t; - } - } - } - - if (Object.keys(msg.ifs).length == 0) { - return; - } - - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowIframe) { - session.sendMessage(msg); - } - } - } catch (e) { - return; - } - } else { - try { - var UUID = iframe.dataset.UUID; - var msg = {}; - msg.ifs = {}; - if ("t" in msg.ifs) { - msg.ifs.t = parseFloat(data.info.mediaReferenceTime + 0.01) || 0; - /* if (!iframe.sendOnNewConnect){ - iframe.sendOnNewConnect = msg; - } else { - iframe.sendOnNewConnect.ifs.t = msg.ifs.t; - } */ - } - - if ("playerState" in data.info) { - msg.ifs.s = parseInt(data.info.playerState); - } - if ("videoData" in data.info) { - msg.ifs.v = data.info.videoData.video_id; - } - if ("playbackRate" in data.info && data.info.playbackRate !== 1) { - msg.ifs.r = parseFloat(data.info.playbackRate); - } - // TODO: the viewers don't have a way to tell the director if they reload what the time is at. - session.sendRequest(msg, UUID); // send to the iframe's owner only. let them be the controller for others. - } catch (e) { - return; - } - } -} - -function processIframeSyncFeedback(ifs, UUID) { - // remote iframe feedback from the remote viewers - // YoutubeAPI("iframe_source", "seekTo", [700]); - // YoutubeAPI("iframe_source", "volume", [100]); - - warnlog(ifs); - return; - - if (!session.iframeEle.sendOnNewConnect) { - session.iframeEle.sendOnNewConnect = {}; - session.iframeEle.sendOnNewConnect.ifs = {}; - session.iframeEle.sendOnNewConnect.ifs.t = null; - session.iframeEle.sendOnNewConnect.ifs.v = null; - session.iframeEle.sendOnNewConnect.ifs.s = null; - session.iframeEle.sendOnNewConnect.ifs.r = null; - } - - if ("t" in ifs) { - if (Math.abs(session.iframeEle.sendOnNewConnect.ifs.t - ifs.t) >= 1) { - //session.iframeEle.sendOnNewConnect.ifs.t = ifs.t; - } else { - delete ifs.t; - } - } - if ("v" in ifs) { - if (session.iframeEle.sendOnNewConnect.ifs.v != ifs.v) { - //session.iframeEle.sendOnNewConnect.ifs.v = ifs.v; - } else { - delete ifs.v; - } - } - if ("s" in ifs) { - if (ifs.s == -1) { - ifs.s = 0; - } - if (session.iframeEle.sendOnNewConnect.ifs.s == -1) { - session.iframeEle.sendOnNewConnect.ifs.s = 0; - } - - if (ifs.s == 2) { - ifs.s = 3; - } - if (session.iframeEle.sendOnNewConnect.ifs.s == 2) { - session.iframeEle.sendOnNewConnect.ifs.s = 3; - } - - if (session.iframeEle.sendOnNewConnect.ifs.s != ifs.s) { - //session.iframeEle.sendOnNewConnect.ifs.s = ifs.s; - } else { - delete ifs.s; - } - } - if ("r" in ifs) { - if (session.iframeEle.sendOnNewConnect.ifs.r != ifs.r) { - //session.iframeEle.sendOnNewConnect.ifs.r = ifs.r; - } else { - delete ifs.r; - } - } - - if (session.iframeEle) { - if (ifs.v) { - // I need to have this change videos . - var vidSrc = createYoutubeLink(ifs.v); - if (vidSrc !== session.iframeSrc) { - session.iframeSrc = vidSrc; - session.iframeEle.src = vidSrc; - } - } else if ("t" in ifs) { - YoutubeAPI(session.iframeEle, "seekTo", [parseFloat(ifs.t)]); - } else if (ifs.r) { - /// setPlaybackRate - YoutubeAPI(session.iframeEle, "setPlaybackRate", [parseFloat(ifs.r)]); - } else if ("s" in ifs) { - /// setPlaybackState - if (ifs.s == -1) { - YoutubeAPI(session.iframeEle, "stopVideo"); - } else if (ifs.s == 0) { - YoutubeAPI(session.iframeEle, "stopVideo"); - } // player stops. - else if (ifs.s == 1) { - YoutubeAPI(session.iframeEle, "playVideo"); - } //Video is playing - else if (ifs.s == 2) { - YoutubeAPI(session.iframeEle, "pauseVideo"); - } //Video is paused - else if (ifs.s == 3) { - YoutubeAPI(session.iframeEle, "pauseVideo"); - } //video is buffering - else if (ifs.s == 5) { - } //Video is cued. - } - } else if (session.iframeSrc) { - if (ifs.v) { - var vidSrc = createYoutubeLink(ifs.v); - if (vidSrc !== session.iframeSrc) { - session.iframeSrc = vidSrc; - var data = {}; - data.iframeSrc = session.iframeSrc; - if (ifs.t && parseInt(ifs.t) > 1) { - data.iframeSrc += "&start=" + parseInt(Math.ceil(ifs.t)); - } - if (ifs.s == "1") { - data.iframeSrc += "&autoplay=1"; - } else { - data.iframeSrc += "&autoplay=0"; - } - for (var uuid in session.pcs) { - if (uuid == UUID) { - continue; - } - if (session.pcs[uuid].allowIframe === true) { - session.sendMessage(data, uuid); - } - } - return; - } - } - // we're going to forward the message directly to the other viewers instead - if ("s" in ifs) { - /// setPlaybackState - var msg = {}; - msg.ifs = ifs; - for (var uuid in session.pcs) { - if (uuid == UUID) { - continue; - } - if (session.pcs[uuid].allowIframe) { - session.sendMessage(msg, uuid); - } - } - } - } -} - -function processIframeSyncUpdates(ifs, UUID) { - // playback updates from remote guest. - // YoutubeAPI("iframe_source", "seekTo", [700]); - // YoutubeAPI("iframe_source", "volume", [100]); - if (ifs.v && "s" in ifs) { - // - } else if ("s" in ifs) { - if ("t" in ifs) { - YoutubeAPI(session.rpcs[UUID].iframeEle, "seekTo", [parseFloat(ifs.t)]); - } - YoutubeAPI(session.rpcs[UUID].iframeEle, "playVideo"); - } else if ("t" in ifs) { - YoutubeAPI(session.rpcs[UUID].iframeEle, "seekTo", [parseFloat(ifs.t)]); - } - if (ifs.r) { - /// setPlaybackRate - YoutubeAPI(session.rpcs[UUID].iframeEle, "setPlaybackRate", [parseFloat(ifs.r)]); - } - if ("s" in ifs) { - /// setPlaybackState - if (ifs.s == -1) { - YoutubeAPI(session.rpcs[UUID].iframeEle, "stopVideo"); - } else if (ifs.s == 0) { - YoutubeAPI(session.rpcs[UUID].iframeEle, "stopVideo"); - } // player stops. - else if (ifs.s == 1) { - YoutubeAPI(session.rpcs[UUID].iframeEle, "playVideo"); - } //Video is playing - else if (ifs.s == 2) { - YoutubeAPI(session.rpcs[UUID].iframeEle, "pauseVideo"); - } //Video is paused - else if (ifs.s == 3) { - YoutubeAPI(session.rpcs[UUID].iframeEle, "pauseVideo"); - } //video is buffering - else if (ifs.s == 5) { - } //Video is cued. - } -} - -function updatePushId() { - if (session.doNotSeed) { - return; - } - - if (urlParams.has("push")) { - updateURL("push=" + session.streamID); - } else if (urlParams.has("id")) { - updateURL("id=" + session.streamID); - } else if (urlParams.has("permaid")) { - updateURL("permaid=" + session.streamID); - } else { - updateURL("push=" + session.streamID); - } -} - -session.publishIFrame = function (iframeURL) { - if (!session.cleanOutput) { - getById("websitesharebutton2").classList.remove("hidden"); - } - - if (session.transcript) { - setTimeout(function () { - setupClosedCaptions(); - }, 1000); - } - - session.iframeSrc = parseURL4Iframe(iframeURL); - - if (!session.iFramesAllowed) { errorlog("Can't create iFRAME - security is tainted due to possible CSS injection"); warnUser("Can't create iFRAME - security is tainted due to possible CSS injection"); return; } - - var iframe = document.createElement("iframe"); - iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location - iframe.src = session.iframeSrc; - iframe.id = "iframe_source"; - iframe.setAttribute("allowtransparency", "true"); - iframe.setAttribute("crossorigin", "anonymous"); - iframe.setAttribute("credentialless", "true"); - iframe.loadedYoutubeListen = false; - session.iframeEle = iframe; - - var container = document.createElement("div"); - iframe.container = container; - container.id = "container_iframe"; - container.appendChild(iframe); - getById("gridlayout").appendChild(container); - - if (session.iframeSrc.startsWith("https://www.youtube.com/")) { - // special handler. - setTimeout( - function (iframe_id) { - YoutubeListen(iframe_id); - }, - 1000, - iframe.id - ); - } - - if (session.cover) { - container.style.setProperty("height", "100%", "important"); - } - - if (session.roomid !== false) { - if (session.roomid === "" && (!session.view || session.view === "")) { - } else { - log("ROOMID EANBLED"); - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - joinRoom(session.roomid); - } - } else { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - getById("logoname").style.display = "none"; - } - getById("head1").className = "hidden"; - - updatePushId(); - - getById("head1").className = "hidden"; - getById("head2").className = "hidden"; - - if (!session.cleanOutput) { - getById("chatbutton").className = "float"; - getById("hangupbutton").className = "float"; - getById("controlButtons").classList.remove("hidden"); - // getById("legal").classList.remove("hidden"); - getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. - //getById("helpbutton").style.display = "inherit"; - //getById("reportbutton").style.display = ""; - } else { - getById("controlButtons").classList.add("hidden"); - } - - if (session.chatbutton === false) { - getById("chatbutton").classList.add("hidden"); - } - - if (session.director) { - // - } else if (session.scene !== false) { - updateMixer(); - } else if (session.roomid !== false) { - if (session.roomid === "") { - if (!session.view || session.view === "") { - session.windowed = session.windowed === null ? true : session.windowed; - container.classList.add("vidcon"); - - getById("mutespeakerbutton").classList.add("hidden"); - container.style.width = "100%"; - container.style.height = "100%"; - container.style.alignItems = "center"; - container.style.maxWidth = "100%"; - container.style.maxHeight = "100%"; - container.style.verticalAlign = "middle"; - container.style.margin = "auto"; - container.style.backgroundColor = "#666"; - container.style.border = "2px solid"; - } else { - session.windowed = session.windowed === null ? false : session.windowed; - window.onresize = updateMixer; - updateMixer(); - } - } else { - window.onresize = updateMixer; - session.windowed = session.windowed === null ? false : session.windowed; - updateMixer(); - } - } else { - window.onresize = updateMixer; - container.style.maxHeight = "1280px"; - container.style.maxWidth = "720px"; - container.style.verticalAlign = "middle"; - container.style.height = "100%"; - container.style.width = "100%"; - container.style.margin = "auto"; - container.style.alignItems = "center"; - container.style.backgroundColor = "#666"; - } - - session.seeding = true; - - updateReshareLink(); - pokeIframeAPI("started-iframe-share"); - session.seedStream(); - - return container; -}; // publishIframe - -/* session.publishWhepSrc = function(){ - - if (!session.whepSrc){errorlog("no WHEP Src");return;} - - if (!session.cleanOutput){ - getById("websitesharebutton2").classList.remove('hidden'); - } - - var UUID = whepIn(session.whepSrc); - - var container = document.createElement("div"); - iframe.container = container; - container.id = "container_iframe"; - container.appendChild(iframe); - getById("gridlayout").appendChild(container); - - if (session.iframeSrc.startsWith("https://www.youtube.com/")){ // special handler. - setTimeout(function(iframe_id){YoutubeListen(iframe_id);}, 1000, iframe.id); - } - - if (session.cover){ - container.style.setProperty('height', '100%', 'important'); - } - - if (session.roomid!==false){ - if ((session.roomid==="") && ((!(session.view)) || (session.view===""))){ - - } else { - log("ROOMID EANBLED"); - getById("head3").classList.add('hidden'); - getById("head3a").classList.add('hidden'); - joinRoom(session.roomid); - } - - } else { - getById("head3").classList.remove('hidden'); - getById("head3a").classList.remove('hidden'); - getById("logoname").style.display = 'none'; - } - getById("head1").className = 'hidden'; - - updatePushId() - - getById("head1").className = 'hidden'; - getById("head2").className = 'hidden'; - - if (!(session.cleanOutput)){ - getById("chatbutton").className="float"; - getById("hangupbutton").className="float"; - getById("controlButtons").classList.remove("hidden"); - getById('sharefilebutton').classList.remove("hidden"); // we won't override "display:none", if set, though. - getById("helpbutton").style.display = "inherit"; - getById("reportbutton").style.display = ""; - } else { - getById("controlButtons").classList.add("hidden"); - } - - if (session.chatbutton === false) { - getById("chatbutton").classList.add("hidden"); - } - - if (session.director){ - // - } else if (session.scene!==false){ - updateMixer(); - } else if (session.roomid!==false){ - if (session.roomid===""){ - if (!session.fullscreen && (!(session.view) || (session.view===""))){ - session.windowed = session.windowed === null ? true : session.windowed; - container.classList.add("vidcon"); - - getById("mutespeakerbutton").classList.add("hidden"); - container.style.width="100%"; - container.style.height="100%"; - container.style.alignItems = "center"; - container.style.maxWidth= "100%"; - container.style.maxHeight= "100%"; - container.style.verticalAlign= "middle"; - container.style.margin= "auto"; - container.style.backgroundColor = "#666"; - container.style.border = "2px solid"; - - } else { - session.windowed = session.windowed === null ? false : session.windowed; - window.onresize = updateMixer; - updateMixer(); - } - } else { - window.onresize = updateMixer; - session.windowed = session.windowed === null ? false : session.windowed; - updateMixer(); - } - } else { - window.onresize = updateMixer; - container.style.maxHeight= "1280px"; - container.style.maxWidth= "720px"; - container.style.verticalAlign= "middle"; - container.style.height="100%"; - container.style.width= "100%"; - container.style.margin= "auto"; - container.style.alignItems = "center"; - container.style.backgroundColor = "#666"; - } - - session.seeding=true; - - updateReshareLink(); - pokeIframeAPI('started-iframe-share'); - session.seedStream(); - - return container; -} // publishWhepSrc */ - -function disabledWebAudioPathway() { - log("Executing disabledWebAudioPathway."); - // if (session.disableWebAudio) { then run this instead; or if webaudio nodes fail.} - // if (iOS || iPad){return session.streamSrc;} // Original comment: iOS devices can't remap video tracks, else KABOOM. Might as well do this for android also. - // This iOS specific return was in comments, if it's critical, it should be uncommented. - // However, the logic below also attempts to handle stream cloning. - - if (session.streamSrcClone) { - log("disabledWebAudioPathway: Cleaning up existing session.streamSrcClone"); - session.streamSrcClone.getTracks().forEach(function (track) { - session.streamSrcClone.removeTrack(track); - track.stop(); - }); - session.streamSrcClone = null; - } - - var newStream = createMediaStream(); // This will be the returned stream - - if (session.streamSrc && typeof session.streamSrc.clone === 'function' && !window.obsstudio) { // Prefer cloning if available and not obsstudio - log("disabledWebAudioPathway: Cloning session.streamSrc (non-obsstudio path)"); - newStream = session.streamSrc.clone(); - } else { - log("disabledWebAudioPathway: Creating new stream and adding tracks manually."); - if (session.streamSrc) { - session.streamSrc.getAudioTracks().forEach(function (track) { - if (track.readyState === 'live') { - // For obsstudio, audio tracks can also be cloned for consistency, though less critical than video. - // For simplicity here, adding original, but could be `track.clone()` - newStream.addTrack(track); - log("disabledWebAudioPathway: Added audio track " + track.id); - } - }); - } - - let videoSourceForDisabledPath = null; - if (session.videoElement && session.videoElement.srcObject) { - videoSourceForDisabledPath = session.videoElement.srcObject; - } else if (session.streamSrc) { - videoSourceForDisabledPath = session.streamSrc; - } - - if (videoSourceForDisabledPath) { - videoSourceForDisabledPath.getVideoTracks().forEach(function (track) { - if (track.readyState === 'live') { - if (window.obsstudio) { - log(`disabledWebAudioPathway (obsstudio): Cloning video track ${track.id}`); - try { - const clonedVideoTrack = track.clone(); - newStream.addTrack(clonedVideoTrack); - } catch (e_clone_video) { - errorlog(`disabledWebAudioPathway (obsstudio): Failed to clone video track ${track.id}. Adding original. Error:`, e_clone_video); - newStream.addTrack(track); // Fallback - } - } else { - log(`disabledWebAudioPathway (non-obsstudio): Adding original video track ${track.id}`); - newStream.addTrack(track); // Original behavior - } - } - }); - } - } - if (iOS || iPad || session.streamSrcClone) { - session.streamSrcClone = newStream; // Store the newly created/cloned stream - } - return newStream; -} - -function outboundAudioPipeline(sourceStream = false) { - - if (session.disableWebAudio) { - return disabledWebAudioPathway(); // Safemode - } - - if (!session.streamSrc && !sourceStream) { - errorlog("STREAM DOES NOT EXIST. This is a problem"); - checkBasicStreamsExist(); - return session.streamSrc; - } - - var streamSrc = sourceStream || session.streamSrc; - - if (iOS || iPad) { - if (session.streamSrcClone) { - var audioTracksForCleanup = session.streamSrcClone.getAudioTracks(); - if (audioTracksForCleanup.length) { - for (var waid in session.webAudios) { - if (session.webAudios[waid] && typeof session.webAudios[waid].stop === 'function') { - session.webAudios[waid].stop(); - } - delete session.webAudios[waid]; - } - } - session.streamSrcClone.getTracks().forEach(function (track) { - session.streamSrcClone.removeTrack(track); - track.stop(); - }); - session.streamSrcClone = null; - } - - // Create iOS-compatible stream (always clone for iOS) - if (session.streamSrc && typeof session.streamSrc.clone === 'function') { - log("iOS: Cloning session.streamSrc"); - streamSrc = session.streamSrc.clone(); - session.streamSrcClone = streamSrc; - } else { - log("iOS: Creating new stream as backup"); - session.streamSrcClone = createOptimizedStream(session.streamSrc, true); - streamSrc = session.streamSrcClone; - } - } - - for (var waid in session.webAudios) { - if (session.webAudios[waid] && typeof session.webAudios[waid].stop === 'function') { - session.webAudios[waid].stop(); - } - delete session.webAudios[waid]; - } - session.webAudios = session.webAudios || {}; - - try { - log("Web Audio processing initiated."); - var audioTracks = streamSrc.getAudioTracks(); - - if (audioTracks.length) { - var webAudio = initWebAudioNode(audioTracks[0].id); - - if (audioTracks.length > 1) { - try { - setupMultiTrackAudio(audioTracks, webAudio); - } catch (e_multi) { - errorlog("Error in multi-track audio setup, falling back: ", e_multi); - try { - webAudio.mediaStreamSource = webAudio.audioContext.createMediaStreamSource(streamSrc); - webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext); - } catch (e_multi_fallback) { - errorlog("Fallback failed: ", e_multi_fallback); - return disabledWebAudioPathway(); - } - } - } else { - try { - webAudio.mediaStreamSource = webAudio.audioContext.createMediaStreamSource(streamSrc); - webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext); - } catch (e_single) { - errorlog("Error creating single track setup: ", e_single); - return disabledWebAudioPathway(); - } - } - - var anonNode = applyAudioProcessing(webAudio, streamSrc); - - const finalOutputStream = createMediaStream(); - webAudio.destination.stream.getAudioTracks().forEach(audioTrack => { - finalOutputStream.addTrack(audioTrack); - }); - - addVideoTracksToStream(finalOutputStream, streamSrc); - - if (webAudio.audioContext && webAudio.audioContext.state === "suspended") { - webAudio.audioContext.resume().catch(e => errorlog("AudioContext resume failed:", e)); - } - - return finalOutputStream; - - } else { - log("No audio tracks found. Handling video passthrough."); - // Return video-only stream - if (window.obsstudio) { - log("OBS (no audio): Creating stream with cloned video tracks"); - const newStream = createMediaStream(); - addVideoTracksToStream(newStream, streamSrc); - return newStream; - } else { - log("Non-OBS (no audio): Using direct video source"); - if (session.videoElement && session.videoElement.srcObject) { - return session.videoElement.srcObject; - } - - const newStream = createMediaStream(); - addVideoTracksToStream(newStream, streamSrc); - return newStream; - } - } - } catch (e_main) { - errorlog("Critical error in outboundAudioPipeline: "); - errorlog(e_main); - return streamSrc; - } -} - -function createOptimizedStream(source, shouldClone = false) { - const newStream = createMediaStream(); - - if (!source) return newStream; - - source.getAudioTracks().forEach(track => { - if (track.readyState === 'live') { - if (shouldClone) { - try { - newStream.addTrack(track.clone()); - } catch (e) { - errorlog("Failed to clone audio track. Adding original. Error:", e); - newStream.addTrack(track); - } - } else { - newStream.addTrack(track); - } - } - }); - - addVideoTracksToStream(newStream, source); - - return newStream; -} - -function addVideoTracksToStream(targetStream, sourceStream) { - let videoSourceStream = sourceStream; - - // Find video source with fallback - if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length > 0) { - videoSourceStream = session.videoElement.srcObject; - } else if (!videoSourceStream || videoSourceStream.getVideoTracks().length === 0) { - videoSourceStream = session.streamSrc; - } - - if (videoSourceStream) { - videoSourceStream.getVideoTracks().forEach(track => { - if (track.readyState === 'live') { - // Only clone for OBS Studio - if (window.obsstudio) { - log(`OBS: Cloning video track ${track.id}`); - try { - const clonedTrack = track.clone(); - targetStream.addTrack(clonedTrack); - } catch (e_clone) { - errorlog(`OBS: Failed to clone track ${track.id}. Adding original. Error:`, e_clone); - targetStream.addTrack(track); - } - } else { - targetStream.addTrack(track); - } - } - }); - } -} - -function initWebAudioNode(trackId) { - var webAudio = { - id: trackId, - micDelay: false, - compressor: false, - analyser: false, - gainNode: false, - splitter: false, - subGainNodes: false, - lowEQ: false, - midEQ: false, - highEQ: false, - lowcut1: false, - lowcut2: false, - lowcut3: false, - waveShaper_vc: null, - oscillator_vc: null, - oscillatorGain_vc: null, - delay_vc: null, - lowEQ_vc: null, - mid_vc: null - }; - - // Create audio context if needed - if (session.audioCtxOutbound) { - // Already created - } else if (session.outboundSampleRate) { - try { - session.audioCtxOutbound = new AudioContext({ sampleRate: session.outboundSampleRate }); - } catch (e) { - session.audioCtxOutbound = new AudioContext(); - errorlog(e); - } - } else if (session.outboundSampleRate === false || Firefox || SafariVersion || session.mobile) { - session.audioCtxOutbound = new AudioContext(); - } else if (session.audioLatency !== false) { - session.audioCtxOutbound = new AudioContext({ - latencyHint: session.audioLatency / 1000.0, - sampleRate: 48000 - }); - } else { - try { - session.audioCtxOutbound = new AudioContext({ sampleRate: 48000 }); - } catch (e) { - session.audioCtxOutbound = new AudioContext(); - errorlog(e); - } - } - - if (session.audioCtxOutbound && session.audioCtxOutbound.sampleRate > 192000) { - console.error("Warning: Your audio playback device has a very high sample rate set; lower it to 48000-Hz to avoid audio issues"); - } - - webAudio.audioContext = session.audioCtxOutbound; - webAudio.destination = session.audioCtxOutbound.createMediaStreamDestination(); - - return webAudio; -} - -// Helper function to handle multi-track audio setup -function setupMultiTrackAudio(audioTracks, webAudio) { - var maxChannelCount = session.stereo === false ? 1 : 2; - webAudio.subGainNodes = {}; - - var mergerNode = webAudio.audioContext.createChannelMerger(maxChannelCount); - - for (var i = 0; i < audioTracks.length; i++) { - try { - var tempIndividualTrackStream = createMediaStream(); - tempIndividualTrackStream.addTrack(audioTracks[i]); - var trackAudioSourceNode = webAudio.audioContext.createMediaStreamSource(tempIndividualTrackStream); - - webAudio.subGainNodes[audioTracks[i].id] = webAudio.audioContext.createGain(); - trackAudioSourceNode.connect(webAudio.subGainNodes[audioTracks[i].id]); - - if (maxChannelCount == 2) { - var individualSplitter = webAudio.audioContext.createChannelSplitter(2); - webAudio.subGainNodes[audioTracks[i].id].connect(individualSplitter); - individualSplitter.connect(mergerNode, 0, 0); - try { - individualSplitter.connect(mergerNode, 1, 1); - } catch (e_stereo) { - errorlog("Stereo connect ch1->input1 failed: ", e_stereo); - try { - individualSplitter.connect(mergerNode, 0, 1); - } catch (e_stereo_fallback) { - errorlog("Stereo connect ch0->input1 fallback failed: ", e_stereo_fallback); - } - } - } else { - webAudio.subGainNodes[audioTracks[i].id].connect(mergerNode, 0, 0); - } - } catch (e_track) { - errorlog("Error processing track: ", e_track); - throw e_track; - } - } - - webAudio.mediaStreamSource = mergerNode; - webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext); -} - -function applyAudioProcessing(webAudio, streamSrc) { - try { - var anonNode = webAudio.gainNode; - - // Channel downmixing - if (session.audioInputChannels == 1) { - anonNode = applyDownmixing(anonNode, webAudio); - } - - // Low cut filter - if (session.lowcut) { - anonNode = applyLowCut(anonNode, webAudio); - } - - // Voice changer - if (session.voicechanger) { - anonNode = applyVoiceChanger(anonNode, webAudio); - } - - // Equalizer - if (session.equalizer) { - anonNode = applyEqualizer(anonNode, webAudio); - } - - // Compressor/Limiter - if (session.compressor === 1) { - webAudio.compressor = audioCompressor(anonNode, webAudio.audioContext); - anonNode = webAudio.compressor; - } else if (session.compressor === 2) { - webAudio.compressor = audioLimiter(anonNode, webAudio.audioContext); - anonNode = webAudio.compressor; - } - - // Mic panning (publisher-side): force mono, then pan to stereo - if (session.micPanning !== false) { - anonNode = applyMicPanning(anonNode, webAudio, session.micPanning); - } - - // Mic delay - if (session.micDelay !== false) { - webAudio.micDelay = micDelayNode(anonNode, webAudio.audioContext); - anonNode = webAudio.micDelay; - } - - // Twilio mix - if (session.twilio && session.twilio.element && session.twilio.element.srcObject && session.twilio.element.srcObject.getAudioTracks().length) { - const twilioSource = webAudio.audioContext.createMediaStreamSource(session.twilio.element.srcObject); - twilioSource.connect(anonNode); - } - - // Noise gate - if (session.noisegate !== false) { - webAudio.analyser = audioMeter(anonNode, webAudio.audioContext); - anonNode = webAudio.analyser; - webAudio.gatingNode = audioGatingNode(anonNode, webAudio.audioContext); - webAudio.gatingNode.connect(webAudio.destination); - } else { - webAudio.analyser = audioMeter(anonNode, webAudio.audioContext); - webAudio.analyser.connect(webAudio.destination); - } - - webAudio.stop = createStopFunction(webAudio); - - if (streamSrc && webAudio.mediaStreamSource) { - const tracks = streamSrc.getTracks(); - if (tracks.length) { - tracks.forEach(track => { - track.addEventListener('ended', () => { - log("Track ended, stopping webAudio"); - webAudio.stop(); - }); - }); - } else if (webAudio.mediaStreamSource.onended !== undefined) { - // Fallback for older browsers - webAudio.mediaStreamSource.onended = () => { - log("MediaStreamSource ended, stopping webAudio"); - webAudio.stop(); - }; - } - } - - session.webAudios[webAudio.id] = webAudio; - } catch (e) { - console.error(e); - return webAudio; - } - - return anonNode; -} - -function applyMicPanning(inputNode, webAudio, value) { - // Convert 0..180 to -1..1 (90 center) - if (value === true || value === "true") { - value = 90; - } - value = parseFloat(value); - if (isNaN(value)) { value = 90; } - var panNorm = (value / 90.0) - 1.0; - if (panNorm < -1) panNorm = -1; - if (panNorm > 1) panNorm = 1; - - // Downmix to mono explicitly - let splitter = webAudio.audioContext.createChannelSplitter(2); - let mono = webAudio.audioContext.createChannelMerger(1); - try { - inputNode.connect(splitter); - splitter.connect(mono, 0, 0); - splitter.connect(mono, 1, 0); - } catch (e) { - // If connection fails (e.g., mono input), fallback to direct - try { inputNode.connect(mono, 0, 0); } catch (ee) { } - } - - // Pre-pan gain reduction to avoid clipping when panned - webAudio.micPanGainNode = webAudio.audioContext.createGain(); - webAudio.micPanGainNode.gain.value = 1 - Math.abs(panNorm) / 2; - mono.connect(webAudio.micPanGainNode); - - // Create panner with Safari fallback - if (webAudio.audioContext.createStereoPanner) { - webAudio.micPanType = "stereo"; - webAudio.micPanNode = webAudio.audioContext.createStereoPanner(); - webAudio.micPanNode.pan.value = panNorm; - } else { - webAudio.micPanType = "panner"; - webAudio.micPanNode = webAudio.audioContext.createPanner(); - webAudio.micPanNode.panningModel = "equalpower"; - webAudio.micPanNode.distanceModel = "inverse"; - let x = panNorm; - let z = 1 - Math.abs(panNorm); - try { - if (typeof webAudio.micPanNode.positionX !== "undefined") { - webAudio.micPanNode.positionX.value = x; - webAudio.micPanNode.positionY.value = 0; - webAudio.micPanNode.positionZ.value = z; - } else { - webAudio.micPanNode.setPosition(x, 0, z); - } - } catch (e) { } - } - - webAudio.micPanGainNode.connect(webAudio.micPanNode); - return webAudio.micPanNode; -} - -function changeMicPanning(value, deviceid = null) { - // Update all active outbound webAudio chains - let pan = parseFloat(value); - if (isNaN(pan)) { pan = 90; } - if (pan < 0) pan = 0; - if (pan > 180) pan = 180; - let norm = (pan / 90.0) - 1.0; - if (norm < -1) norm = -1; - if (norm > 1) norm = 1; - session.micPanning = pan; - - for (var waid in session.webAudios) { - try { - let wa = session.webAudios[waid]; - if (!wa) continue; - if (wa.micPanNode) { - if (wa.micPanType === "stereo" && wa.micPanNode.pan) { - wa.micPanNode.pan.setValueAtTime(norm, wa.audioContext.currentTime); - } else { - let x = norm; - let z = 1 - Math.abs(norm); - if (typeof wa.micPanNode.positionX !== "undefined") { - wa.micPanNode.positionX.setValueAtTime(x, wa.audioContext.currentTime); - wa.micPanNode.positionY.setValueAtTime(0, wa.audioContext.currentTime); - wa.micPanNode.positionZ.setValueAtTime(z, wa.audioContext.currentTime); - } else if (wa.micPanNode.setPosition) { - wa.micPanNode.setPosition(x, 0, z); - } - } - } - if (wa.micPanGainNode && wa.micPanGainNode.gain) { - wa.micPanGainNode.gain.setValueAtTime(1 - Math.abs(norm) / 2, wa.audioContext.currentTime); - } - } catch (e) { errorlog(e); } - } -} - -// helper to keep approval popup text current with label/streamID -session.updateApprovalPrompt = function (UUID) { - try { - if (!session.director || !session.approval_popup) { return; } - var label = (session.rpcs[UUID] && session.rpcs[UUID].label) || ("Guest " + (UUID || '').substring(0, 8)); - var sid = (session.rpcs[UUID] && session.rpcs[UUID].streamID) || UUID; - try { label = ("" + label).replace(/[<>]/g, ""); sid = ("" + sid).replace(/[<>]/g, ""); } catch (e) { } - var line = "A guest is waiting to be admitted.\n\n" + - "Guest: " + label + "\n" + - "ID: " + sid + "\n\n" + - (session.directorState === false ? "Approve?\n(This sends the action to the main director.)" : "Approve?"); - updateConfirmAlt('approval-' + UUID, line); - } catch (e) { errorlog(e); } -}; - -function requestChangeMicPanning(value, UUID, track = 0) { - var msg = {}; - msg.requestChangeMicPanning = true; - msg.value = value; - msg.UUID = UUID; - msg.track = track; - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-change-micpanning", { value: value, track: track }, UUID); -} -function createStopFunction(webAudio) { - return function () { - // Prevent multiple calls - if (webAudio.stopped) { - errorlog("Trying to stop webaudio more than once"); - return; - } - webAudio.stopped = true; - - // Clear analyzer interval if it exists - try { - if (webAudio.analyser && webAudio.analyser.interval) { - clearInterval(webAudio.analyser.interval); - } - } catch (e) { - errorlog("Error clearing analyser interval:", e); - } - - // Special handling for subGainNodes (collection of nodes) - if (webAudio.subGainNodes) { - for (var id in webAudio.subGainNodes) { - try { - if (webAudio.subGainNodes[id]) { - webAudio.subGainNodes[id].disconnect(); - webAudio.subGainNodes[id] = null; - } - } catch (e) { - errorlog("Error disconnecting subGainNode " + id + ":", e); - } - } - webAudio.subGainNodes = null; - } - - // List of properties to skip disconnecting - const skipProperties = ["stop", "id", "audioContext", "mediaStreamSource", "subGainNodes", "stopped"]; - - // Disconnect all other nodes - for (var node in webAudio) { - if (!webAudio[node] || skipProperties.includes(node)) { - continue; - } - - try { - // Only disconnect if it has a disconnect method (is an audio node) - if (typeof webAudio[node].disconnect === 'function') { - webAudio[node].disconnect(); - log("Disconnected node: " + node); - } - webAudio[node] = null; - } catch (e) { - errorlog("Error disconnecting node " + node + ":", e); - } - } - - // Remove from session tracking - if (session.webAudios && webAudio.id && session.webAudios[webAudio.id]) { - delete session.webAudios[webAudio.id]; - } - }; -} - -function changeLowCut(freq, deviceid = null) { - log("LOW EQ"); - - for (var webAudio in session.webAudios) { - if (!session.webAudios[webAudio].lowcut1) { - errorlog("EQ not setup"); - return; - } - if (!session.webAudios[webAudio].lowcut2) { - errorlog("EQ not setup"); - return; - } - if (!session.webAudios[webAudio].lowcut3) { - errorlog("EQ not setup"); - return; - } - session.webAudios[webAudio].lowcut1.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); - session.webAudios[webAudio].lowcut2.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); - session.webAudios[webAudio].lowcut3.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); - } -} - -function changeLowEQ(lowEQ, deviceid = null) { - log("LOW EQ"); - - for (var webAudio in session.webAudios) { - if (!session.webAudios[webAudio].lowEQ) { - errorlog("EQ not setup"); - return; - } - session.webAudios[webAudio].lowEQ.gain.setValueAtTime(lowEQ, session.webAudios[webAudio].audioContext.currentTime); - } -} - -function changeMidEQ(midEQ, deviceid = null) { - for (var webAudio in session.webAudios) { - if (!session.webAudios[webAudio].midEQ) { - errorlog("EQ not setup"); - return; - } - session.webAudios[webAudio].midEQ.gain.setValueAtTime(midEQ, session.webAudios[webAudio].audioContext.currentTime); - } -} - -function changeHighEQ(highEQ, deviceid = null) { - for (var webAudio in session.webAudios) { - if (!session.webAudios[webAudio].highEQ) { - errorlog("EQ not setup"); - return; - } - session.webAudios[webAudio].highEQ.gain.setValueAtTime(highEQ, session.webAudios[webAudio].audioContext.currentTime); - } -} - -function changeMicDelay(delay, deviceid = null) { - log("changeMicDelay :" + delay); - for (var waid in session.webAudios) { - // add a mic delay - if (!session.webAudios[waid].micDelay) { - errorlog("Mic Delay not setup"); - } else { - session.webAudios[waid].micDelay.delayTime.setValueAtTime(delay / 1000, session.webAudios[waid].audioContext.currentTime); - } - } -} - -function changeSubGain(gain, deviceid = null) { - if (gain !== false) { - gain = parseFloat(gain / 100.0) || 0; - } else { - gain = 1.0; - } - for (var webAudio in session.webAudios) { - try { - if (!session.webAudios[webAudio].subGainNodes) { - errorlog("EQ not setup"); - return; - } - if (deviceid in session.webAudios[webAudio].subGainNodes) { - session.webAudios[webAudio].subGainNodes[deviceid].gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); - } else { - errorlog("NOT FOUND:" + deviceid); - } - break; - } catch (e) { - errorlog(e); - } - } -} - -function changeMainGain(gain, fadeout = 0) { - for (var webAudio in session.webAudios) { - if (!session.webAudios[webAudio].gainNode) { - return; - } - if (gain !== false) { - gain = parseFloat(gain / 100.0) || 0; - } else { - gain = 1.0; - } - if (fadeout) { - try { - session.webAudios[webAudio].gainNode.gain.linearRampToValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime + fadeout / 1000); - } catch (e) { - session.webAudios[webAudio].gainNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); - } - } else { - session.webAudios[webAudio].gainNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); - } - } -} - -function changeGatingGain(gain, fadeout = 0) { - for (var webAudio in session.webAudios) { - if (!session.webAudios[webAudio].gatingNode) { - return; - } - if (gain !== false) { - gain = parseFloat(gain / 100.0) || 0; - } else { - gain = 1.0; - } - if (fadeout) { - try { - session.webAudios[webAudio].gatingNode.gain.linearRampToValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime + fadeout / 1000); - } catch (e) { - session.webAudios[webAudio].gatingNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); - } - } else { - session.webAudios[webAudio].gatingNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); - } - } -} - -function applyDownmixing(inputNode, webAudio) { - // Complex downmixing logic with channel counting and gain adjustment - let totalChannels = 0; - let activeChannels = 0; - let tracks = webAudio.audioContext._stream ? webAudio.audioContext._stream.getAudioTracks() : []; - - tracks.forEach(track => { - if (track.getSettings && track.getSettings().channelCount) { - let trackChannels = track.getSettings().channelCount; - totalChannels += trackChannels; - if (track.enabled) { - activeChannels += trackChannels; - } - } else { - // Fallback if getSettings is not available - totalChannels += 2; // Assume stereo - if (track.enabled) { - activeChannels += 2; - } - } - }); - - totalChannels = Math.max(totalChannels, 1); - activeChannels = Math.max(activeChannels, 1); - - webAudio.splitter = webAudio.audioContext.createChannelSplitter(totalChannels); - inputNode.connect(webAudio.splitter); - webAudio.merger = webAudio.audioContext.createChannelMerger(1); - - // Create a gain node for volume adjustment - webAudio.downmixGain = webAudio.audioContext.createGain(); - - // Connect splitter outputs to merger through the gain node - for (let i = 0; i < totalChannels; i++) { - webAudio.splitter.connect(webAudio.downmixGain, i, 0); - } - - webAudio.downmixGain.connect(webAudio.merger, 0, 0); - - // Set gain to 1 / sqrt(activeChannels) to maintain perceived loudness - let gainValue = 1 / Math.sqrt(activeChannels); - webAudio.downmixGain.gain.setValueAtTime(gainValue, webAudio.audioContext.currentTime); - - log(`Downmixing ${totalChannels} total channels (${activeChannels} active) to mono. Gain set to ${gainValue.toFixed(3)}`); - - return webAudio.merger; -} - -function applyLowCut(inputNode, webAudio) { - // Apply high-pass filter chain for low frequency cut - webAudio.lowcut1 = webAudio.audioContext.createBiquadFilter(); - webAudio.lowcut1.type = "highpass"; - webAudio.lowcut1.frequency.value = session.lowcut; - - webAudio.lowcut2 = webAudio.audioContext.createBiquadFilter(); - webAudio.lowcut2.type = "highpass"; - webAudio.lowcut2.frequency.value = session.lowcut; - - webAudio.lowcut3 = webAudio.audioContext.createBiquadFilter(); - webAudio.lowcut3.type = "highpass"; - webAudio.lowcut3.frequency.value = session.lowcut; - - inputNode.connect(webAudio.lowcut1); - webAudio.lowcut1.connect(webAudio.lowcut2); - webAudio.lowcut2.connect(webAudio.lowcut3); - - return webAudio.lowcut3; -} - -function applyVoiceChanger(inputNode, webAudio) { - function makeDistortionCurve(amount = 10) { - var sampleRate = webAudio.audioContext.sampleRate || 48000; - var curve = new Float32Array(sampleRate); - var x; - for (let i = 0; i < sampleRate; ++i) { - x = (i * 2) / sampleRate - 1; - curve[i] = ((3 + amount) * x * 20 * (Math.PI / 180)) / (Math.PI + amount * Math.abs(x)); - } - return curve; - } - - let waveShaper = webAudio.audioContext.createWaveShaper(); - waveShaper.curve = makeDistortionCurve(5); - - var realCoeffs = new Float32Array([1, 0]); - var imagCoeffs = new Float32Array([0, 1]); - - var numCoeffs = 20; // The more coefficients you use, the better the approximation - var realCoeffs = new Float32Array(numCoeffs); - var imagCoeffs = new Float32Array(numCoeffs); - - realCoeffs[0] = 0.5; - for (var i = 1; i < numCoeffs; i++) { - // note i starts at 1 - imagCoeffs[i] = (1 / (i * Math.PI)) * (1 - Math.random() / 2); - } - - let oscillator = webAudio.audioContext.createOscillator(); - oscillator.frequency.value = 10; - - const wave = webAudio.audioContext.createPeriodicWave(realCoeffs, imagCoeffs); - oscillator.setPeriodicWave(wave); - - let oscillatorGain = webAudio.audioContext.createGain(); - oscillatorGain.gain.value = 0.005; - oscillator.connect(oscillatorGain); - oscillator.start(0); - - let delay = webAudio.audioContext.createDelay(); - delay.delayTime.value = 0.01; - oscillatorGain.connect(delay.delayTime); - - let lowEQ = webAudio.audioContext.createBiquadFilter(); - lowEQ.type = "peaking"; - lowEQ.frequency.value = 200; - lowEQ.Q.value = 0.5; - lowEQ.gain.value = 6; - - let mid = webAudio.audioContext.createBiquadFilter(); - mid.type = "peaking"; - mid.frequency.value = 500; - mid.Q.value = 0.5; - mid.gain.value = -10; - - inputNode.connect(delay); - delay.connect(waveShaper); - waveShaper.connect(mid); - mid.connect(lowEQ); - - return lowEQ; -} - -function applyEqualizer(inputNode, webAudio) { - // https://webaudioapi.com/samples/frequency-response/ for a tool to help set values - webAudio.lowEQ = webAudio.audioContext.createBiquadFilter(); - webAudio.lowEQ.type = "lowshelf"; - webAudio.lowEQ.frequency.value = 100; - webAudio.lowEQ.gain.value = 0; - - webAudio.midEQ = webAudio.audioContext.createBiquadFilter(); - webAudio.midEQ.type = "peaking"; - webAudio.midEQ.frequency.value = 1000; - webAudio.midEQ.Q.value = 0.5; - webAudio.midEQ.gain.value = 0; - - webAudio.highEQ = webAudio.audioContext.createBiquadFilter(); - webAudio.highEQ.type = "highshelf"; - webAudio.highEQ.frequency.value = 10000; - webAudio.highEQ.gain.value = 0; - - inputNode.connect(webAudio.lowEQ); - webAudio.lowEQ.connect(webAudio.midEQ); - webAudio.midEQ.connect(webAudio.highEQ); - - return webAudio.highEQ; -} - -function micDelayNode(mediaStreamSource, audioContext) { - if (session.micDelay !== false) { - var delay = parseFloat(session.micDelay / 1000) || 0; - var delayNode = audioContext.createDelay(delay + 3); - } else { - var delay = 0; - var delayNode = audioContext.createDelay(3); - } - delayNode.delayTime.value = delay; - mediaStreamSource.connect(delayNode); - return delayNode; -} - -function audioGainNode(mediaStreamSource, audioContext) { - var gainNode = audioContext.createGain(); - if (session.audioGain !== false) { - var gain = parseFloat(session.audioGain / 100.0) || 0; - } else { - var gain = 1.0; - } - gainNode.gain.value = gain; - mediaStreamSource.connect(gainNode); - return gainNode; -} - -function audioGatingNode(mediaStreamSource, audioContext) { - var gateNode = audioContext.createGain(); - gateNode.gain.value = 1.0; - mediaStreamSource.connect(gateNode); - return gateNode; -} - -function audioMeter(mediaStreamSource, audioContext) { - var analyser = audioContext.createAnalyser(); - mediaStreamSource.connect(analyser); - analyser.fftSize = 256; - analyser.smoothingTimeConstant = 0.05; - - var bufferLength = analyser.frequencyBinCount; - var dataArray = new Uint8Array(bufferLength); - var timer = null; - - var meter1 = document.getElementById("meter1") || false; - var meter2 = document.getElementById("meter2") || false; - var meter3 = document.getElementById("meter3") || false; - var meter4 = document.getElementById("meter4") || false; - - var currentlyActive = 0; // mode 5 - - var ng1 = 10; - var ng2 = 25; - var ng3 = 30; - - if (session.noisegateSettings) { - if (session.noisegateSettings.length) { - ng1 = parseInt(session.noisegateSettings[0]) || 0; // gated volume target (lower to this level) - } - if (session.noisegateSettings.length > 1) { - ng2 = parseInt(session.noisegateSettings[1]) || ng2; // not loud (threshold level) - } - if (session.noisegateSettings.length > 2) { - ng3 = parseInt(session.noisegateSettings[2]) || 0; // stickiness; time (ms) - ng3 = ng3 / 100.0; // convert to the actual units (100ms) - } - } - if (session.noisegate) { - changeGatingGain(ng1, 200); - } - - function draw() { - try { - analyser.getByteFrequencyData(dataArray); - var total = 0; - for (var i = 0; i < dataArray.length; i++) { - total += dataArray[i]; - } - total = total / 100; - if (session.quietOthers && session.quietOthers == 2) { - if (total > 10) { - if (session.muted_activeSpeaker == false) { - session.muted_activeSpeaker = true; - session.speakerMuted = true; - clearTimeout(timer); - toggleSpeakerMute(true); // okay, sicne this is quietOthers - } - } else if (session.muted_activeSpeaker == true) { - session.speakerMuted = false; - session.muted_activeSpeaker = false; - session.activelySpeaking = false; - clearTimeout(timer); - timer = setTimeout(function () { - toggleSpeakerMute(true); - }, 250); // okay, sicne this is quietOthers - } - } - - if (session.pushLoudness == true) { - var loudnessObj = {}; - loudnessObj[session.streamID] = parseInt(total); - if (isIFrame) { - parent.postMessage({ loudness: loudnessObj, action: "loudness", value: total }, session.iframetarget); - } - } - - if (session.noisegate) { - if (total <= ng2) { - if (currentlyActive == ng3) { - changeGatingGain(ng1, 200); // set volume to 40% relative to what it is now. - log("GAIN LOWERED"); - currentlyActive = ng3 + 1; - } else if (currentlyActive < ng3) { - currentlyActive += 1; - } - } else if (currentlyActive == ng3 + 1) { - changeGatingGain(100, 200); - currentlyActive = 0; - log("GAIN INCREASED"); - } else { - currentlyActive = 0; - } - } - - if (meter1) { - if (document.getElementById("meter1")) { - if (total == 0) { - meter1.style.width = "1px"; - meter2.style.width = "0px"; - } else if (total <= 1) { - meter1.style.width = "1px"; - meter2.style.width = "0px"; - } else if (total <= 150) { - meter1.style.width = total + "px"; - meter2.style.width = "0px"; - } else if (total > 150) { - if (total > 200) { - total = 200; - } - meter1.style.width = "150px"; - meter2.style.width = total - 150 + "px"; - } - } else { - meter1 = false; - } - if (session.audioGain !== false) { - if (document.getElementById("previewWebcam")) { - changeMainGain(100); // full volume while in preview mode - } else { - changeMainGain(session.audioGain); - } - } - return; - } else if (toggleSettingsState && document.getElementById("meter3")) { - if (total == 0) { - meter3.style.width = "1px"; - meter4.style.width = "0px"; - } else if (total <= 1) { - meter3.style.width = "1px"; - meter4.style.width = "0px"; - } else if (total <= 150) { - meter3.style.width = total + "px"; - meter4.style.width = "0px"; - } else if (total > 150) { - if (total > 200) { - meter3.style.width = "150px"; - meter4.style.width = "50px"; - } else { - meter3.style.width = "150px"; - meter4.style.width = total - 150 + "px"; - } - } - if (document.getElementById("mutetoggle")) { - total *= 3; - if (total > 255) { - total = 255; - } - total = parseInt(total); - document.getElementById("mutetoggle").style.color = "rgb(" + (255 - total) + ",255," + (255 - total) + ")"; - } - meter1 = false; - return; - } else if (session.cleanOutput) { - meter1 = false; - return; - } else if (document.getElementById("mutetoggle")) { - total *= 3; - if (total > 255) { - total = 255; - } - total = parseInt(total); - document.getElementById("mutetoggle").style.color = "rgb(" + (255 - total) + ",255," + (255 - total) + ")"; - } else { - clearInterval(analyser.interval); - warnlog("METERS NOT FOUND"); - } - meter1 = false; - } catch (e) { - errorlog(e); - } - } - - analyser.interval = setInterval(function () { - draw(); - }, 100); - return analyser; -} - -function audioCompressor(mediaStreamSource, audioContext) { - var compressor = audioContext.createDynamicsCompressor(); - compressor.threshold.value = -40; - compressor.knee.value = 10; - compressor.ratio.value = 4; // 3 - compressor.attack.value = 0.002; // 0.001 - compressor.release.value = 0.1; // 0.06 - mediaStreamSource.connect(compressor); - return compressor; -} - -function audioLimiter(mediaStreamSource, audioContext) { - var compressor = audioContext.createDynamicsCompressor(); - compressor.threshold.value = -5; - compressor.knee.value = 0; - compressor.ratio.value = 20.0; // 1 to 20 - compressor.attack.value = 0.001; - compressor.release.value = 0.1; - mediaStreamSource.connect(compressor); - return compressor; -} - -function activeSpeaker(border = false) { - var lastActiveSpeaker = null; - - var someoneElseIfSpeaking = false; - var anyoneIsSpeaking = 0; - var defaultSpeaker = false; - var anyVideoAvailable = false; // Track if any video streams are available at all - var changed = false; - - // First pass: check if any video is available - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && - session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { - anyVideoAvailable = true; - break; - } - } - - for (var UUID in session.rpcs) { - - if (session.scene) { - let pass = checkMuteState(UUID); - // If no one is visible and this person has video, show them immediately - if (pass && !anyoneIsSpeaking && !defaultSpeaker && anyVideoAvailable === false && - session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && - session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { - session.rpcs[UUID].defaultSpeaker = true; - defaultSpeaker = true; - anyVideoAvailable = true; - changed = true; - continue; - } else if (pass) { - session.rpcs[UUID].activelySpeaking = false; - if (session.rpcs[UUID].defaultSpeaker && session.rpcs[UUID].defaultSpeaker !== true) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } - session.rpcs[UUID].defaultSpeaker = false; - continue; - } - } - - if (session.activeSpeaker > 2 && !(session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted)) { - session.rpcs[UUID].activelySpeaking = false; // we're not showing audio-only sources in this mode. - if (session.rpcs[UUID].defaultSpeaker && session.rpcs[UUID].defaultSpeaker !== true) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } - session.rpcs[UUID].defaultSpeaker = false; - continue; - } - - if (session.rpcs[UUID].stats._Audio_Loudness_average) { - if (session.rpcs[UUID].stats.Audio_Loudness && session.rpcs[UUID].stats.Audio_Loudness > 10) { - session.rpcs[UUID].stats._Audio_Loudness_average = parseFloat(session.rpcs[UUID].stats.Audio_Loudness * 0.07 + session.rpcs[UUID].stats._Audio_Loudness_average * 0.93); - } else { - session.rpcs[UUID].stats._Audio_Loudness_average = parseFloat(session.rpcs[UUID].stats._Audio_Loudness_average * 0.975); - } - } else { - session.rpcs[UUID].stats._Audio_Loudness_average = 1; - } - if (session.rpcs[UUID].stats._Audio_Loudness_average > 13) { - if (border) { - if (session.rpcs[UUID].videoElement) { - session.rpcs[UUID].videoElement.style.border = "green solid 1px"; - session.rpcs[UUID].videoElement.style.padding = "0"; - } - } else if (!session.rpcs[UUID].activelySpeaking) { - session.rpcs[UUID].activelySpeaking = true; - lastActiveSpeaker = UUID; - session.rpcs[UUID].stats._Audio_Loudness_average += 50; - } - } else if (session.rpcs[UUID].stats._Audio_Loudness_average > 6) { - // - } else { - if (border) { - if (session.rpcs[UUID].videoElement) { - session.rpcs[UUID].videoElement.style.border = ""; - session.rpcs[UUID].videoElement.style.padding = "1px"; - } - } else if (session.rpcs[UUID].activelySpeaking) { - session.rpcs[UUID].activelySpeaking = false; - lastActiveSpeaker = UUID; - } - } - if (session.rpcs[UUID].stats.Audio_Loudness > 13 || (session.rpcs[UUID].stats.Audio_Loudness > 5 && session.rpcs[UUID].stats._Audio_Loudness_average > 3) || session.rpcs[UUID].stats._Audio_Loudness_average > 6) { - someoneElseIfSpeaking = true; - } - if (session.rpcs[UUID].activelySpeaking) { - anyoneIsSpeaking += 1; - } - if (session.rpcs[UUID].defaultSpeaker === true) { - defaultSpeaker = true; - } - } - - var loudest = null; - var loudestActive = null; - - if (session.activeSpeaker === 1 || session.activeSpeaker === 3) { - // will only show one speaker at a time; the loudest or last-loud speaker - if (!anyoneIsSpeaking) { - if (defaultSpeaker) { - // already good to go. - } else if (lastActiveSpeaker) { - if (session.rpcs[lastActiveSpeaker].defaultSpeaker !== false) { - clearTimeout(session.rpcs[lastActiveSpeaker].defaultSpeaker); - } else { - changed = true; - log("lastActiveSpeaker is default"); - } - session.rpcs[lastActiveSpeaker].defaultSpeaker = true; - } else if (session.scene === false || (session.nopreview === false && session.minipreview !== 1)) { - // we don't need to care. - } else if (anyVideoAvailable === false) { - // Immediately select the first available video source if no one is currently visible - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && - session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - log(UUID + " is speaker now (no lull)"); - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } - } - // Fall through to original logic if needed - if (!changed) { - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - log(UUID + " is speaker now"); - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } - } - if (!changed && session.activeSpeaker <= 2) { - // switch to streams that have no video track - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].label) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - log(UUID + " is speaker now"); - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } else if (!changed) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - log(UUID + " is speaker now"); - } - session.rpcs[UUID].defaultSpeaker = true; - } - } - } - } - } else { - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - log(UUID + " is speaker now"); - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } - } - if (!changed && session.activeSpeaker <= 2) { - // switch to streams that have no video track - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].label) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - log(UUID + " is speaker now"); - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } else if (!changed) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - log(UUID + " is speaker now"); - } - session.rpcs[UUID].defaultSpeaker = true; - } - } - } - } - } else { - for (var UUID in session.rpcs) { - if (!("_Audio_Loudness_average" in session.rpcs[UUID].stats)) { - // never could have been loudest, since no loudness value. - continue; - } - - if (session.rpcs[UUID].activelySpeaking) { - if (!loudestActive) { - loudestActive = UUID; - } else if (session.rpcs[UUID].stats._Audio_Loudness_average > session.rpcs[loudestActive].stats._Audio_Loudness_average) { - if (session.rpcs[loudestActive].defaultSpeaker === true) { - if (!session.activeSpeakerTimeout) { - session.rpcs[loudestActive].defaultSpeaker = false; - changed = true; - log(loudestActive + " is loudest but not speaker anymore"); - } else { - session.rpcs[loudestActive].defaultSpeaker = setTimeout( - function (uuid) { - session.rpcs[uuid].defaultSpeaker = false; - updateMixer(); - }, - session.activeSpeakerTimeout, - loudestActive - ); - } - } - loudestActive = UUID; - } else if (session.rpcs[UUID].defaultSpeaker === true) { - if (!session.activeSpeakerTimeout) { - session.rpcs[UUID].defaultSpeaker = false; - changed = true; - log(UUID + " is not speaker anymore"); - } else { - session.rpcs[UUID].defaultSpeaker = setTimeout( - function (uuid) { - session.rpcs[uuid].defaultSpeaker = false; - updateMixer(); - }, - session.activeSpeakerTimeout, - UUID - ); - } - } - } else if (session.rpcs[UUID].defaultSpeaker === true) { - if (!session.activeSpeakerTimeout) { - session.rpcs[UUID].defaultSpeaker = false; - changed = true; - log(UUID + " is not speaker anymore"); - } else { - session.rpcs[UUID].defaultSpeaker = setTimeout( - function (uuid) { - session.rpcs[uuid].defaultSpeaker = false; - updateMixer(); - }, - session.activeSpeakerTimeout, - UUID - ); - } - } - } - - if (loudestActive && session.rpcs[loudestActive].defaultSpeaker !== true) { - if (session.rpcs[loudestActive].defaultSpeaker) { - clearTimeout(session.rpcs[loudestActive].defaultSpeaker); - } else { - changed = true; - } - for (let UUID in session.rpcs) { - if (loudestActive !== UUID) { - if (session.rpcs[UUID].defaultSpeaker === true) { - session.rpcs[UUID].defaultSpeaker = false; // Reset immediately before any new logic - changed = true; - } else if (session.rpcs[UUID].defaultSpeaker) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - session.rpcs[UUID].defaultSpeaker = false; - } - } - } - - session.rpcs[loudestActive].defaultSpeaker = true; - } - } - } else if (session.activeSpeaker === 2 || session.activeSpeaker === 4) { - // will show whoever is talking; mixed together; if no one is talking, just shows yourself - - if (!anyoneIsSpeaking) { - if (defaultSpeaker) { - // already good to go. - } else if (lastActiveSpeaker) { - if (session.rpcs[lastActiveSpeaker].defaultSpeaker !== false) { - clearTimeout(session.rpcs[lastActiveSpeaker].defaultSpeaker); - } else { - changed = true; - } - session.rpcs[lastActiveSpeaker].defaultSpeaker = true; - } else if (session.scene === false || (session.nopreview === false && session.minipreview !== 1)) { - // we don't need to care. - } else if (anyVideoAvailable === false) { - // Immediately select the first available video source if no one is currently visible - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && - session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - log(UUID + " is speaker now (no lull)"); - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } - } - // Fall through to original logic if needed - if (!changed) { - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } - } - if (!changed && session.activeSpeaker <= 2) { - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].label) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } else if (!changed) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - } - session.rpcs[UUID].defaultSpeaker = true; - } - } - } - } - } else { - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } - } - if (!changed && session.activeSpeaker <= 2) { - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].label) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - } - session.rpcs[UUID].defaultSpeaker = true; - break; - } else if (!changed) { - if (session.rpcs[UUID].defaultSpeaker !== false) { - clearTimeout(session.rpcs[UUID].defaultSpeaker); - } else { - changed = true; - } - session.rpcs[UUID].defaultSpeaker = true; - } - } - } - } - } else { - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].activelySpeaking && !session.rpcs[UUID].defaultSpeaker) { - session.rpcs[UUID].defaultSpeaker = true; - changed = true; - } else if (!session.rpcs[UUID].activelySpeaking && session.rpcs[UUID].defaultSpeaker) { - if (!session.activeSpeakerTimeout) { - session.rpcs[UUID].defaultSpeaker = false; - changed = true; - } else if (session.rpcs[UUID].defaultSpeaker === true) { - session.rpcs[UUID].defaultSpeaker = setTimeout( - function (uuid) { - session.rpcs[uuid].defaultSpeaker = false; - updateMixer(); - }, - session.activeSpeakerTimeout, - UUID - ); - } - } - } - } - } - if (session.quietOthers && session.quietOthers === 1) { - if (someoneElseIfSpeaking) { - if (session.muted_activeSpeaker == false) { - session.muted_activeSpeaker = true; - session.muted = true; - toggleMute(true); - } - } else if (session.muted_activeSpeaker == true) { - session.muted = false; - session.muted_activeSpeaker = false; - toggleMute(true); - } - } else if (session.quietOthers && session.quietOthers === 3) { - // purely for fun. It's the opposite of a noise-gate I guess. - if (someoneElseIfSpeaking) { - if (session.muted_activeSpeaker == false) { - session.muted_activeSpeaker = true; - session.speakerMuted = true; - toggleSpeakerMute(true); // okay, sicne this is quietOthers - } - } else if (session.muted_activeSpeaker == true) { - session.speakerMuted = false; - session.muted_activeSpeaker = false; - toggleSpeakerMute(true); // okay, sicne this is quietOthers - } - } - - if (changed) { - setTimeout(function () { - updateMixer(); - }, 0); - } -} - -function randomizeArray(unshuffled) { - var arr = unshuffled - .map(a => ({ - sort: Math.random(), - value: a - })) - .sort((a, b) => a.sort - b.sort) - .map(a => a.value); // shuffle once - - for (var i = arr.length - 1; i > 0; i--) { - // shuffle twice - var j = Math.floor(Math.random() * (i + 1)); - var tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } - return arr; -} - -async function joinRoom(roomname) { - if (roomname.length) { - roomname = sanitizeRoomName(roomname); - log("Join room: " + roomname); - - // In auth mode, use auth-aware room joining - if (session.authMode && window.vdoAuth) { - const hasAccess = await window.vdoAuth.joinRoom(roomname); - if (!hasAccess) { - return; // Access denied or auth required - } - // Room ID might have changed if it was an alias - roomname = session.roomid; - } - - updateVolume(false); // chance of a race condition, but unlikely and not a big deal if so. - session.joinRoom(roomname).then( - function (response) { - // callback from server; we've joined the room. Just the listing is returned - - if (session.joiningRoom === "seedPlz") { - // allow us to seed, now that we have joined the room. - session.joiningRoom = false; // joined - session.seedStream(); - } else { - session.joiningRoom = false; // no seeding callback - } - - // Create universal token for directors in auth mode - if (session.director && session.authMode && session.authToken && !session.universalViewToken) { - vdoAuth.createUniversalToken().then(() => { - if (session.universalViewToken) { - updateAllSoloLinks(); - } - }); - } - - // Apply any pending room settings selected pre-join (access mode, allowlist) - if (session.director && session.authMode && window.vdoAuth && session.authToken && session.pendingRoomSettings) { - try { - window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, session.pendingRoomSettings); - } catch (e) { console.error(e); } - // Clear once applied - session.pendingRoomSettings = null; - } - var token = ""; - if (session.token) { - token += "&token=" + session.token; - } - - if (!session.cleanOutput) { - if (session.roomhost) { - if (session.defaultPassword === false) { - if (session.password === false) { - var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + "&password=false" + token; - warnUser("You can invite others with:\n\n" + invite + "", false, false); - } else { - generateHash(session.password + session.salt, 4).then(function (hash) { - // change the hash length from 4 to 3 when VDO.Ninja v24.10 or newer is in production. - var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + "&hash=" + hash + token; - warnUser("You can invite others with:\n\n" + invite + "", false, false); - }); - } - } else { - var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + token; - warnUser("You can invite others with:\n\n" + invite + "", false, false); - } - } - } - - log("Members in Room"); - log(response); - - if (session.randomize === true) { - response = randomizeArray(response); - log("Randomized List of Viewers"); - log(response); - for (var i in response) { - if ("UUID" in response[i]) { - if (response[i].streamID) { - if (response[i].UUID in session.rpcs) { - log("RTC already connected"); /// lets just say instead of Stream, we have - } else { - log(response[i].streamID); - var streamID = session.desaltStreamID(response[i].streamID); - if (session.queue) { - if (session.directorList.indexOf(response[i].UUID) >= 0) { - // Only queueType 2 (&screen) sees director immediately. - // queueType 3 (&hold) and 4 (&holdwithvideo) are fully isolated - // from the director until activated. - if (session.queueType == 2) { - warnlog("PLAYING DIRECTOR"); - play(streamID, response[i].UUID); - } - } else if (session.view_set && session.view_set.includes(streamID)) { - play(streamID, response[i].UUID); - } else if (session.queueList.length < 5000) { - if (!(streamID in session.watchTimeoutList) && !session.queueList.includes(streamID)) { - session.queueList.push(streamID); - } - } - } else { - log("STREAM ID DESALTED 3: " + streamID); - setTimeout( - function (sid) { - play(sid); - }, - Math.floor(Math.random() * 100), - streamID - ); // add some furtherchance with up to 100ms added latency - } - } - } - } - } - } else { - for (var i in response) { - if ("UUID" in response[i]) { - if (response[i].streamID) { - if (response[i].UUID in session.rpcs) { - log("RTC already connected"); /// lets just say instead of Stream, we have - } else { - log(response[i].streamID); - var streamID = session.desaltStreamID(response[i].streamID); - if (session.queue) { - if (session.directorList.indexOf(response[i].UUID) >= 0) { - // Only queueType 2 (&screen) sees director immediately. - // queueType 3 (&hold) and 4 (&holdwithvideo) are fully isolated - // from the director until activated. - if (session.queueType == 2) { - play(streamID, response[i].UUID); - } - } else if (session.view_set && session.view_set.includes(streamID)) { - play(streamID, response[i].UUID); - } else if (session.queueList.length < 5000) { - if (!(streamID in session.watchTimeoutList) && !session.queueList.includes(streamID)) { - session.queueList.push(streamID); - } - } - } else { - log("STREAM ID DESALTED 4: " + streamID); - play(streamID, response[i].UUID); // play handles the group room mechanics here - } - } - } - } - } - } - updateQueue(); - pokeIframeAPI("joined-room-complete"); - - if (session.include.length) { - // we want to request what hasn't been requested already, since we are joining a room. - session.include.forEach(sid => { - if (sid in session.waitingWatchList) { - return; - } else { - session.watchStream(sid); - } - }); - } - }, - function (error) { - return {}; - } - ); - } else { - log("Room name not long enough or contained all bad characaters"); - } -} - -async function createRoom(roomname = false, reload = false) { - if (reload === true) { - let oldDirectorSettings = getStorage("directorOtherSettings"); - var passwordRoom = oldDirectorSettings.password; - if (passwordRoom === session.defaultPassword) { - passwordRoom = ""; - } else if (passwordRoom === false) { - passwordRoom = ""; - session.password = false; - } - roomname = oldDirectorSettings.roomid; - if (!roomname) { - warnUser("Couldn't load previous session"); - return; - } - if (urlParams.has("dir")) { - updateURL("dir=" + roomname, true, false); // make the link reloadable. - } else { - updateURL("director=" + roomname, true, false); // make the link reloadable. - } - - session.codecGroupFlag = session.codecGroupFlag || oldDirectorSettings.codecGroupFlag || session.codecGroupFlag; - - session.label = session.label || oldDirectorSettings.label || session.label; - session.codecGroupFlag = session.codecGroupFlag || oldDirectorSettings.codecGroupFlag || session.codecGroupFlag; - session.showDirector = session.showDirector || oldDirectorSettings.showDirector || session.showDirector; - - if (oldDirectorSettings.broadcast) { - getById("broadcastFlag").checked = true; - } - if (session.showDirector) { - getById("showdirectorFlag").checked = true; - } - } else { - if (roomname == false) { - roomname = getById("videoname1").value; - roomname = sanitizeRoomName(roomname); - - clearDirectorSettings(); - - if (roomname.length != 0) { - if (urlParams.has("dir")) { - updateURL("dir=" + roomname, true, false); // make the link reloadable. - } else { - updateURL("director=" + roomname, true, false); // make the link reloadable. - } - } - } - if (roomname.length == 0) { - //if (!(session.cleanOutput)) { - // warnUser("Please enter a room name before continuing"); - //} - - getById("videoname1").focus(); - getById("videoname1").classList.remove("shake"); - setTimeout(function () { - getById("videoname1").classList.add("shake"); - }, 10); - - return; - } - log(roomname); - - var passwordRoom = document.getElementById("passwordRoom") ? sanitizePassword(document.getElementById("passwordRoom").value) : ""; - - // Pre-join SSO room setup (optional) - try { - var ssoBox = getById('useSSOForRoom'); - if (ssoBox && ssoBox.checked) { - // Enable auth mode for this room - session.authMode = true; - // Director should sign in before managing the room - // Note: join gating handled by vdoAuth.joinRoom in joinRoom() - // Capture desired access mode to apply after join - var selected = document.querySelector('input[name="ssoAccessMode"]:checked'); - var accessMode = (selected && selected.value) ? selected.value : 'public'; - var allowlist = []; - if (accessMode === 'allowlist') { - var csv = (getById('preAllowlistCSV') && getById('preAllowlistCSV').value) ? getById('preAllowlistCSV').value : ''; - if (csv) { - allowlist = csv.split(',').map(x => x.trim()).filter(x => x.length > 0); - } - } - // Store to apply after join - session.pendingRoomSettings = { accessMode: accessMode, allowlist: allowlist }; - // If guests must sign in (authenticated/allowlist), mark as requireAuth for UX - if (accessMode === 'authenticated' || accessMode === 'allowlist') { - session.requireAuth = true; - } - } - } catch (e) { } - } - - session.roomid = roomname; - getById("dirroomid").innerHTML = decodeURIComponent(session.roomid); - getById("roomid").innerHTML = session.roomid; - - var passAdd = ""; - var passAdd2 = ""; - - if (passwordRoom.length) { - session.password = passwordRoom; - session.defaultPassword = false; - - if (session.password === "false" || session.password === "0" || session.password === "off") { - session.password = false; - if (urlParams.has("pass")) { - updateURL("pass=0"); - passAdd = "&pass=0"; - passAdd2 = "&pass=0"; - } else if (urlParams.has("pw")) { - updateURL("pw=0"); - passAdd = "&pw=0"; - passAdd2 = "&pw=0"; - } else if (urlParams.has("p")) { - updateURL("p=0"); - passAdd = "&p=0"; - passAdd2 = "&p=0"; - } else if (urlParams.has("password")) { - updateURL("password=false"); - passAdd = "&password=false"; - passAdd2 = "&password=false"; - } else { - updateURL("p=0"); - passAdd = "&p=0"; - passAdd2 = "&p=0"; - } - } else { - if (urlParams.has("pass")) { - updateURL("pass=" + session.password); - } else if (urlParams.has("pw")) { - updateURL("pw=" + session.password); - } else if (urlParams.has("p")) { - updateURL("p=" + session.password); - } else { - updateURL("password=" + session.password); - } - } - } - - await registerToken(); - - if (session.defaultPassword === false && session.password) { - passAdd2 = "&password=" + session.password; - return generateHash(session.password + session.salt, 4) - .then(async function (hash) { - passAdd = "&hash=" + hash; - await createRoomCallback(passAdd, passAdd2); - }) - .catch(errorlog); - } else if (session.defaultPassword === false && session.password === false) { - passAdd = "&p=0"; - passAdd2 = "&p=0"; - await createRoomCallback(passAdd, passAdd2); - } else { - await createRoomCallback(passAdd, passAdd2); - } -} - -function copyVideoFrameToClipboard(videoElement, e = false) { - try { - var canvas = document.createElement("canvas"); - - canvas.width = videoElement.videoWidth; - canvas.height = videoElement.videoHeight; - - var ctx = canvas.getContext("2d"); - ctx.drawImage(videoElement, 0, 0); - - var img = new Image(); - img.src = canvas.toDataURL(); - - canvas.toBlob(function (blob) { - navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]); - }, "image/png"); - - popupMessage(e, "Frame copied to clipboard as as PNG Image"); - } catch (e) { - errorlog(e); - } -} - -function saveVideoFrameToDisk(videoElement, e = false, filename = false) { - try { - var canvas = document.createElement("canvas"); - - canvas.width = videoElement.videoWidth; - canvas.height = videoElement.videoHeight; - - var ctx = canvas.getContext("2d"); - ctx.drawImage(videoElement, 0, 0); - - var img = new Image(); - img.src = canvas.toDataURL(); - - canvas.toBlob(function (blob) { - var link = document.createElement("a"); - if (filename) { - link.download = filename; - } else if (e) { - link.download = (videoElement.id || "video") + "_" + parseInt(performance.now()) + ".png"; - } else { - link.download = (videoElement.id || "video") + "_" + parseInt(Date.now()) + ".png"; - } - link.href = URL.createObjectURL(blob); - link.click(); - URL.revokeObjectURL(link.href); - }, "image/png"); - if (e) { - popupMessage(e, "Saving current frame to disk"); - } - } catch (e) { - errorlog(e); - } -} - -function sendVideoFrameToIframe(videoElement, e = false, request = {}) { - try { - var canvas = document.createElement("canvas"); - - canvas.width = videoElement.videoWidth; - canvas.height = videoElement.videoHeight; - - var ctx = canvas.getContext("2d"); - ctx.drawImage(videoElement, 0, 0); - - var img = new Image(); - img.src = canvas.toDataURL(); - - canvas.toBlob(function (blob) { - var response = {}; - response.imageData = blob; - response.imageType = "png"; - if (request.streamID) { - response.streamID = request.streamID; - } - if (request.UUID) { - response.UUID = request.UUID; - } - if (request.cib) { - response.cib = request.cib; - } - if (videoElement.id) { - response.videoID = videoElement.id; - } - - pokeIframeAPI("image-frame-capture", response); - }, "image/png"); - } catch (e) { - errorlog(e); - } -} - -function isLivePeerConnection(pc) { - if (!pc) { - return false; - } - var state = pc.connectionState || pc.iceConnectionState || ""; - if (!state) { - return true; - } - state = state.toLowerCase(); - return !(state === "failed" || state === "disconnected" || state === "closed"); -} - -async function checkDirectorStreamID() { - if (session.directorStreamID) { - for (var UUID in session.rpcs) { - if (!isLivePeerConnection(session.rpcs[UUID])) { - continue; - } - if (session.rpcs[UUID].streamID) { - var hashedSID = await generateHash(session.rpcs[UUID].streamID); - if (hashedSID === session.directorStreamID) { - session.directorUUID = UUID; // main director - session.directorList = []; - session.directorList.push(UUID); // approved co/directors - session.directorUUID = UUID; - session.newMainDirectorSetup(); - return; - } - } - } - for (var UUID in session.pcs) { - if (!isLivePeerConnection(session.pcs[UUID])) { - continue; - } - if (session.pcs[UUID].streamID) { - var hashedSID = await generateHash(session.pcs[UUID].streamID); - if (hashedSID === session.directorStreamID) { - session.directorList = []; - session.directorList.push(UUID); - session.directorUUID = UUID; - session.newMainDirectorSetup(); - return; - } - } - } - if (session.streamID == session.directorStreamID) { - session.directorState = true; - session.directorUUID = false; - pokeAPI("director", true); - pokeIframeAPI("director", true); - warnlog("You are joining with a token, but are the director?"); - } - session.directorList = []; - } -} - -async function checkToken() { - // this lets us use a server+password validation method for the director. - if (!session.token) { - return; - } - if (!session.roomid) { - return; - } - if (session.mainDirectorPassword) { - return; - } - - try { - var request = new XMLHttpRequest(); - - var hashedRoom = session.roomid; - if (session.password) { - hashedRoom += session.password; - } - hashedRoom += "i^4&u#Fz5Eu#MsK^chF5*XAEYi1g"; - hashedRoom = await generateHash(hashedRoom); - hashedRoom = hashedRoom.slice(0, 50); - - request.open("GET", "https://tokens.vdo.ninja/?token=" + session.token + "&room=" + hashedRoom, false); - request.send(null); - - if (request.status === 200) { - try { - var result = JSON.parse(request.responseText); - if ("UUID" in result) { - session.directorUUID = result.UUID; - session.directorList = []; - session.directorList.push(session.directorUUID); - session.directorStreamID = false; - session.newMainDirectorSetup(); - } else if ("streamID" in result) { - session.directorStreamID = result.streamID; - checkDirectorStreamID(); - } - } catch (e) { - session.directorUUID = false; - session.directorStreamID = false; - session.directorList = []; - errorlog(e); - } - } else { - session.directorUUID = false; - session.directorStreamID = false; - session.directorList = []; - errorlog("Didn't get a token response"); - } - } catch (e) { - errorlog(e); - } -} - -async function registerToken() { - // this lets us use a server+password validation method for the director. - if (!session.roomid) { - return; - } - if (!session.streamID) { - return; - } - if (!session.mainDirectorPassword) { - return; - } - - var longToken = session.mainDirectorPassword + "3wJVW^5qYU4DxGi6VhxN6RF04Q%$"; // this lets us use the same token across multiple rooms - var hashedToken = await generateHash(longToken); // keep it anonymous - hashedToken = hashedToken.slice(0, 50); - - var hashedRoom = session.roomid; - if (session.password) { - hashedRoom += session.password; - } - hashedRoom += "i^4&u#Fz5Eu#MsK^chF5*XAEYi1g"; - hashedRoom = await generateHash(hashedRoom); - hashedRoom = hashedRoom.slice(0, 50); - - var data2send = {}; - var hashedSID = await generateHash(session.streamID); - data2send.streamID = hashedSID; // not sure if there's a way around this. - data2send = JSON.stringify(data2send); - - var request = new XMLHttpRequest(); - request.open("POST", "https://tokens.vdo.ninja/?token=" + hashedToken + "&room=" + hashedRoom, false); - console.log("https://tokens.vdo.ninja/?token=" + hashedToken + "&room=" + hashedRoom); - request.send(data2send); - - if (request.status === 200) { - try { - if (request.responseText && request.responseText.length === 16) { - session.token = request.responseText; - console.log("share token: " + session.token); - session.directorState = true; - pokeAPI("director", true); - pokeIframeAPI("director", true); - } - } catch (e) { - session.directorState = false; - pokeAPI("director", false); - pokeIframeAPI("director", false); - } - } else { - session.directorState = false; - pokeAPI("director", false); - pokeIframeAPI("director", false); - } -} - -function hideDirectorinvites(ele, skip = true) { - if (getById("directorLinks2").style.display == "none") { - ele.innerHTML = ' LINKS (GUEST INVITES & SCENES)'; - getById("directorLinks2").style.display = "inline-block"; - getById("customizeLinks").classList.remove("hidden"); - } else { - ele.innerHTML = ' LINKS (GUEST INVITES & SCENES)'; - getById("directorLinks2").style.display = "none"; - getById("help_directors_room").style.display = "none"; - getById("roomnotes2").style.display = "none"; - getById("customizeLinks").classList.add("hidden"); - } - if (getById("directorLinks1").style.display == "none") { - getById("directorLinks1").style.display = "inline-block"; - getById("customizeLinks").classList.remove("hidden"); - } else { - getById("directorLinks1").style.display = "none"; - getById("help_directors_room").style.display = "none"; - getById("roomnotes2").style.display = "none"; - getById("customizeLinks").classList.add("hidden"); - } - if (skip) { - saveDirectorSettings(); - } -} - -function toggleCoDirector_changeurl(ele) { - session.codirector_changeURL = ele.checked; // doesn't do anything yet though. -} - -function toggleCoDirector_transfer(ele) { - session.codirector_transfer = ele.checked; -} - -function updateConfirmAlt(context, inputText) { - try { - if (!context) { return; } - var ctx = ("" + context).replace(/["<>]/g, ""); - var modal = document.querySelector('.promptModal[data-context="' + ctx + '"]'); - if (!modal) { return; } - var text = "" + ("" + inputText).replace("\n", "
    ") + ""; - text = text.replace(/\n/g, "
    "); - var msg = modal.querySelector('.promptModalMessage'); - if (msg) { msg.innerHTML = text; } - } catch (e) { /* noop */ } -} - -function toggleCoDirector_approve(ele) { - // UI label: "Allow co-directors to approve held guests" - // Checked means approvals allowed; unchecked means disabled - return; -} - -// Route approvals are default; no UI toggle needed anymore. - -function toggleApprovalPopup(ele) { - session.approval_popup = ele.checked; - try { - var token = ""; - if (session.token) { token += "&token=" + session.token; } - var url = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token; - if (session.approval_popup) { url += "&approvepopup"; } - try { console.log("[flags] toggled approval_popup=" + session.approval_popup + "; co-director invite=" + url); } catch (e) { } - if (session.password !== session.sitePassword) { - if (session.password === false) { url += "&password=false"; } - else { url += "&password=" + session.password; } - } - if (getById("codirectorSettings_invite")) { - getById("codirectorSettings_invite").value = url; - } - } catch (e) { /* noop */ } -} - -async function toggleCoDirector(ele) { - //session.coDirectorAllowed = ele.checked; - if (!ele.checked) { - getById("codirectorSettings").style.display = "none"; - return; - } - if (!session.directorPassword) { - session.directorPassword = await promptAlt(getTranslation("enter-new-codirector-password"), false); - if (!session.directorPassword) { - session.directorPassword = false; - ele.checked = false; - return; - } - session.directorPassword = sanitizePassword(session.directorPassword); - } - updateURL("codirector=" + session.directorPassword, true, false); - getById("coDirectorEnableSpan").style.display = "none"; - - await generateHash(session.directorPassword + session.salt + "abc123", 12) - .then(function (hash) { - // million to one error. - log("dir room hash is " + hash); - session.directorHash = hash; - return; - }) - .catch(errorlog); - - if (session.codirector_transfer) { - getById("codirectorSettings_transfer").checked = true; - } else { - getById("codirectorSettings_transfer").checked = false; - } - if (session.codirector_changeURL) { - getById("codirectorSettings_changeurl").checked = true; - } else { - getById(codirectorSettings_changeurl).checked = false; - } - - var token = ""; - if (session.token) { - token += "&token=" + session.token; - } - - getById("codirectorSettings_invite").value = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token; - if (session.approval_popup) { - getById("codirectorSettings_invite").value += "&approvepopup"; - } - if (session.password !== session.sitePassword) { - if (session.password === false) { - getById("codirectorSettings_invite").value += "&password=false"; - } else { - getById("codirectorSettings_invite").value += "&password=" + session.password; - } - } - - getById("codirectorSettings").style.display = "block"; -} - -function getParentHostname() { - const parentUrl = document.referrer; - if (parentUrl) { - const url = new URL(parentUrl); - return url.hostname; - } - return null; -} - -async function toggleWidgetURL(ele) { - if (ele.id === "widgetURL") { - ele = getById("widgetURCheck"); - } else if (!ele.checked) { - getById("widgetURL").classList.add("hidden"); - session.widget = false; - - var data = {}; - data.widgetSrc = false; - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowWidget === true) { - session.sendMessage(data, UUID); - } - } - - if (session.director) { - let widget = document.getElementById("widget"); - if (widget) { - getById("widget").remove(); - getById("directorlayout").classList.remove("widget"); - getById("directorlayout").classList.remove("left"); - } - } - pokeIframeAPI("widget-src", session.widget); - return; - } - var widget = await promptAlt(getTranslation("enter-url-for-widget"), false, false, session.widget); - if (widget !== null) { - session.widget = widget; - } - if (session.widget) { - getById("widgetURL").value = session.widget; - getById("widgetURL").classList.remove("hidden"); - updateMixer(); - } else { - session.widget = false; - getById("widgetURL").classList.add("hidden"); - ele.checked = false; - } - - var data = {}; - data.widgetSrc = session.widget; - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowWidget === true) { - session.sendMessage(data, UUID); - } - } - - if (session.director) { - let widget = document.getElementById("widget"); - if (!widget && session.widget && session.iFramesAllowed) { - widget = document.createElement("iframe"); - widget.id = "widget"; - widget = loadIframe(parseURL4Iframe(session.widget), widget); - if (widget) { - getById("directorlayout").classList.add("widget"); - - if (session.widgetleft) { - widget.classList.add("left"); - getById("directorlayout").classList.add("left"); - } - - log(widget.src); - document.body.appendChild(widget); - } - } else if (session.widget && widget && session.iFramesAllowed) { - loadIframe(parseURL4Iframe(session.widget), widget); - } else if (widget) { - getById("widget").remove(); - getById("directorlayout").classList.remove("widget"); - getById("directorlayout").classList.remove("left"); - } - } - - pokeIframeAPI("widget-src", session.widget); -} - -async function createRoomCallback(passAdd, passAdd2) { - if (session.meshcast) { - if (!session.cleanOutput && !session.cleanDirector) { - document.getElementById("meshcastMenu").classList.remove("hidden"); - } - } - - if (!session.switchMode) { - getById("directorlayout").classList.remove("hidden"); - getById("gridlayout").classList.add("hidden"); - } - - var broadcastFlag = getById("broadcastFlag"); - try { - if (broadcastFlag.checked) { - broadcastFlag = true; - } else { - broadcastFlag = false; - } - } catch (e) { - broadcastFlag = false; - } - - var broadcastString = ""; - if (broadcastFlag) { - broadcastString = "&broadcast"; - getById("broadcastSlider").checked = true; - //customizeLinks1 - //saveDirectorSettings - } - - var wss = ""; - if (session.wssSetViaUrl) { - if (session.customWSS && session.customWSS !== true) { - wss = "&pie=" + session.customWSS; - } else if (session.customWSS == true) { - wss = "&wss=" + session.wss; - } else { - wss = "&wss2=" + session.wss; - } - } - - var queue = ""; - if (session.queue) { - queue = "&queue"; - getById("directorLinks2").style.opacity = "0.2"; - getById("directorLinks2").style.pointerEvents = "none"; - getById("directorLinks2").style.cursor = "not-allowed"; - } - - var showdirectorFlag = getById("showdirectorFlag"); - try { - if (showdirectorFlag.checked) { - showdirectorFlag = true; - } else { - showdirectorFlag = false; - } - } catch (e) { - showdirectorFlag = false; - } - - if (showdirectorFlag) { - updateURL("showdirector", true, false); - session.showDirector = session.showDirector || true; - //getById("broadcastSlider").checked=true; - } - - var codecGroupFlag = getById("codecGroupFlag"); - - if (session.codecGroupFlag) { - codecGroupFlag = session.codecGroupFlag || ""; - } else if (codecGroupFlag) { - if (codecGroupFlag.value) { - if (codecGroupFlag.value === "vp9") { - codecGroupFlag = "&codec=vp9"; - getById("codech264toggle").disabled = true; - } else if (codecGroupFlag.value === "h264") { - codecGroupFlag = "&codec=h264"; - getById("codech264toggle").checked = true; - } else if (codecGroupFlag.value === "vp8") { - codecGroupFlag = "&codec=vp8"; - getById("codech264toggle").disabled = true; - } else if (codecGroupFlag.value === "av1") { - codecGroupFlag = "&codec=av1"; - getById("codech264toggle").disabled = true; - } else { - codecGroupFlag = ""; - } - } else { - codecGroupFlag = ""; - } - - session.codecGroupFlag = session.codecGroupFlag || codecGroupFlag || session.codecGroupFlag; - } - if (session.bitrateGroupFlag) { - codecGroupFlag += session.bitrateGroupFlag; - } - - stashRoomSession(broadcastFlag); - - formSubmitting = false; - try { - var m = getById("mainmenu"); - m.remove(); - document.querySelectorAll(".hidden2").forEach(ele2 => { - ele2.classList.remove("hidden2"); - }); - } catch (e) { } - - getById("head1").className = "hidden"; - getById("head2").className = "hidden"; - getById("head4").className = ""; - - try { - if (session.label === false) { - document.title = "Control Room"; - } - } catch (e) { - errorlog(e); - } - - session.director = true; - screensharesupport = false; - - if (session.meterStyle === false) { - session.meterStyle = 1; // director specific style - } - if (session.signalMeter === null) { - session.signalMeter = true; - } - if (session.batteryMeter === null) { - session.batteryMeter = true; - } - - if (session.directorPassword) { - getById("coDirectorEnable").checked = true; - getById("coDirectorEnableSpan").style.display = "none"; - - var token = ""; - if (session.token) { - token += "&token=" + session.token; - } - - getById("codirectorSettings_invite").value = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token; - if (session.approval_popup) { - getById("codirectorSettings_invite").value += "&approvepopup"; - } - if (session.password !== session.sitePassword) { - if (session.password == false) { - getById("codirectorSettings_invite").value += "&password=false"; - } else { - getById("codirectorSettings_invite").value += "&password=" + session.password; - } - } - - if (session.codirector_transfer) { - getById("codirectorSettings_transfer").checked = true; - } else { - getById("codirectorSettings_transfer").checked = false; - } - if (session.codirector_changeURL) { - getById("codirectorSettings_changeurl").checked = true; - } else { - getById("codirectorSettings_changeurl").checked = false; - } - getById("codirectorSettings").style.display = "block"; - } - - window.onresize = updateMixer; - window.onorientationchange = function () { - if (Firefox) { - updateForceRotate(true); - } - setTimeout(async function () { - if (session.forceAspectRatio) { - await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - } - if (session.effect && session.effect === "7") { - digitalZoom(); - } - updateForceRotate(); - updateMixer(); - }, 200); - }; - getById("reshare").parentNode.removeChild(getById("reshare")); - - //getById("mutespeakerbutton").style.display = null; - if (session.speakerMuted_default === false) { - //session.speakerMuted = false; // the director will start with audio playback muted. - toggleSpeakerMute(true); // let it be what it is. - } else { - session.speakerMuted = true; // the director will start with audio playback muted. - toggleSpeakerMute(true); // okay since only run on start - } - - var token = ""; - if (session.token) { - token += "&token=" + session.token; - } - - // Add auth parameters if in auth mode - var authParams = ""; - if (session.authMode) { - authParams = "&auth=true"; - - // Create universal token for scene links if we're authenticated - if (session.authToken && !session.universalViewToken) { - vdoAuth.createUniversalToken().then(() => { - // Update all links once token is created - if (session.universalViewToken) { - // Update scene link with universal token - var sceneAuthParams = "&universaltoken=" + session.universalViewToken; - getById("director_block_3").dataset.raw = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; - getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; - getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; - - // Update all solo links - updateAllSoloLinks(); - } - }); - } - } - - getById("director_block_1").dataset.raw = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams; - getById("director_block_1").href = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams; - getById("director_block_1").innerText = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams; - - // For scene links, use universal token if available - var sceneAuthParams = ""; - if (session.authMode && session.universalViewToken) { - sceneAuthParams = "&universaltoken=" + session.universalViewToken; - } else if (session.authMode) { - sceneAuthParams = authParams; - } - - getById("director_block_3").dataset.raw = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; - getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; - getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; - - if (session.cleanDirector == false && session.cleanOutput == false) { - getById("roomHeader").style.display = ""; - //getById("directorLinks").style.display = ""; - getById("directorLinks1").style.display = "inline-block"; - getById("directorLinks2").style.display = "inline-block"; - - getById("calendarButton").style.display = "inline-block"; - } else { - getById("guestFeeds").innerHTML = ""; - } - - getById("guestFeeds").style.display = ""; - - if (!session.cleanOutput) { - if (session.queue) { - getById("queuebutton").classList.remove("hidden"); - } - getById("chatbutton").classList.remove("hidden"); - getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. - getById("controlButtons").classList.remove("hidden"); - // getById("legal").classList.remove("hidden"); - getById("mutespeakerbutton").classList.remove("hidden"); - getById("websitesharebutton").classList.remove("hidden"); - //getById("screensharebutton").classList.remove("hidden"); - - if (session.totalRoomBitrate) { - getById("roomsettingsbutton").classList.remove("hidden"); - } - - if (!session.showDirector) { - // if null or false, we want to show the solo link, since the director won't have their control box. The director will be visible in their solo link - getById("miniPerformer").innerHTML = ''; - //miniTranslate(getById("miniPerformer")); - - // Use soloLinkGenerator to get proper auth parameters - var directorSoloLink = soloLinkGenerator(session.streamID, true); - getById("grabDirectorSoloLink").dataset.raw = directorSoloLink; - getById("grabDirectorSoloLink").href = directorSoloLink; - getById("grabDirectorSoloLink").innerText = directorSoloLink; - getById("grabDirectorSoloLinkParent").classList.remove("hidden"); - } else { - getById("miniPerformer").innerHTML = ''; - } - miniTranslate(getById("miniPerformer")); - - getById("miniPerformer").className = ""; - - var tabindex = 26; - if (session.rooms && session.rooms.length > 0) { - var container = getById("rooms"); - container.innerHTML += 'Arm Transfer: '; - session.rooms.forEach(function (r) { - // if(session.roomid == r) return; //don't include self - container.innerHTML += '"; - tabindex++; - }); - } - } else { - getById("miniPerformer").style.display = "none"; - getById("controlButtons").classList.add("hidden"); - // getById("legal").classList.add("hidden"); - } - - if (session.chatbutton === true) { - getById("chatbutton").classList.remove("hidden"); - getById("controlButtons").classList.remove("hidden"); - } else if (session.chatbutton === false) { - getById("chatbutton").classList.add("hidden"); - } - - if (session.effect === false) { - session.effect = null; // so the director can see the effects - } - - getById("avatarDiv3").classList.remove("hidden"); // lets the director see the avatar option - - clearInterval(session.updateLocalStatsInterval); - session.updateLocalStatsInterval = setInterval(function () { - updateLocalStats(); - }, session.statsInterval); - - var directorWebsiteShare = getStorage("directorWebsiteShare"); // {"website":session.iframeSrc, "roomid":session.roomid} - - if (typeof directorWebsiteShare === "object" && directorWebsiteShare !== null && "website" in directorWebsiteShare) { - if (directorWebsiteShare.website == false) { - clearDirectorSettings(); - } else if (directorWebsiteShare.roomid && directorWebsiteShare.roomid == session.roomid) { - session.iframeSrc = directorWebsiteShare.website; - session.defaultIframeSrc = directorWebsiteShare.website; - - getById("websitesharebutton").classList.add("hidden"); - getById("websitesharebutton2").classList.remove("hidden"); - } - } - - session.group.forEach(group => { - // changeGroupDirectorAPI(group, state=null, update=true) - changeGroupDirectorAPI(group, true, false); // update the UI only - }); - - session.groupView.forEach(group => { - // changeGroupDirectorAPI(group, state=null, update=true) - changeGroupViewDirectorAPI(group, true); // update the UI only - }); - - if (session.showDirector) { - getById("highlightDirectorSpan").style.display = "none"; - getById("highlightDirectorSpan").remove(); - } else { - getById("highlightDirector").dataset.sid = session.streamID; - } - - setTimeout(() => { - loadDirectorSettings(); - if (broadcastFlag) { - saveDirectorSettings(); - } - }, 100); - - joinRoom(session.roomid); - - pokeIframeAPI("create-room", session.roomid); - - try { - if (!gotDevices2AlreadyRan && (iOS || iPad)) { - await enumerateDevices().then(gotDevices2); // this is needed for iOS; was previous set to timeout at 100ms, but would be useful everywhere I think. (Breaks director's auto start, so just iOS for now) - } - } catch (e) { - errorlog(e); - } - - if (session.autostart) { - setTimeout(function () { - press2talk(true); - }, 400); - } else { - session.seeding = true; - session.seedStream(); - } -} // createRoomCallback - -function handleRoomSelect(room) { - var elems = document.querySelectorAll(".btnArmTransferRoom"); - [].forEach.call(elems, function (el) { - el.classList.remove("selected"); - }); - if (previousRoom == room) { - previousRoom = ""; - armedTransfer = false; - stillNeedRoom = true; - } else { - previousRoom = room; - stillNeedRoom = false; - armedTransfer = true; - getById("roomselect_" + room).classList.add("selected"); - } -} - -function getDirectorSettings(scene = false) { - var settings = {}; - - var eles = document.querySelectorAll('[data-action-type="solo-video"]'); - settings.soloVideo = false; - var soloVideoMode = null; - for (var i = 0; i < eles.length; i++) { - if (eles[i].value == 1) { - warnlog(eles[i]); - if (eles[i].dataset.sid) { - if (eles[i].classList && eles[i].classList.contains("altpress")) { - soloVideoMode = "alt"; - } - settings.soloVideo = eles[i].dataset.sid; // who is solo, if someone is solo - } - } - } - if (soloVideoMode) { - settings.soloVideoMode = soloVideoMode; - } else { - delete settings.soloVideoMode; - } - if (scene) { - var eles = document.querySelectorAll('[data-action-type="addToScene"][data-scene="' + scene + '"'); - settings.scene = {}; - for (var i = 0; i < eles.length; i++) { - if (eles[i].value == 1) { - if (eles[i].dataset.sid) { - var msg = {}; - msg.scene = scene; - msg.action = "display"; - msg.value = eles[i].value; - msg.target = eles[i].dataset.sid; - - settings.scene[eles[i].dataset.sid] = msg; - } - } - } - } - - settings.showDirector = session.showDirector; - - settings.mute = {}; - var eles = document.querySelectorAll('[data-action-type="mute-scene"]'); - for (var i = 0; i < eles.length; i++) { - if (eles[i].value == 1) { - // if muted - if (eles[i].dataset.sid) { - var msg = {}; - msg.action = "mute"; - msg.scene = true; - msg.value = 1; - msg.target = eles[i].dataset.sid; - settings.mute[eles[i].dataset.sid] = msg; - } - } - } - return settings; -} - -function normalizeLayoutStateValue(state) { - if (typeof state === "undefined") { - return undefined; - } - if (state === null) { - return false; - } - if (state === true) { - return false; - } - if (state === false) { - return false; - } - if (typeof state === "number") { - return state ? state : false; - } - if (typeof state === "string") { - const normalized = state.trim().toLowerCase(); - if (!normalized) { - return false; - } - if (normalized === "false" || normalized === "off" || normalized === "auto" || normalized === "0") { - return false; - } - if (normalized === "true") { - return false; - } - } - return state; -} - -function isAutoLayoutState(state) { - const normalized = normalizeLayoutStateValue(state); - return normalized === false || typeof normalized === "undefined" || normalized === null; -} - -function requestInfocus(ele, evt = null, value = null) { - try { - var sid = ele.dataset.sid; - } catch (e) { - warnlog("no stream ID found; requestinfocus"); - var sid = false; - if (ele.id === "highlightDirector") { - if (session.streamID) { - sid = session.streamID; - } - } - } - - if (value !== null) { - if (value) { - ele.value == 0; // we will toggle it in a second anyways. - } else { - ele.value == 1; - } - } - - var special = false; - if (evt) { - special = evt.ctrlKey || evt.metaKey || false; - if (special) { - special = true; - } - } - - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - ele.classList.remove("altpress"); - var actionMsg = {}; - actionMsg.infocus = false; - //session.sendMessage(actionMsg); - } else { - var actionMsg = {}; - if (special) { - actionMsg.infocus2 = sid; - } else { - actionMsg.infocus = sid; - } - //session.sendMessage(actionMsg); - - var eles = document.querySelectorAll('[data-action-type="solo-video"]'); - for (var i = 0; i < eles.length; i++) { - log(eles); - eles[i].classList.remove("pressed"); - eles[i].ariaPressed = "false"; - eles[i].classList.remove("altpress"); - eles[i].value = 0; - } - ele.value = 1; - - if (special) { - ele.classList.add("altpress"); - } else { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } - if (ele.id !== "highlightDirector") { - getById("highlightDirector").checked = false; - } - } - - for (var uuid in session.pcs) { - var layoutState = session.pcs[uuid].layoutState; - if (!session.pcs[uuid].solo && isAutoLayoutState(layoutState)) { - // only issue highlight commands to non-solo links when the scene is auto mixing - session.sendMessage(actionMsg, uuid); - } - } - - syncDirectorState(ele); - - if (ele.value == 1) { - return true; - } else { - return false; - } -} - -var fixScrollReset = null; -var fixScrollResetValue = null; - -function requestAudioSettings(ele) { - var UUID = ele.dataset.UUID; - - try { - clearTimeout(fixScrollReset); - fixScrollResetValue = getById("directorlayout").scrollTop; - fixScrollReset = setTimeout( - function (scrollpos) { - fixScrollReset = null; - getById("directorlayout").scrollTop = scrollpos; - }, - 1000, - fixScrollResetValue - ); - - query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").value = 0; - query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").classList.remove("pressed"); - query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").ariaPressed = "false"; - query("#container_" + UUID + " .advancedVideoSettings").classList.add("hidden"); - query("#container_" + UUID + " .advancedVideoSettings").innerHTML = ""; - } catch (e) { } - - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - query("#container_" + UUID + " .advancedAudioSettings").classList.add("hidden"); - query("#container_" + UUID + " .advancedAudioSettings").innerHTML = ""; - return false; - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - query("#container_" + UUID + " .advancedAudioSettings").innerHTML = ""; - var actionMsg = {}; - actionMsg.getAudioSettings = true; - session.sendRequest(actionMsg, UUID); - return true; - } -} - -function requestVideoSettings(ele) { - var UUID = ele.dataset.UUID; - - try { - clearTimeout(fixScrollReset); - fixScrollResetValue = getById("directorlayout").scrollTop; - fixScrollReset = setTimeout( - function (scrollpos) { - fixScrollReset = null; - getById("directorlayout").scrollTop = scrollpos; - }, - 1000, - fixScrollResetValue - ); - - query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").value = 0; - query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").classList.remove("pressed"); - query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").ariaPressed = "false"; - query("#container_" + UUID + " .advancedAudioSettings").classList.add("hidden"); - query("#container_" + UUID + " .advancedAudioSettings").innerHTML = ""; - } catch (e) { } - - if (ele.value == 1) { - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - query("#container_" + UUID + " .advancedVideoSettings").classList.add("hidden"); - query("#container_" + UUID + " .advancedVideoSettings").innerHTML = ""; - return false; - } else { - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - query("#container_" + UUID + " .advancedVideoSettings").innerHTML = ""; - var actionMsg = {}; - actionMsg.getVideoSettings = true; - session.sendRequest(actionMsg, UUID); - return true; - } -} - -function combinedLayoutSimple(layout) { - var combined = {}; - Object.keys(layout).forEach(i => { - if (!layout[i]) { - return; - } - - if (i === "") { - layout[i].forEach(j => { - if (!j) { - return; - } - var streamID = null; - if ("slot" in j) { - try { - streamID = session.currentSlots[parseInt(j.slot) + 1]; - } catch (e) { - errorlog(e); - streamID = null; - } - } - if (!streamID) { - if (!combined[""]) { - combined[""] = []; - } - combined[""].push(j); - } else { - combined[streamID] = j; - } - }); - } else { - var streamID = null; - if ("slot" in layout[i]) { - try { - streamID = session.currentSlots[parseInt(layout[i].slot) + 1]; - } catch (e) { - errorlog(e); - streamID = null; - } - } - if (!streamID) { - if (!combined[""]) { - combined[""] = []; - } - combined[""].push(layout[i]); - } else { - combined[streamID] = layout[i]; - } - } - }); - return combined; -} - -async function createDirectorOnlyBox() { - var soloLink = soloLinkGenerator(session.streamID); - - if (document.getElementById("deleteme")) { - getById("deleteme").parentNode.removeChild(getById("deleteme")); - } - var controls = getById("controls_directors_blank").cloneNode(true); - controls.classList.remove("hidden"); - controls.id = "controls_director"; - - var container = document.createElement("div"); - container.className = "vidcon directorMargins"; - container.id = "container_director"; // needed to delete on user disconnect - container.setAttribute("aria-label", miscTranslations["your-camera"]); - container.setAttribute("role", "region"); - - var buttons = ""; - if (session.slotmode && session.showDirector) { - var biggestSlot = 0; - var slotDefault = null; - - // Check past slots first - if (session.streamID in session.pastSlots) { - slotDefault = session.pastSlots[session.streamID]; - } - - // Get all current slots from RPC state - var allSlots = []; - if (session.slotmode == 1) { - Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => { - if (currentSlot) { - if (parseInt(currentSlot) > biggestSlot) { - biggestSlot = parseInt(currentSlot); - } - if (slotDefault === parseInt(currentSlot)) { - slotDefault = null; - } - allSlots.push(parseInt(currentSlot)); - } - }); - biggestSlot += 1; - } else if (slotDefault !== null && session.slotmode == 2) { - // Check if slot is already in use - include director's stream - const slotInUse = Object.keys(session.rpcs).some(UUID => - getSlotState(UUID) === slotDefault - ) || getSlotState(session.streamID) === slotDefault || getSlotState(session.streamID + ":s") === slotDefault; - - if (slotInUse) { - slotDefault = null; // This slot is already in use - } - } - - // Determine final slot value - if (slotDefault !== null) { - biggestSlot = slotDefault; - } else if (session.slotmode == 1) { - var bestfree = 0; - for (var i = 1; i <= biggestSlot; i++) { - if (allSlots.includes(i)) { - continue; - } else { - bestfree = i; - break; - } - } - biggestSlot = bestfree; - } - - // Set slot name - var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; - var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : ""; - - // Build HTML with same structure - buttons += - `
    - -
    `; - - // Sync the initial state - syncSlotState(session.streamID, biggestSlot, false); // false since UI is being created here - } - buttons += - "
    \ -
    ID: " + - session.streamID + - "\ - \ - \ - " + - getTranslation("add-a-label") + - "\ -
    \ -
    "; - - container.innerHTML = buttons; - - var oldGroups = []; - document.querySelectorAll("#groups [data-action-type='toggle-group'][data-group]:not(.green)").forEach(ee => { - oldGroups.push(ee.dataset.group); - }); - getById("groups").remove(); - - if (session.hidesololinks == false) { - // won't be updating the solo link to a view-only one ever, since director is always expected to be in a room - controls.innerHTML += - "
    \ - " + - sanitizeChat(soloLink) + - "\ - \ -
    \ -
    "; - if (session.directorUUID) { - controls.innerHTML += "

    This is you, a co-director.
    You are also a performer.

    "; - } else { - controls.innerHTML += "

    This is you, the director.
    You are also a performer.

    "; - } - } - - controls.querySelectorAll("[data-action-type]").forEach(ele => { - // give action buttons some self-reference - ele.dataset.sid = session.streamID; - }); - - container.appendChild(controls); - - getById("guestFeeds").appendChild(container); - - Object.keys(session.sceneList).forEach((scene, index) => { - if (document.getElementById("container_director")) { - if (!getById("container_director").querySelectorAll('[data-scene="' + scene + '"]').length) { - var newScene = document.createElement("div"); - newScene.innerHTML = '"; - newScene.classList.add("customScene"); - //getById("container_director").appendChild(newScene); - - var added = false; - getById("container_director") - .querySelectorAll(".customScene>[data-scene]") - .forEach(ele => { - if (!added && ele.dataset.scene > scene + "") { - ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); - added = true; - } - }); - if (!added) { - getById("container_director").appendChild(newScene); - } - } - } - }); - - getById("groups").showDirector = true; - - session.group.forEach(group => { - // changeGroupDirectorAPI(group, state=null, update=true) - changeGroupDirectorAPI(group, true, false); // update the UI only / - }); - - oldGroups.forEach(group => { - // changeGroupDirectorAPI(group, state=null, update=true) - changeGroupDirectorAPI(group, false, false); // update the UI only / - }); - - var labelID = document.getElementById("label_director"); - - labelID.onclick = async function (ee) { - var oldlabel = ee.target.innerText; - if (session.label === false) { - oldlabel = ""; - } - window.focus(); - var newlabel = await promptAlt(getTranslation("enter-new-display-name"), false, false, oldlabel); - if (newlabel !== null) { - newlabel = newlabel.trim(); - if (newlabel === "") { - newlabel = false; - //ee.target.innerHTML = getTranslation("add-a-label"); - miniTranslate(ee.target, "add-a-label"); - ee.target.classList.add("addALabel"); - } else { - ee.target.innerText = newlabel; - ee.target.classList.remove("addALabel"); - } - session.label = newlabel; - var data = {}; - data.changeLabel = true; - data.value = session.label; - session.sendMessage(data); - stashRoomSession(); - } - }; - labelID.style.float = "left"; - labelID.style.top = "2px"; - labelID.style.marginLeft = "5px"; - labelID.style.position = "relative"; - labelID.style.cursor = "pointer"; - if (session.label) { - labelID.innerText = session.label; - } - pokeIframeAPI("control-box", true, true); - -} - -async function createDirectorScreenshareOnlyBox() { - // sstype=3 - var screenStreamID = session.streamID + ":s"; - var soloLink = soloLinkGenerator(screenStreamID); - - if (document.getElementById("deleteme")) { - getById("deleteme").parentNode.removeChild(getById("deleteme")); - } - var controls = getById("controls_directors_blank").cloneNode(true); - controls.classList.remove("hidden"); - controls.id = "controls_screen_director"; - - var container = document.createElement("div"); - container.className = "vidcon directorMargins"; - container.id = "container_screen_director"; // needed to delete on user disconnect - container.setAttribute("aria-label", miscTranslations["your-screenshare"]); - container.setAttribute("role", "region"); - - var buttons = ""; - if (session.slotmode) { - var biggestSlot = 0; - var slotDefault = null; - - if (screenStreamID in session.pastSlots) { - slotDefault = session.pastSlots[screenStreamID]; - } - - var allSlots = []; - if (session.slotmode == 1) { - Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => { - if (currentSlot) { - if (parseInt(currentSlot) > biggestSlot) { - biggestSlot = parseInt(currentSlot); - } - if (slotDefault === parseInt(currentSlot)) { - slotDefault = null; - } - allSlots.push(parseInt(currentSlot)); - } - }); - biggestSlot += 1; - } - - // Determine final slot value - if (slotDefault !== null) { - biggestSlot = slotDefault; - } else if (session.slotmode == 1) { - var bestfree = 0; - for (var i = 1; i <= biggestSlot; i++) { - if (allSlots.includes(i)) { - continue; - } else { - bestfree = i; - break; - } - } - biggestSlot = bestfree; - } - - var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; - var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : ""; - - buttons += - `
    - -
    `; - - // Sync the initial state - syncSlotState(screenStreamID, biggestSlot, false); // false since UI is being created here - } - buttons += - "
    \ -
    ID: " + - screenStreamID + - "\ - \ - \ - " + - getTranslation("add-a-label") + - "\ -
    \ -
    "; - - container.innerHTML = buttons; - - var oldGroups = []; - document.querySelectorAll("#groups [data-action-type='toggle-group'][data-group]:not(.green)").forEach(ee => { - oldGroups.push(ee.dataset.group); - }); - getById("groups").remove(); - - if (session.hidesololinks == false) { - // won't be updating the solo link to a view-only one ever, since director is always expected to be in a room - controls.innerHTML += - "
    \ - " + - sanitizeChat(soloLink) + - "\ - \ -
    \ -
    "; - if (session.directorUUID) { - controls.innerHTML += "

    This is you, a co-director.
    You are also a performer.

    "; - } else if (session.showDirector === false) { - try { - controls.querySelectorAll('[data-action-type="addToScene"]').forEach(ele => { - ele.classList.add("hidden"); - }); - } catch (e) { - errorlog(e); - } - controls.innerHTML += "

    This is your screen share
    It's *not* a performer.

    "; - } else { - controls.innerHTML += "

    This your screen share.
    It's also a performer.

    "; - } - } - - controls.querySelectorAll("[data-action-type]").forEach(ele => { - // give action buttons some self-reference - ele.dataset.sid = screenStreamID; - }); - - container.appendChild(controls); - - getById("guestFeeds").appendChild(container); - - Object.keys(session.sceneList).forEach((scene, index) => { - if (document.getElementById("container_screen_director")) { - if (!getById("container_screen_director").querySelectorAll('[data-scene="' + scene + '"]').length) { - var newScene = document.createElement("div"); - newScene.innerHTML = '"; - newScene.classList.add("customScene"); - //getById("container_screen_director").appendChild(newScene); - - var added = false; - getById("container_screen_director") - .querySelectorAll(".customScene>[data-scene]") - .forEach(ele => { - if (!added && ele.dataset.scene > scene + "") { - ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); - added = true; - } - }); - if (!added) { - getById("container_screen_director").appendChild(newScene); - } - } - } - }); - - getById("groups").showDirector = true; - - session.group.forEach(group => { - // changeGroupDirectorAPI(group, state=null, update=true) - changeGroupDirectorAPI(group, true, false); // update the UI only / - }); - - oldGroups.forEach(group => { - // changeGroupDirectorAPI(group, state=null, update=true) - changeGroupDirectorAPI(group, false, false); // update the UI only / - }); - - document.querySelectorAll("#container_screen_director #label_director").forEach(elex => { - elex.remove(); - }); - - pokeIframeAPI("control-box", true, true); - -} - -function shiftPC(ele, shift, director = false) { - if (director) { - var target = document.getElementById("container_director"); - } else { - var target = document.getElementById("container_" + ele.dataset.UUID); - } - - if (!target) { - return; - } - - target.shifted = true; - - var target2 = false; - - if (shift == 1) { - if (target.nextSibling) { - target2 = target.nextSibling; - target.parentNode.insertBefore(target.nextSibling, target); - } - } else { - if (target.previousSibling) { - target2 = target.previousSibling; - target.parentNode.insertBefore(target, target.previousSibling); - } - } - updateLockedElements(); - - if (session.api) { - var slots = {}; - var elements = getById("guestFeeds").children; - for (var i = 0; i < elements.length; i++) { - if (elements[i] === target) { - var tmp = target.querySelector("[data-sid]"); - if (tmp) { - var lock = target.querySelector("[data-locked]"); - if (lock) { - lock = parseInt(lock.dataset.locked); - } - tmp = tmp.dataset.sid; - slots[tmp] = lock || i + 1; - } - } else if (elements[i] === target2) { - var tmp2 = target2.querySelector("[data-sid]"); - if (tmp2) { - var lock = target2.querySelector("[data-locked]"); - if (lock) { - lock = parseInt(lock.dataset.locked); - } - tmp2 = tmp2.dataset.sid; - slots[tmp2] = lock || i + 1; - } - } - } - pokeAPI("positionChange", slots); - } -} - -function updateLockedElements() { - var eles = getById("guestFeeds").children; - for (var i = 0; i < eles.length; i++) { - try { - var UUID = eles[i].UUID; - var lock = document.getElementById("position_" + UUID).dataset.locked; - if (parseInt(lock)) { - lockPosition(document.getElementById("position_" + UUID), true); - } - } catch (e) { } - } -} - -function lockPosition(ele, apply = false) { - var UUID = ele.dataset.UUID; - if (apply) { - if (ele.dataset.locked && parseInt(ele.dataset.locked)) { - if (getById("guestFeeds")) { - var currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1; - ele.innerHTML = "#" + ele.dataset.locked + ""; - ele.parentNode.classList.add("locked"); - - while (currentPosition > parseInt(ele.dataset.locked)) { - var node = document.getElementById("container_" + UUID); - (parent = node.parentNode), (prev = node.previousSibling), (oldChild = parent.removeChild(node)); - parent.insertBefore(oldChild, prev); - currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1; - } - - while (currentPosition < parseInt(ele.dataset.locked) && getById("guestFeeds").children.length > currentPosition) { - var node = document.getElementById("container_" + UUID); - (parent = node.parentNode), (next = node.nextSibling), (oldChild = parent.removeChild(node)); - parent.insertBefore(node, next.nextSibling); - currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1; - } - } - } else { - ele.dataset.locked = 0; - ele.innerHTML = ""; - ele.parentNode.classList.remove("locked"); - } - } else { - if (ele.dataset.locked && parseInt(ele.dataset.locked)) { - ele.dataset.locked = 0; - ele.innerHTML = ""; - ele.parentNode.classList.remove("locked"); - } else { - if (getById("guestFeeds")) { - ele.dataset.locked = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1; - ele.innerHTML = "#" + ele.dataset.locked + ""; - ele.parentNode.classList.add("locked"); - } - } - } -} - -function allowDropSlot(event) { - event.preventDefault(); -} - -function dragSlot(event) { - log("drag"); - - var ele = event.target; - if (!ele.dataset.sid && ele.parentNode.dataset.sid) { - ele = ele.parentNode; - } - - event.dataTransfer.setDragImage(getById("dragImage"), 24, 24); - event.dataTransfer.setData("text", ele.dataset.sid); - - var eles = document.querySelectorAll(".slotsbar"); - for (var i = 0; i < eles.length; i++) { - if (eles[i].dataset.sid == ele.dataset.sid) { - continue; - } - eles[i].style.boxShadow = "0px 0px 8px 2px #FFF"; - } -} - -function dragendSlot(event) { - var eles = document.querySelectorAll(".slotsbar"); - for (var i = 0; i < eles.length; i++) { - eles[i].style.boxShadow = "unset"; - } - return true; -} - -function dropSlot(event) { - log("drop"); - event.preventDefault(); - event.stopPropagation(); - - // Get the dragged streamID - var SID = event.dataTransfer.getData("text"); - if (!SID) return; - - var origThing = document.querySelector("[data-sid='" + SID + "'][data-slot]"); - if (!origThing) return; - - // Get target streamID - var targetSID = event.target.dataset.sid || event.target.parentNode.dataset.sid; - if (!targetSID) return; - - var targetThing = document.querySelector("[data-sid='" + targetSID + "'][data-slot]"); - if (!targetThing) return; - - // Get original slots - const origSlot = parseInt(origThing.dataset.slot); - const targetSlot = parseInt(targetThing.dataset.slot); - - // Key fix: We need to swap the DOM elements *and* swap the session.currentSlots entries - // Save the original values - const tempStreamID = session.currentSlots[targetSlot]; // Save the target slot's original value - - // Update session.currentSlots (this is the crucial part) - session.currentSlots[targetSlot] = SID; - session.currentSlots[origSlot] = tempStreamID; - - // Update the data-sid attributes for the visual swap - targetThing.dataset.sid = SID; - origThing.dataset.sid = targetSID; - - // Update the UI text as well - const targetButton = targetThing.querySelector('button'); - const origButton = origThing.querySelector('button'); - - if (targetButton) { - targetButton.innerText = targetSlot ? `slot: ${targetSlot}` : 'unset'; - } - - if (origButton) { - origButton.innerText = origSlot ? `slot: ${origSlot}` : 'unset'; - } - - // Update past slots for future reference - session.pastSlots[SID] = targetSlot; // we don't need to run syncSlotState(), as this handles it - session.pastSlots[targetSID] = origSlot; - - // Tell any iframes about the swap - pokeIframeAPI("slot-updated", targetSlot, null, SID); - pokeIframeAPI("slot-updated", origSlot, null, targetSID); - - // Notify all peers of the update - broadcastSlotUpdate(); - - return false; -} - -function dragenterSlot(event) { - event.preventDefault(); - if (event.target.classList.contains("slotsbar")) { - event.target.style.border = "3px dotted black"; - } -} - -function dragleaveSlot(event) { - event.preventDefault(); - if (event.target.classList.contains("slotsbar")) { - event.target.style.border = ""; - } -} - -async function changeSlot(event, ele) { - var picker = document.getElementById("slotPicker"); - - if (picker) { - clearTimeout(modalTimeout); - - if (document.getElementById("modalBackdrop")) { - getById("alertModal").innerHTML = ""; // Delete modal - getById("alertModal").remove(); - getById("modalBackdrop").innerHTML = ""; // Delete modal - getById("modalBackdrop").remove(); - } - - zindex = 31 + document.querySelectorAll(".alertModal").length; - message = picker.innerHTML; - - modalTemplate = `
    -
    - × - ${message} -
    -
    -
    `; - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - - document.getElementById("modalBackdrop").addEventListener("click", closeModal); - - document - .getElementById("alertModalMessage") - .querySelectorAll("div[data-slot]") - .forEach(choice => { - choice.onclick = function () { - setSlot(ele, parseInt(this.dataset.slot)); - closeModal(); - }; - }); - - if (event) { - positionAlertModalNearEvent(document.getElementById("alertModal"), event); - } - - getById("alertModal").addEventListener("click", function (e) { - e.stopPropagation(); - return false; - }); - } else { - var slot = await promptAlt("Which slot to change to?"); - setSlot(ele, slot); - } -} - -function setSlot(ele, slot) { - log("setSlot()"); - getById("slotPicker").classList.add("hidden"); - if (slot !== null) { - try { - slot = parseInt(slot) || 0; - - // Find container with stream ID - const container = ele.closest('[data-sid]'); - const streamID = container ? container.dataset.sid : null; - - if (!streamID) { - return false; - } - - // Critical part: Check if the slot is already occupied and swap instead of replace - const existingStreamID = session.currentSlots[slot]; - if (existingStreamID && existingStreamID !== streamID) { - // Find the current slot of the stream we're moving - let currentSlot = null; - Object.entries(session.currentSlots).forEach(([key, value]) => { - if (value === streamID) { - currentSlot = parseInt(key); - } - }); - - // If the stream we're moving is already in a slot, update that slot's value - if (currentSlot !== null) { - // Perform the swap - session.currentSlots[slot] = streamID; - session.currentSlots[currentSlot] = existingStreamID; - - // Update the other element's UI - const otherSlotBar = document.querySelector(`[data-sid="${existingStreamID}"][data-slot]`); - if (otherSlotBar) { - otherSlotBar.dataset.slot = currentSlot; - applySlotColor(otherSlotBar, currentSlot); - const otherButton = otherSlotBar.querySelector('button'); - if (otherButton) { - otherButton.innerText = currentSlot ? `slot: ${currentSlot}` : 'unset'; - } - } - - // Update UI for the element we're setting - container.dataset.slot = slot; - applySlotColor(container, slot); - ele.innerText = slot ? `slot: ${slot}` : 'unset'; - - // Update pastSlots - session.pastSlots[streamID] = slot; - session.pastSlots[existingStreamID] = currentSlot; - - // Update iframes - pokeIframeAPI("slot-updated", slot, null, streamID); - pokeIframeAPI("slot-updated", currentSlot, null, existingStreamID); - } else { - // We're moving a stream that wasn't in a slot before - // First clear any current assignment for this stream - Object.entries(session.currentSlots).forEach(([key, value]) => { - if (value === streamID) { - delete session.currentSlots[key]; - } - }); - - // Then assign it to the new slot - session.currentSlots[slot] = streamID; - - // Update UI - container.dataset.slot = slot; - applySlotColor(container, slot); - ele.innerText = slot ? `slot: ${slot}` : 'unset'; - - // Update pastSlots - session.pastSlots[streamID] = slot; - - // Update iframe - pokeIframeAPI("slot-updated", slot, null, streamID); - } - } else { - // No conflict, just set the slot normally - // Clear any existing slot for this stream - Object.entries(session.currentSlots).forEach(([key, value]) => { - if (value === streamID) { - delete session.currentSlots[key]; - } - }); - - // Set the new slot - if (slot) { - session.currentSlots[slot] = streamID; - } - - // Update UI - container.dataset.slot = slot; - applySlotColor(container, slot); - ele.innerText = slot ? `slot: ${slot}` : 'unset'; - - // Update pastSlots - session.pastSlots[streamID] = slot; - - // Update iframe - pokeIframeAPI("slot-updated", slot, null, streamID); - } - - // Always update all peers - broadcastSlotUpdate(); - - } catch (e) { - errorlog(e); - return false; - } - return true; - } - return false; -} - -function swapNodes(n1, n2) { - log("swapping nodes"); - var p1 = n1.parentNode; - var p2 = n2.parentNode; - var i1, i2; - - if (!p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1)) return; - - for (var i = 0; i < p1.children.length; i++) { - if (p1.children[i].isEqualNode(n1)) { - i1 = i; - } - } - for (var i = 0; i < p2.children.length; i++) { - if (p2.children[i].isEqualNode(n2)) { - i2 = i; - } - } - - if (p1.isEqualNode(p2) && i1 < i2) { - i2++; - } - p1.insertBefore(n2, p1.children[i1]); - p2.insertBefore(n1, p2.children[i2]); -} - -function getCurrentSlot(streamID) { - const slotEntry = Object.entries(session.currentSlots).find(([_, sid]) => sid === streamID); - return slotEntry ? slotEntry[0] : false; -} - -function getSlotState(UUID) { - if (!UUID || !(UUID in session.rpcs)) return false; - return getCurrentSlot(session.rpcs[UUID].streamID); -} - -function combinedLayout(layout) { - if (!Array.isArray(layout)) return layout || {}; - - var combined = {}; - for (var i = 0; i < layout.length; i++) { - if (!layout[i] || !("slot" in layout[i])) { - if (!combined[""]) combined[""] = []; - combined[""].push(layout[i]); - continue; - } - - const slotNumber = parseInt(layout[i].slot || 0) + 1; - const streamID = session.currentSlots[slotNumber]; - - if (!streamID) { - if (!combined[""]) combined[""] = []; - combined[""].push(layout[i]); - continue; - } - - combined[streamID] = layout[i]; - } - return combined; -} - -function syncSlotState(streamID, slotValue = false, updateUI = true) { - // Clear any existing slots for this stream - Object.entries(session.currentSlots).forEach(([slot, sid]) => { - if (sid === streamID) delete session.currentSlots[slot]; - }); - - // Set new slot if one provided - if (slotValue) { - session.currentSlots[slotValue] = streamID; - } - - // Update UI if requested - if (updateUI) { - const slotsBar = document.querySelector(`[data-sid="${streamID}"][data-slot]`); - if (slotsBar) { - slotsBar.dataset.slot = slotValue; - applySlotColor(slotsBar, slotValue); - const slotButton = slotsBar.querySelector('button'); - if (slotButton) { - slotButton.innerText = slotValue ? `slot: ${slotValue}` : 'unset'; - } - } - } - - pokeIframeAPI("slot-updated", slotValue, null, streamID); // need to support self-director - session.pastSlots[streamID] = slotValue || 0; - - clearTimeout(session.slotBroadcastThrottle); - session.slotBroadcastThrottle = setTimeout(function () { broadcastSlotUpdate(); }, 10); - return true; -} -function broadcastSlotUpdate(UUID = false) { - try { - if (!session.slotmode || !session.director) { - return; - } - if (!UUID) { - if (session.slotBroadcastThrottle) { - clearTimeout(session.slotBroadcastThrottle); - session.slotBroadcastThrottle = null; - } - session.sendMessage({ slotsUpdate: session.currentSlots }); - } else { - session.sendMessage({ slotsUpdate: session.currentSlots }, UUID); - } - } catch (e) { - errorlog(e); - } -} -function updateSlotUI() { - // Update all slot UI elements based on the current state in session.currentSlots - Object.entries(session.currentSlots).forEach(([slot, streamID]) => { - const slotBar = document.querySelector(`[data-sid="${streamID}"][data-slot]`); - if (slotBar) { - slotBar.dataset.slot = slot; - applySlotColor(slotBar, slot); - const button = slotBar.querySelector('button'); - if (button) { - button.innerText = slot ? `slot: ${slot}` : 'unset'; - } - } - }); -} - -function createControlBox(UUID, soloLink, streamID, slot_init = false) { - if (document.getElementById("deleteme")) { - getById("deleteme").parentNode.removeChild(getById("deleteme")); - } - - // Remove any existing container with this UUID to prevent stacking - var existingContainer = document.getElementById("container_" + UUID); - if (existingContainer) { - existingContainer.parentNode.removeChild(existingContainer); - } - - var controls = getById("controls_blank").cloneNode(true); - controls.classList.remove("hidden"); - controls.id = "controls_" + UUID; - - var container = document.createElement("div"); - container.className = "vidcon directorMargins"; - container.id = "container_" + UUID; // needed to delete on user disconnect - container.UUID = UUID; - container.dataset.UUID = UUID; - container.dataset.sid = streamID; - - if (session.orderby) { - try { - var added = false; - for (var i = 0; i < getById("guestFeeds").children.length; i++) { - if (getById("guestFeeds").children[i].UUID && !getById("guestFeeds").children[i].shifted) { - if (getById("guestFeeds").children[i].UUID in session.rpcs) { - if (session.rpcs[getById("guestFeeds").children[i].UUID].streamID.toLowerCase() > streamID.toLowerCase()) { - getById("guestFeeds").insertBefore(container, getById("guestFeeds").children[i]); - added = true; - break; - } - } - } - } - if (!added) { - getById("guestFeeds").appendChild(container); - } - } catch (e) { - getById("guestFeeds").appendChild(container); - } - } else { - getById("guestFeeds").appendChild(container); - } - - //controls.innerHTML += ""; - if (session.rpcs[UUID].pseudoguest) { - controls.querySelectorAll("[data-action-type]").forEach(ele => { - ele.dataset.UUID = UUID; - ele.dataset.sid = streamID; - }); - return; - } - //controls.innerHTML += ""; - - if (!session.rpcs[UUID].voiceMeter) { - if (session.meterStyle == 1) { - // director specific style - session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate2").cloneNode(true); - } else { - session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate").cloneNode(true); - session.rpcs[UUID].voiceMeter.style.opacity = 0; - if (session.meterStyle == 2) { - session.rpcs[UUID].voiceMeter.classList.add("video-meter-2"); - session.rpcs[UUID].voiceMeter.classList.remove("video-meter"); - } else { - session.rpcs[UUID].voiceMeter.classList.add("video-meter-director"); - } - } - session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID; - session.rpcs[UUID].voiceMeter.dataset.level = 0; - session.rpcs[UUID].voiceMeter.classList.remove("hidden"); - } - - session.rpcs[UUID].remoteMuteElement = getById("muteStateTemplate").cloneNode(true); - session.rpcs[UUID].remoteMuteElement.id = ""; - session.rpcs[UUID].remoteMuteElement.style.top = "5px"; - session.rpcs[UUID].remoteMuteElement.style.right = "7px"; - - session.rpcs[UUID].remoteVideoMuteElement = getById("videoMuteStateTemplate").cloneNode(true); - session.rpcs[UUID].remoteVideoMuteElement.id = ""; - session.rpcs[UUID].remoteVideoMuteElement.style.top = "5px"; - session.rpcs[UUID].remoteVideoMuteElement.style.right = "28px"; - - session.rpcs[UUID].remoteRaisedHandElement = getById("raisedHandTemplate").cloneNode(true); - session.rpcs[UUID].remoteRaisedHandElement.id = ""; - session.rpcs[UUID].remoteRaisedHandElement.style.top = "5px"; - session.rpcs[UUID].remoteRaisedHandElement.style.right = "49px"; - - var handsID = "hands_" + UUID; - - // controls.innerHTML += "
    Links
    "; //Seems to create an empty div. - - if (session.hidesololinks == false) { - controls.innerHTML += - "
    \ - " + - sanitizeChat(soloLink) + - "\ - \ -
    "; - } - - controls.innerHTML += - '\ - '; - - controls.innerHTML += - '\ - '; - - controls.querySelectorAll("[data-action-type]").forEach(ele => { - // give action buttons some self-reference - ele.dataset.UUID = UUID; - ele.dataset.sid = streamID; - }); - - - var buttons = ""; - if (session.slotmode && slot_init !== 0) { // slot_init === 0 means guest explicitly opted out of slots - var biggestSlot = 0; - var slotDefault = null; - - // Handle initial slot value from initialization or past slots - if (slot_init && session.slotmode == 1) { - slotDefault = slot_init || null; - } - if (streamID in session.pastSlots) { - slotDefault = session.pastSlots[streamID]; - } - - // Get all current slots from RPC state - var allSlots = []; - if (session.slotmode == 1) { - Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => { - if (currentSlot) { - if (parseInt(currentSlot) > biggestSlot) { - biggestSlot = parseInt(currentSlot); - } - if (slotDefault === parseInt(currentSlot)) { - slotDefault = null; - } - allSlots.push(parseInt(currentSlot)); - } - }); - biggestSlot += 1; - } else if (slotDefault !== null && session.slotmode == 2) { - // Check if slot is already in use by any remote participant - const remoteSlotInUse = Object.keys(session.rpcs).some(UUID => - getSlotState(UUID) === slotDefault - ); - - // Check if slot is used by the director - const directorSlotInUse = Object.entries(session.currentSlots).some(([slot, sid]) => - parseInt(slot) === slotDefault && (sid === session.streamID || sid === session.streamID + ":s") - ); - - if (remoteSlotInUse || directorSlotInUse) { - slotDefault = null; // This slot is already in use - } - } - - // Determine final slot value - if (slotDefault !== null) { - // the default slot is available - biggestSlot = slotDefault; - } else if (slot_init && session.slotmode == 1) { - // was manually set, so can't be something else but 0 - biggestSlot = 0; - } else if (session.slotmode == 1) { - var bestfree = 0; - for (var i = 1; i <= biggestSlot; i++) { - if (allSlots.includes(i)) { - continue; - } else { - bestfree = i; - break; - } - } - biggestSlot = bestfree; - } - - // Set slot name - var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; - var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : ""; - - buttons += - `
    - -
    `; - - // Sync the initial state - syncSlotState(streamID, biggestSlot, false); // false since UI is being created here - } - buttons += - "
    ID: " + - streamID + - "\ - \ - \ - \ -
    "; - - container.innerHTML = buttons; - updateLockedElements(); - - var videoContainerControlBox = document.createElement("div"); - videoContainerControlBox.className = "controlVideoBox"; - container.containerControlBox = videoContainerControlBox; - container.appendChild(videoContainerControlBox); - - var videoContainer = document.createElement("div"); - videoContainer.id = "videoContainer_" + UUID; // needed to delete on user disconnect - videoContainer.style.margin = "0"; - videoContainer.style.position = "relative"; - videoContainer.style.minHeight = "30px"; - - videoContainerControlBox.appendChild(videoContainer); - - if (session.signalMeter) { - if (!session.rpcs[UUID].signalMeter) { - session.rpcs[UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true); - session.rpcs[UUID].signalMeter.id = "signalMeter_" + UUID; - session.rpcs[UUID].signalMeter.dataset.level = 0; - session.rpcs[UUID].signalMeter.classList.remove("hidden"); - session.rpcs[UUID].signalMeter.dataset.UUID = UUID; - session.rpcs[UUID].signalMeter.title = getTranslation("signal-meter"); - - if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.cpuLimited) { - // was quality_limitation_reason - session.rpcs[UUID].signalMeter.dataset.cpu = "1"; - } - - if (session.statsMenu !== false) { - session.rpcs[UUID].signalMeter.addEventListener("click", function (e) { - // show stats of video if double clicked - log("clicked signal meter"); - try { - e.preventDefault(); - if (session.statsMenu !== false) { - var uid = e.currentTarget.dataset.UUID; - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, uid); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - e.stopPropagation(); - return false; - } catch (e) { - errorlog(e); - } - }); - } - } - videoContainer.appendChild(session.rpcs[UUID].signalMeter); - } - - if (session.batteryMeter) { - if (!session.rpcs[UUID].batteryMeter) { - session.rpcs[UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true); - session.rpcs[UUID].batteryMeter.id = "batteryMeter_" + UUID; - batteryMeterInfoUpdate(UUID); - } - videoContainer.appendChild(session.rpcs[UUID].batteryMeter); - } - - if (session.showConnections) { - if (!session.rpcs[UUID].connectionDetails) { - createConnectionDetailsEle(UUID); - } - videoContainer.appendChild(session.rpcs[UUID].connectionDetails); - } - - var iframeDetails = document.createElement("div"); - iframeDetails.id = "iframeDetails_" + UUID; // needed to delete on user disconnect - iframeDetails.className = "iframeDetails hidden"; - - videoContainer.appendChild(session.rpcs[UUID].voiceMeter); - videoContainer.appendChild(session.rpcs[UUID].remoteMuteElement); - videoContainer.appendChild(session.rpcs[UUID].remoteVideoMuteElement); - videoContainer.appendChild(session.rpcs[UUID].remoteRaisedHandElement); - videoContainer.appendChild(iframeDetails); - container.appendChild(controls); - - session.group.forEach(group => { - var ele = controls.querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]'); - if (!ele) { - var newGroup = htmlToElement('"); - - var added = false; - container.querySelectorAll(".customGroup>[data-group]").forEach(ele => { - log(ele); - if (!added && ele.dataset.group > group + "") { - ele.parentNode.insertBefore(newGroup, ele); - added = true; - } - }); - if (!added) { - var newGroupCon = container.querySelector(".customGroup"); - if (!newGroupCon) { - newGroupCon = document.createElement("div"); - newGroupCon.classList.add("customGroup"); - container.appendChild(newGroupCon); - } - newGroupCon.appendChild(newGroup); - } - } - }); - - initSceneList(UUID); - syncSceneState(streamID); - syncOtherState(streamID); - - pokeIframeAPI("control-box", true, UUID); - - // Broadcast updated slots immediately so scenes with &viewslot update - if (session.slotmode && session.director) { - broadcastSlotUpdate(); - } -} - -function createControlBoxScreenshare(UUID, soloLink, streamID) { - if (document.getElementById("deleteme")) { - getById("deleteme").parentNode.removeChild(getById("deleteme")); - } - var controls = getById("controls_blank").cloneNode(true); - controls.classList.remove("hidden"); - controls.id = "controls_" + UUID; - - var container = document.createElement("div"); - container.className = "vidcon directorMargins"; - container.id = "container_" + UUID; // needed to delete on user disconnect - container.UUID = UUID; - container.dataset.UUID = UUID; - container.dataset.sid = streamID; - - if (session.orderby) { - try { - var added = false; - for (var i = 0; i < getById("guestFeeds").children.length; i++) { - if (getById("guestFeeds").children[i].UUID && !getById("guestFeeds").children[i].shifted) { - if (getById("guestFeeds").children[i].UUID in session.rpcs) { - if (session.rpcs[getById("guestFeeds").children[i].UUID].streamID.toLowerCase() > streamID.toLowerCase()) { - getById("guestFeeds").insertBefore(container, getById("guestFeeds").children[i]); - added = true; - break; - } - } - } - } - if (!added) { - getById("guestFeeds").appendChild(container); - } - } catch (e) { - getById("guestFeeds").appendChild(container); - } - } else { - getById("guestFeeds").appendChild(container); - } - - controls.querySelector(".controlsGrid").classList.add("notmain"); - - if (!session.rpcs[UUID].voiceMeter) { - if (session.meterStyle == 1) { - session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate2").cloneNode(true); - } else { - session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate").cloneNode(true); - session.rpcs[UUID].voiceMeter.style.opacity = 0; - if (session.meterStyle == 2) { - session.rpcs[UUID].voiceMeter.classList.add("video-meter-2"); - session.rpcs[UUID].voiceMeter.classList.remove("video-meter"); - } else { - session.rpcs[UUID].voiceMeter.classList.add("video-meter-director"); - } - } - session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID; - session.rpcs[UUID].voiceMeter.dataset.level = 0; - session.rpcs[UUID].voiceMeter.classList.remove("hidden"); - } - - session.rpcs[UUID].remoteMuteElement = getById("muteStateTemplate").cloneNode(true); - session.rpcs[UUID].remoteMuteElement.id = ""; - session.rpcs[UUID].remoteMuteElement.style.top = "5px"; - session.rpcs[UUID].remoteMuteElement.style.right = "7px"; - - session.rpcs[UUID].remoteVideoMuteElement = getById("videoMuteStateTemplate").cloneNode(true); - session.rpcs[UUID].remoteVideoMuteElement.id = ""; - session.rpcs[UUID].remoteVideoMuteElement.style.top = "5px"; - session.rpcs[UUID].remoteVideoMuteElement.style.right = "28px"; - - session.rpcs[UUID].remoteRaisedHandElement = getById("raisedHandTemplate").cloneNode(true); - session.rpcs[UUID].remoteRaisedHandElement.id = ""; - session.rpcs[UUID].remoteRaisedHandElement.style.top = "5px"; - session.rpcs[UUID].remoteRaisedHandElement.style.right = "49px"; - - var videoContainer = document.createElement("div"); - videoContainer.id = "videoContainer_" + UUID; // needed to delete on user disconnect - videoContainer.style.margin = "0"; - videoContainer.style.position = "relative"; - videoContainer.style.minHeight = "30px"; - - var iframeDetails = document.createElement("div"); - iframeDetails.id = "iframeDetails_" + UUID; // needed to delete on user disconnect - iframeDetails.className = "iframeDetails hidden"; - - //controls.innerHTML += ""; - //controls.innerHTML += ""; - - var handsID = "hands_" + UUID; - - controls.innerHTML += "
    "; - - if (session.hidesololinks == false) { - controls.innerHTML += - "
    \ - " + - sanitizeChat(soloLink) + - "\ - \ -
    "; - } - - controls.innerHTML += - '\ -
    '; - - controls.querySelectorAll("[data-action-type]").forEach(ele => { - // give action buttons some self-reference - ele.dataset.UUID = UUID; - ele.dataset.sid = streamID; - }); - - var buttons = ""; - if (session.slotmode) { - var biggestSlot = 0; - var slotDefault = null; - - // Check past slots first - if (streamID in session.pastSlots) { - slotDefault = session.pastSlots[streamID]; - } - - // Get all current slots from RPC state - var allSlots = []; - if (session.slotmode == 1) { - Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => { - if (currentSlot) { - if (parseInt(currentSlot) > biggestSlot) { - biggestSlot = parseInt(currentSlot); - } - if (slotDefault === parseInt(currentSlot)) { - slotDefault = null; - } - allSlots.push(parseInt(currentSlot)); - } - }); - biggestSlot += 1; - } - - // Determine final slot value - if (slotDefault !== null) { - biggestSlot = slotDefault; - } else if (session.slotmode == 1) { - var bestfree = 0; - for (var i = 1; i <= biggestSlot; i++) { - if (allSlots.includes(i)) { - continue; - } else { - bestfree = i; - break; - } - } - biggestSlot = bestfree; - } - - // Set slot name and update past slots - var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; - var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : ""; - session.pastSlots[streamID] = biggestSlot; - - // Build HTML with same structure - buttons += - `
    - -
    `; - - // Sync the initial state - syncSlotState(streamID, biggestSlot, false); // false since UI is being created here - } - - buttons += - "
    ID: " + - streamID + - "\ - \ - \ - \ -
    "; - - container.innerHTML = buttons; - updateLockedElements(); - - var videoContainerControlBox = document.createElement("div"); - videoContainerControlBox.className = "controlVideoBox"; - container.containerControlBox = videoContainerControlBox; - - container.appendChild(videoContainerControlBox); - videoContainerControlBox.appendChild(videoContainer); - - if (session.signalMeter) { - if (!session.rpcs[UUID].signalMeter) { - session.rpcs[UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true); - session.rpcs[UUID].signalMeter.id = "signalMeter_" + UUID; - session.rpcs[UUID].signalMeter.dataset.level = 0; - session.rpcs[UUID].signalMeter.classList.remove("hidden"); - session.rpcs[UUID].signalMeter.dataset.UUID = UUID; - session.rpcs[UUID].signalMeter.title = getTranslation("signal-meter"); - - //if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.cpu_maxed){ - // session.rpcs[UUID].signalMeter.dataset.cpu = "1"; - //} - - session.rpcs[UUID].signalMeter.addEventListener("click", function (e) { - // show stats of video if double clicked - log("clicked signal meter"); - try { - e.preventDefault(); - if (session.statsMenu !== false) { - var uid = e.currentTarget.dataset.UUID; - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, uid); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - e.stopPropagation(); - return false; - } catch (e) { - errorlog(e); - } - }); - } - videoContainer.appendChild(session.rpcs[UUID].signalMeter); - } - - if (session.batteryMeter) { - //////// - if (!session.rpcs[UUID].batteryMeter) { - session.rpcs[UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true); - session.rpcs[UUID].batteryMeter.id = "batteryMeter_" + UUID; - batteryMeterInfoUpdate(UUID); - } - videoContainer.appendChild(session.rpcs[UUID].batteryMeter); - } - - if (session.showConnections) { - if (!session.rpcs[UUID].connectionDetails) { - createConnectionDetailsEle(UUID); - } - videoContainer.appendChild(session.rpcs[UUID].connectionDetails); - } - - videoContainer.appendChild(session.rpcs[UUID].voiceMeter); - videoContainer.appendChild(session.rpcs[UUID].remoteMuteElement); - videoContainer.appendChild(session.rpcs[UUID].remoteVideoMuteElement); - videoContainer.appendChild(session.rpcs[UUID].remoteRaisedHandElement); - videoContainer.appendChild(iframeDetails); - videoContainer.appendChild(session.rpcs[UUID].videoElement); - container.appendChild(controls); - - session.group.forEach(group => { - var ele = controls.querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]'); - if (!ele) { - var newGroup = htmlToElement('"); - - var added = false; - container.querySelectorAll(".customGroup>[data-group]").forEach(ele => { - log(ele); - if (!added && ele.dataset.group > group + "") { - ele.parentNode.insertBefore(newGroup, ele); - added = true; - } - }); - if (!added) { - var newGroupCon = container.querySelector(".customGroup"); - if (!newGroupCon) { - newGroupCon = document.createElement("div"); - newGroupCon.classList.add("customGroup"); - container.appendChild(newGroupCon); - } - newGroupCon.appendChild(newGroup); - } - } - }); - - initSceneList(UUID); - pokeIframeAPI("control-box", true, UUID); -} - -function remoteRemoveQueue(ele) { - let ts = { ...transferSettings }; - ts.justResetting = true; - session.directMigrateIssue(session.roomid, ts, ele.dataset.UUID); - - ele.classList.add("hidden"); - try { - session.applyQueueStateChange(ele.dataset.UUID, false, "remote-remove-queue"); - } catch (e) { - errorlog(e); - } -} -function minimizeMe(button, director = false) { - var container = null; - if (!director) { - container = getById("container_" + button.dataset.UUID); - } else { - container = getById(director); - } - if (!container) { - return; - } - - var wasMinimized = container.classList.contains("minimized"); - if (!wasMinimized) { - var measuredWidth = container.offsetWidth || container.scrollWidth; - if (measuredWidth) { - container.dataset.minimizedWidth = measuredWidth; - } - } - - var isMinimized = container.classList.toggle("minimized"); - if (isMinimized) { - var storedWidth = parseFloat(container.dataset.minimizedWidth); - if (!storedWidth) { - storedWidth = container.scrollWidth || container.offsetWidth; - } - if (storedWidth) { - container.style.width = storedWidth + "px"; - container.style.minWidth = storedWidth + "px"; - } - } else { - container.style.removeProperty("width"); - container.style.removeProperty("min-width"); - var currentWidth = container.offsetWidth || container.scrollWidth; - if (currentWidth) { - container.dataset.minimizedWidth = currentWidth; - } else { - delete container.dataset.minimizedWidth; - } - } -} - -function blackoutMode() { - var overlay = document.getElementById("blackoutOverlay"); - if (!overlay) { - overlay = document.createElement('div'); - overlay.id = "blackoutOverlay"; - overlay.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: black; - color: white; - display: flex; - justify-content: center; - align-items: center; - font-size: 14px; - cursor: pointer; - z-index: 9999; - `; - overlay.textContent = 'Click to exit black-out mode'; - document.body.appendChild(overlay); - } else { - overlay.classList.remove("hidden"); - } - - function exitBlackout() { - overlay.classList.add("hidden"); - overlay.removeEventListener('click', exitBlackout); - } - - overlay.addEventListener('click', exitBlackout); -} - -function cycleCameras() { - if (session.screenShareState) { - warnUser("Stop the screen-share first."); - return; - } - var videoSelect = document.querySelector("select#videoSource3").options; - // don't show flip option if only one camera. - // don't show if not a mobile device - // don't show if AD=0 - - var matched = false; - var maxIndex = parseInt(getById("flipcamerabutton").dataset.maxIndex) || parseInt(videoSelect.length); - if (maxIndex > parseInt(videoSelect.length)) { - maxIndex = parseInt(videoSelect.length); - } - - for (var i = 0; i < maxIndex; i++) { - var selOption = videoSelect[i]; - if (selOption.selected) { - matched = true; - } else if (matched) { - if (getById("flipcamerabutton").classList.contains("flip")) { - getById("flipcamerabutton").classList.remove("flip"); - getById("flipcamerabutton").classList.add("flip2"); - } else { - getById("flipcamerabutton").classList.remove("flip2"); - getById("flipcamerabutton").classList.add("flip"); - } - document.querySelector("select#videoSource3").value = selOption.value; - activatedPreview = false; - grabVideo(session.quality, "videosource", "select#videoSource3"); - return; - } - } - for (var i = 0; i < maxIndex; i++) { - var selOption = videoSelect[i]; - if (selOption.selected) { - return; // do nothing; the camera that is selected is the only camera available it seems. - } else { - if (getById("flipcamerabutton").classList.contains("flip")) { - getById("flipcamerabutton").classList.remove("flip"); - getById("flipcamerabutton").classList.add("flip2"); - } else { - getById("flipcamerabutton").classList.remove("flip2"); - getById("flipcamerabutton").classList.add("flip"); - } - document.querySelector("select#videoSource3").value = selOption.value; - activatedPreview = false; - grabVideo(session.quality, "videosource", "select#videoSource3"); - return; - } - } -} - -function addToGoogleCalendar() { - var title = "Live Stream"; - //var dates = "20180512T230000Z/20180513T030000Z"; - var linkout = getById("director_block_1").innerText; - var details = "Join the live stream as a performer at the following link:

    ===> " + linkout + "

    To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest

    Do not share the details of this invite with others, unless explicitly told to."; - details = details.split(" ").join("+"); - details = details.split("&").join("%26"); - var linkToOpen = "https://calendar.google.com/calendar/r/eventedit?text=" + title + "&details=" + details; - //https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134 - - window.open(linkToOpen); -} - -function addToOutlookCalendar() { - var title = "Live Stream"; - var linkout = getById("director_block_1").innerText; - var details = "Join the live stream as a performer at the following link:

    ===> " + linkout + "

    To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest

    Do not share the details of this invite with others, unless explicitly told to."; - details = details.split(" ").join("%20"); - details = details.split("&").join("%26"); - - var linkToOpen = "https://outlook.live.com/owa/?path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&subject=" + title + "&body=" + details; - //https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134 - - window.open(linkToOpen); -} - -function addToYahooCalendar() { - var title = "Live Stream"; - var linkout = getById("director_block_1").innerText; - var details = "Join the live stream as a performer at the following link:

    ===> " + linkout + "

    To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest

    Do not share the details of this invite with others, unless explicitly told to."; - details = details.split(" ").join("%20"); - details = details.split("&").join("%26"); - var linkToOpen = "https://calendar.yahoo.com?v60&title=" + title + "&desc=" + details; - //https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134 - - window.open(linkToOpen); -} - -function toggle(ele, tog = false, inline = true) { - var x = ele; - if (x.style.display === "none") { - if (inline) { - x.style.display = "inline-block"; - } else { - x.style.display = "block"; - } - } else { - x.style.display = "none"; - } - if (tog) { - if (tog.dataset.saved) { - tog.innerHTML = tog.dataset.saved; - delete tog.dataset.saved; - } else { - tog.dataset.saved = tog.innerHTML; - tog.innerHTML = "Hide This"; - } - } -} - -function toggleByDataset(filter) { - var elements = document.querySelectorAll('[data-cluster="' + filter + '"]'); // ie: .cluster1 - for (var i = 0; i < elements.length; i++) { - elements[i].classList.toggle("hidden"); - } -} - -var SelectedAudioOutputDevices = false; // session.sink -var SelectedAudioInputDevices = []; // .. -var SelectedVideoInputDevices = []; // .. - -async function enumerateDevices() { - log("enumerated start"); - - const timeout = new Promise((_, reject) => - setTimeout(() => reject(function () { - if (!session.cleanOutput) { - warnUser("The browser has not responded to our request to list available media devices.\n\nPossible solutions:\n\n- Restart the computer and try again\n- Try another browser\n- Remove or uninstall devices that are not needed\n- Uninstall and reinstall your browser"); - } - new Error("Device enumeration timed out.\n\nThe browser has not responded to our request to list available media devices.\n\nPossible solutions:\n\n- Restart the computer and try again\n- Try another browser\n- Remove or uninstall devices that are not needed\n- Uninstall and reinstall your browser"); - }), 15000) - ); - - const enumeratePromise = new Promise(async (resolve, reject) => { - try { - if (typeof navigator.mediaDevices === "object" && typeof navigator.mediaDevices.enumerateDevices === "function") { - resolve(await navigator.mediaDevices.enumerateDevices()); - } else if (typeof navigator.enumerateDevices === "function") { - log("enumerated failed 1"); - resolve(await navigator.enumerateDevices()); - } else { - window.MediaStreamTrack.getSources(devices => { - resolve( - devices - .filter(device => { - return device.kind.toLowerCase() === "video" || device.kind.toLowerCase() === "videoinput"; - }) - .map(device => { - return { - deviceId: device.deviceId != null ? device.deviceId : "", - groupId: device.groupId, - kind: "videoinput", - label: device.label, - toJSON: /* istanbul ignore next */ function () { - return this; - } - }; - }) - ); - }); - } - } catch (e) { - errorlog(e); - if (!session.cleanOutput) { - if (location.protocol !== "https:") { - warnUser("Error listing the media devices.\n\nYour browser will not allow access to media devices without SSL enabled.\n\nPossible solutions include switching to https, accessing the site from http://localhost, or enabling the `unsafely-treat-insecure-origin-as-secure` browser switch."); - } else if ("isSecureContext" in window && window.isSecureContext === false) { - warnUser("Error listing the media devices.\n\nThe website may have assets loaded in an insecure context."); - } else { - warnUser("An unknown error occured while trying to list the media devices."); - } - } - reject(e); - } - }); - - return Promise.race([enumeratePromise, timeout]); -} - -function requestOutputAudioStream() { - try { - //warnlog("GET USER MEDIA"); - warnlog("navigator.mediaDevices.getUserMedia starting..."); - return navigator.mediaDevices - .getUserMedia({ - audio: true, - video: false - }) - .then(function (stream1) { - // Apple needs thi to happen before I can access EnumerateDevices. - log("get media sources; request audio stream"); - return enumerateDevices().then(function (deviceInfos) { - stream1.getTracks().forEach(function (track) { - // We don't want to keep it without audio; so we are going to try to add audio now. - track.stop(); // I need to do this after the enumeration step, else it breaks firefox's labels - }); - const audioOutputSelect = getById("outputSourceScreenshare"); - audioOutputSelect.remove(0); - audioOutputSelect.removeAttribute("onclick"); - - for (let i = 0; i !== deviceInfos.length; ++i) { - const deviceInfo = deviceInfos[i]; - if (deviceInfo == null) { - continue; - } - const option = document.createElement("option"); - option.value = deviceInfo.deviceId; - if (deviceInfo.kind === "audiooutput") { - const option = document.createElement("option"); - if (audioOutputSelect.length === 0) { - option.dataset.default = true; - } else { - option.dataset.default = false; - } - option.value = deviceInfo.deviceId || "default"; - if (option.value == session.sink) { - option.selected = "true"; - } - option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; - audioOutputSelect.appendChild(option); - } else { - log("Some other kind of source/device: ", deviceInfo); - } - } - }); - }); - } catch (e) { - if (!session.cleanOutput) { - if (window.isSecureContext) { - warnUser("An error has occured when trying to access the default audio device. The reason is not known."); - } else if (iOS || iPad) { - warnUser("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported."); - } else { - warnUser("Error accessing the default audio device.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia"); - } - } - } -} - -let selectedScreenShareAudioDevices = []; -async function requestAudioStream() { // for the screen share. - const deviceList = document.getElementById('audioDeviceList'); - const errorElement = document.getElementById('audioSelectError'); - const showDevicesButton = document.getElementById('showAudioDevices'); - - try { - // Request audio permission first - const stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: false - }); - - // Stop tracks after getting permission - stream.getTracks().forEach(track => track.stop()); - - // Enumerate devices - const devices = await navigator.mediaDevices.enumerateDevices(); - const audioInputs = devices.filter(device => device.kind === 'audioinput'); - - // Clear and show device list - deviceList.innerHTML = ''; - deviceList.style.display = 'block'; - showDevicesButton.style.display = 'none'; - - selectedScreenShareAudioDevices = []; - - // Create checkbox for each device - audioInputs.forEach(device => { - const deviceLabel = device.label || `Microphone ${device.deviceId.slice(0, 4)}`; - - const div = document.createElement('div'); - div.className = 'audio-device-item'; - - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.id = device.deviceId; - checkbox.value = device.deviceId; - - // Check if device was previously selected - if (session.audioDevice && ((typeof session.audioDevice === 'object' && session.audioDevice.includes(device.deviceId)) || normalizeDeviceLabel(deviceLabel).includes(session.audioDevice))) { - checkbox.checked = true; - selectedScreenShareAudioDevices.push(device.deviceId); - } - - checkbox.addEventListener('change', function () { - // Update session.audioDevice array - // if (!session.audioDevice || typeof session.audioDevice !== 'object') { - // session.audioDevice = []; - // } - if (!selectedScreenShareAudioDevices || typeof selectedScreenShareAudioDevices !== 'object') { - selectedScreenShareAudioDevices = []; - } - - if (this.checked) { - // if (!session.audioDevice.includes(this.value)) { - // session.audioDevice.push(this.value); - // } - if (!selectedScreenShareAudioDevices.includes(this.value)) { - selectedScreenShareAudioDevices.push(this.value); - } - } else { - //session.audioDevice = session.audioDevice.filter(id => id !== this.value); - selectedScreenShareAudioDevices = selectedScreenShareAudioDevices.filter(id => id !== this.value); - } - }); - - const label = document.createElement('label'); - label.htmlFor = device.deviceId; - label.textContent = deviceLabel; - - div.appendChild(checkbox); - div.appendChild(label); - deviceList.appendChild(div); - }); - - errorElement.style.display = 'none'; - - } catch (e) { - let errorMessage = ''; - - if (!window.isSecureContext) { - errorMessage = 'This website must be loaded in a secure context (HTTPS) to access audio devices.'; - } else if (/ipad|iphone|ipod/.test(navigator.userAgent.toLowerCase())) { - errorMessage = 'iOS 13.4 or later is recommended for audio device access.'; - } else { - errorMessage = 'An error occurred while accessing audio devices.'; - } - - errorElement.textContent = errorMessage; - errorElement.style.display = 'block'; - } -} - -function saveSettings() { - if (session.store) { - try { - var tmp = {}; - if (SelectedAudioInputDevices) { - tmp.SelectedAudioInputDevices = SelectedAudioInputDevices.filter(n => n); - } - if (session.sink && session.sink != "default") { - tmp.SelectedAudioOutputDevices = session.sink; - } else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices != "default") { - tmp.SelectedAudioOutputDevices = SelectedAudioOutputDevices; - } - tmp.SelectedVideoInputDevices = SelectedVideoInputDevices; - setStorage("session_store", JSON.stringify(tmp)); - log("Saving settings"); - } catch (e) { - errorlog(e); - } - } -} - -function loadSettings() { - if (session.store) { - try { - session.store = getStorage("session_store"); - if (session.store) { - session.store = JSON.parse(session.store); - } else { - session.store = {}; - } - - if (session.store && session.store.SelectedAudioOutputDevices) { - if (typeof session.store.SelectedAudioOutputDevices == "string") { - SelectedAudioOutputDevices = session.store.SelectedAudioOutputDevices; - } else if (typeof session.store.SelectedAudioOutputDevices == "object") { - if (session.store.SelectedAudioOutputDevices.length) { - SelectedAudioOutputDevices = session.store.SelectedAudioOutputDevices[0]; - } - } - } - if (session.store && session.store.SelectedAudioInputDevices) { - session.store.SelectedAudioInputDevices = session.store.SelectedAudioInputDevices.filter(n => n); - SelectedAudioInputDevices = session.store.SelectedAudioInputDevices; - } - if (session.store && session.store.SelectedVideoInputDevices) { - SelectedVideoInputDevices = session.store.SelectedVideoInputDevices; - } - } catch (e) { } - } -} - -function normalizeDeviceLabel(deviceName) { - return String(deviceName).replace(/[\W]+/g, "_").toLowerCase(); -} - -// Conservative audio label normalizer to alias Windows "Default -" / "Communications -" prefixed devices -function normalizeAudioAliasLabel(label) { - try { - if (!label) return ""; - let s = String(label).trim(); - // Normalize case and whitespace - s = s.replace(/^\s+|\s+$/g, ""); - // Remove leading Default/Communications prefixes with common separators ("-", ":", em/en dashes) - // Keep the rest intact (do NOT strip digits or other differences) - s = s.replace(/^(?:Default|Communications)\s*[-:\u2013\u2014]?\s*/i, ""); - return s.toLowerCase(); - } catch (e) { - return String(label || "").toLowerCase(); - } -} - -function gotDevices(deviceInfos, miconly = false) { - log("got devices!1"); - log(deviceInfos); - - deviceInfos.sort((a, b) => { - // Put "default" devices first - if (a.deviceId.toLowerCase() === "default") return -1; - if (b.deviceId.toLowerCase() === "default") return 1; - - // Then sort by label if both exist - if (a.label && b.label) { - return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); - } - - return 0; - }); - - try { - if (Firefox && !FirefoxEnumerated) { - if (session.streamSrc && session.streamSrc.getTracks().length) { - FirefoxEnumerated = true; - } - } - - var option = document.createElement("input"); - option.type = "checkbox"; - option.value = "ZZZ"; - option.name = "multiselect1"; - option.id = "multiselect1"; - option.style.display = "none"; - option.checked = true; - - var label = document.createElement("label"); - label.for = option.name; - label.innerHTML = ' No Audio'; - - var listele = document.createElement("li"); - listele.appendChild(option); - listele.appendChild(label); - - const audioInputSelect = document.getElementById("audioSource") || document.getElementById("audioSource3"); - audioInputSelect.innerHTML = ""; - audioInputSelect.appendChild(listele); - - const audioOutputSelect = document.getElementById("outputSource") || document.getElementById("outputSource3"); - audioOutputSelect.innerHTML = ""; - - option.onchange = function (event) { - // make sure to clear 'no audio option' if anything else is selected - if (!getById("multiselect1").checked) { - getById("multiselect1").checked = true; - } else { - var list = audioInputSelect.querySelectorAll("li>input"); - for (var i = 0; i < list.length; i++) { - if (list[i].id !== "multiselect1") { - list[i].checked = false; - } - } - } - SelectedAudioInputDevices = [event.currentTarget.value]; - saveSettings(); - }; - - const multiselectTrigger = document.getElementById("multiselect-trigger") || document.getElementById("multiselect-trigger3"); - multiselectTrigger.dataset.state = "0"; - multiselectTrigger.classList.add("closed"); - multiselectTrigger.classList.remove("open"); - getById("chevarrow1").classList.add("bottom"); - - const videoSelect = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3"); - const selectors = [videoSelect]; - - const values = selectors.map(select => select.value); - selectors.forEach(select => { - while (select.firstChild) { - select.removeChild(select.firstChild); - } - }); - - function comp(a, b) { - if (a.kind === "audioinput") { - return 0; - } else if (a.kind === "audiooutput") { - return 0; - } - const labelA = a.label.toUpperCase(); - const labelB = b.label.toUpperCase(); - if (labelA > labelB) { - return 1; - } else if (labelA < labelB) { - return -1; - } - return 0; - } - //deviceInfos.sort(comp); // I like this idea, but it messes with the defaults. I just don't know what it will do. - var deviceInfo; - - // This is to hide NDI from default device. NDI Tools fucks up. - var tmp = []; - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (!(deviceInfo.kind === "videoinput" && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek")))) { - tmp.push(deviceInfo); - } - } - - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (deviceInfo.kind === "videoinput" && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek"))) { - tmp.push(deviceInfo); - log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); - } - } - deviceInfos = tmp; - - if (typeof session.audioDevice == "object") { - // this sorts according to users's manual selection - var matched1 = []; - var matched2 = []; - var notmatched = []; - for (let i = 0; i !== deviceInfos.length; ++i) { - if (deviceInfos[i].kind === "audioinput") { - var deviceMatched = false; - if (session.audioDevice.includes(deviceInfos[i].deviceId)) { - matched1.push(deviceInfos[i]); - deviceMatched = true; - } else if (session.audioDevice.includes(normalizeDeviceLabel(deviceInfos[i].label))) { - matched1.push(deviceInfos[i]); - deviceMatched = true; - } else { - for (var j = 0; j < session.audioDevice.length; j++) { - if (normalizeDeviceLabel(deviceInfos[i].label).includes(session.audioDevice[j])) { - matched2.push(deviceInfos[i]); - log("A DEVICE FOUND = " + deviceInfos[i].label); - deviceMatched = true; - break; - } - } - } - if (!deviceMatched) { - notmatched.push(deviceInfos[i]); - } - } else { - notmatched.push(deviceInfos[i]); - } - } - - matched2.sort((a, b) => { - if (a.label && b.label) { - if (a.label.length < b.label.length) { - return -1 - } - return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); - } - return 0; - }); - - var matched = matched1.concat(matched2); - deviceInfos = matched.concat(notmatched); - } else if (session.store && session.store.SelectedAudioInputDevices) { - var matched = []; - var notmatch = []; - for (let i = 0; i < deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (session.store.SelectedAudioInputDevices.includes(deviceInfo.deviceId)) { - matched.push(deviceInfo); - log("EXACT A DEVICE FOUND -- from saved session"); - } else { - notmatch.push(deviceInfo); - } - } - deviceInfos = matched.concat(notmatch); - } - - if (session.sink || SelectedAudioOutputDevices) { - // this sorts according to users's manual selection - var matched = []; - var notmatch = []; - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (deviceInfo.kind === "audiooutput" && deviceInfo.deviceId === session.sink) { - matched.push(deviceInfo); - } else if (!session.sink && deviceInfo.kind === "audiooutput" && deviceInfo.deviceId === SelectedAudioOutputDevices) { - matched.push(deviceInfo); - } else { - notmatch.push(deviceInfo); - } - } - deviceInfos = matched.concat(notmatch); - } - - if (session.videoDevice && session.videoDevice !== 1) { - var tmp = []; - var tmp2 = []; - var tmp3 = []; - var deviceIdMatch = false; - - // First pass - check for label matches - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).startsWith(session.videoDevice)) { - tmp.push(deviceInfo); - log("Starts With V DEVICE FOUND"); - } else if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice)) { - tmp2.push(deviceInfo); - log("Includes With V DEVICE FOUND"); - } else { - tmp3.push(deviceInfo); - } - } - - // If no label matches found, try device ID match - if (tmp.length === 0 && tmp2.length === 0) { - for (let i = 0; i < tmp3.length; ++i) { - deviceInfo = tmp3[i]; - if (deviceInfo.kind === "videoinput" && deviceInfo.deviceId === session.videoDevice) { - tmp.push(deviceInfo); - deviceIdMatch = true; - log("EXACT DEVICE ID MATCH FOUND"); - break; - } - } - } - - if (tmp2.length && !deviceIdMatch) { - tmp = tmp.concat(tmp2); - } - if (tmp3.length) { - tmp = tmp.concat(tmp3); - } - - deviceInfos = tmp; - log("VDEVICE:" + session.videoDevice); - log(deviceInfos); - } else if (session.videoDevice === false && session.facingMode) { - var tmp = []; - if (session.facingMode == "environment") { - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("back")) { - tmp.push(deviceInfo); - log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); - } else if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("rear")) { - tmp.push(deviceInfo); - log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); - } - } - } else if (session.facingMode == "user") { - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("front")) { - tmp.push(deviceInfo); - log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); - } - } - } - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (!(deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice))) { - if (deviceInfo.deviceId !== session.videoDevice) { - tmp.push(deviceInfo); - } - } - } - deviceInfos = tmp; - log("VDECICE:" + session.videoDevice); - log(deviceInfos); - } else if (session.store && session.store.SelectedVideoInputDevices && session.videoDevice === false) { - var matched = []; - var notmatch = []; - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (session.store.SelectedVideoInputDevices.includes(deviceInfo.deviceId)) { - matched.push(deviceInfo); - log("EXACT V DEVICE FOUND -- from saved session"); - } else { - notmatch.push(deviceInfo); - } - } - deviceInfos = matched.concat(notmatch); - delete session.store.SelectedVideoInputDevices; - } else if (session.mobile) { - var tmp = []; - - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("front")) { - tmp.push(deviceInfo); - log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); - } - } - - for (let i = 0; i !== deviceInfos.length; ++i) { - deviceInfo = deviceInfos[i]; - if (!(deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice))) { - if (deviceInfo.deviceId !== session.videoDevice) { - tmp.push(deviceInfo); - } - } - } - deviceInfos = tmp; - log("AUTO FRONT:" + session.videoDevice); - log(deviceInfos); - } - - if (session.audioDevice && typeof session.audioDevice == "object") { - var adMatch = [...session.audioDevice]; - } else if (session.store && session.store.SelectedAudioInputDevices && session.store.SelectedAudioInputDevices.length) { - var adMatch = [...session.store.SelectedAudioInputDevices]; - } else { - var adMatch = false; - } - - if (session.store && session.store.SelectedAudioInputDevices) { - delete session.store.SelectedAudioInputDevices; - } - - var counter = 1; - var addedDeviceIds = new Set(); // Track already added devices - for (let i = 0; i !== deviceInfos.length; ++i) { - var deviceInfo = deviceInfos[i]; - if (deviceInfo == null) { - continue; - } - - if (deviceInfo.kind === "audioinput") { - // Skip if this device was already added - if (addedDeviceIds.has(deviceInfo.deviceId)) { - log("Skipping duplicate audio device: " + deviceInfo.label); - continue; - } - addedDeviceIds.add(deviceInfo.deviceId); - option = document.createElement("input"); - option.type = "checkbox"; - counter++; - listele = document.createElement("li"); - listele.style.display = "none"; - - if (typeof adMatch == "object") { - for (var j = 0; j < adMatch.length; j++) { - if (!adMatch[j]) { - // skip, already matched - } else if (adMatch[j] == deviceInfo.deviceId) { - option.checked = true; - listele.style.display = "block"; - option.style.display = "none"; - getById("multiselect1").checked = false; - try { - getById("multiselect1").parentNode.style.display = "none"; - } catch (e) { } - adMatch[j] = null; - break; - } else if (normalizeDeviceLabel(deviceInfo.label).includes(adMatch[j])) { - option.checked = true; - listele.style.display = "block"; - option.style.display = "none"; - getById("multiselect1").checked = false; - try { - getById("multiselect1").parentNode.style.display = "none"; - } catch (e) { } - adMatch[j] = null; - break; - } - } - } - - if (typeof adMatch !== "object" && counter == 2) { - option.checked = true; - listele.style.display = "block"; - option.style.display = "none"; - getById("multiselect1").checked = false; - try { - getById("multiselect1").parentNode.style.display = "none"; - } catch (e) { } - } - - option.value = deviceInfo.deviceId || "default"; - option.name = "multiselect" + counter; - option.id = "multiselect" + counter; - option.label = deviceInfo.label; - - label = document.createElement("label"); - label.for = option.name; - - label.innerHTML = " " + (deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1)); - - listele.appendChild(option); - listele.appendChild(label); - audioInputSelect.appendChild(listele); - - option.onchange = function (event) { - // make sure to clear 'no audio option' if anything else is selected - getById("multiselect1").checked = false; - log("UNCHECKED"); - if (!CtrlPressed) { - SelectedAudioInputDevices = []; - audioInputSelect.querySelectorAll("input[type='checkbox']").forEach(function (item) { - if (event.currentTarget.id !== item.id) { - item.checked = false; - } else { - item.checked = true; - SelectedAudioInputDevices = [event.currentTarget.value]; - } - }); - } else { - if (event.currentTarget.checked) { - if (!SelectedAudioInputDevices) { - SelectedAudioInputDevices = [event.currentTarget.value]; - } else if (!SelectedAudioInputDevices.includes(event.currentTarget.value)) { - SelectedAudioInputDevices.push(event.currentTarget.value); - } - } else if (event.currentTarget.value) { - while (SelectedAudioInputDevices.includes(event.currentTarget.value)) { - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(event.currentTarget.value), 1); - } - } - } - if (session.mobile && !(iOS || iPad) && event.currentTarget.label === "USB audio" && !session.cleanOutput) { - warnUser("Notice: USB audio devices may not work on all mobile devices.\n\nConsider using FireFox mobile instead, as it tends to work with USB audio devices more often."); - } - saveSettings(); - }; - - if (deviceInfo.label.includes("Yeti ")) { - if (!session.cleanOutput) { - //getById("audioTipContext1").innerHTML = getTranslation("blue-yeti-tip"); - miniTranslate(getById("audioTipContext1"), "blue-yeti-tip"); - getById("audioTip1").classList.remove("hidden"); - } - } - } else if (deviceInfo.kind === "videoinput") { - option = document.createElement("option"); - option.value = deviceInfo.deviceId || "default"; - option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; - videoSelect.appendChild(option); - } else if (deviceInfo.kind === "audiooutput") { - option = document.createElement("option"); - if (audioOutputSelect.length === 0) { - option.dataset.default = true; - } else { - option.dataset.default = false; - } - option.value = deviceInfo.deviceId || "default"; - if (option.value == session.sink) { - option.selected = "true"; - } else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices == option.value) { - option.selected = "true"; - } - option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; - audioOutputSelect.appendChild(option); - } else { - log("Some other kind of source/device: ", deviceInfo); - } - } - - if (Firefox && !session.mobile) { - var option = document.createElement("option"); - option.value = "others"; - option.text = getTranslation("show-more-options"); - audioOutputSelect.appendChild(option); - } - - if (audioOutputSelect.childNodes.length == 0) { - option = document.createElement("option"); - option.value = "default"; - option.text = getTranslation("system-default"); - audioOutputSelect.appendChild(option); - } - - option = document.createElement("option"); - option.text = getTranslation("disable-video"); - option.value = "ZZZ"; - videoSelect.appendChild(option); // NO AUDIO OPTION - - if (miconly) { - option.selected = "true"; - } - - selectors.forEach((select, selectorIndex) => { - if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) { - select.value = values[selectorIndex]; - } - }); - } catch (e) { - errorlog(e); - } -} - -function getUserMediaVideoParams(resolutionFallbackLevel, isSafariBrowser) { - switch (resolutionFallbackLevel) { - case -1: - return {}; - case -2: - if (isSafariBrowser) { - return { - width: { - min: 360, - ideal: 3840, - max: 3840 - }, - height: { - min: 360, - ideal: 2160, - max: 2160 - } - }; - } else if (Firefox) { - return { - width: { - ideal: 3840 - }, - height: { - ideal: 2160 - } - }; - } else { - return { - width: { - min: 720, - ideal: 3840, - max: 3840 - }, - height: { - min: 720, - ideal: 2160, - max: 2160 - } - }; - } - case -3: - if (isSafariBrowser) { - return { - width: { - min: 360, - ideal: 2560, - max: 1440 - }, - height: { - min: 360, - ideal: 1440, - max: 1440 - } - }; - } else if (Firefox) { - return { - width: { - ideal: 2560 - }, - height: { - ideal: 1440 - } - }; - } else { - return { - width: { - min: 720, - ideal: 2560, - max: 2560 - }, - height: { - min: 720, - ideal: 1440, - max: 1440 - } - }; - } - case 0: - if (isSafariBrowser) { - return { - width: { - min: 360, - ideal: 1920, - max: 1920 - }, - height: { - min: 360, - ideal: 1080, - max: 1080 - } - }; - } else if (Firefox) { - return { - width: { - ideal: 1920 - }, - height: { - ideal: 1080 - } - }; - } else { - return { - width: { - min: 720, - ideal: 1920, - max: 1920 - }, - height: { - min: 720, - ideal: 1080, - max: 1920 - } - }; - } - case 1: - if (isSafariBrowser) { - return { - width: { - min: 360, - ideal: 1280, - max: 1280 - }, - height: { - min: 360, - ideal: 720, - max: 720 - } - }; - } else if (Firefox) { - return { - width: { - ideal: 1280 - }, - height: { - ideal: 720 - } - }; - } else { - return { - width: { - min: 720, - ideal: 1280, - max: 1280 - }, - height: { - min: 720, - ideal: 720, - max: 1280 - } - }; - } - case 2: - if (isSafariBrowser) { - return { - width: { - min: 640 - }, - height: { - min: 360 - } - }; - } else if (Firefox) { - return { - width: { - ideal: 640 - }, - height: { - ideal: 360 - } - }; - } else { - return { - width: { - min: 240, - ideal: 640, - max: 1280 - }, - height: { - min: 240, - ideal: 360, - max: 1280 - } - }; - } - case 3: - if (isSafariBrowser) { - return { - width: { - min: 360, - ideal: 1280, - max: 1440 - } - }; - } else { - return { - width: { - min: 360, - ideal: 1280, - max: 1440 - } - }; - } - case 4: - if (isSafariBrowser) { - return { - height: { - min: 360, - ideal: 720, - max: 960 - } - }; - } else { - return { - height: { - ideal: 720, - max: 960 - } - }; - } - case 5: - if (isSafariBrowser) { - return { - width: { - min: 360, - ideal: 640, - max: 1440 - }, - height: { - min: 360, - ideal: 360, - max: 720 - } - }; - } else { - return { - width: { - ideal: 640, - max: 1920 - }, - height: { - ideal: 360, - max: 1920 - } - }; // same as default, but I didn't want to mess with frameRates until I gave it all a try first - } - case 6: - if (isSafariBrowser) { - return {}; // iphone users probably don't need to wait any longer, so let them just get to it - } else { - return { - width: { - min: 360, - ideal: 640, - max: 3840 - }, - height: { - min: 360, - ideal: 360, - max: 2160 - } - }; - } - case 7: - return { - // If the camera is recording in low-light, it may have a low frameRate. It coudl also be recording at a very high resolution. - width: { - min: 360, - ideal: 640 - }, - height: { - min: 360, - ideal: 360 - } - }; - - case 8: - return { - width: { - min: 360 - }, - height: { - min: 360 - }, - frameRate: 10 - }; // same as default, but I didn't want to mess with frameRates until I gave it all a try first - case 9: - return { - frameRate: 0 - }; // Some Samsung Devices report they can only support a frameRate of 0. - case 10: - return {}; - default: - return {}; - } -} - -function addScreenDevices(device) { - if (device.kind == "audio") { - const audioInputSelect = getById("audioSource3"); - const listele = document.createElement("li"); - listele.style.display = "block"; - - const option = document.createElement("input"); - option.type = "checkbox"; - option.checked = true; - - if (getById("multiselect-trigger3").dataset.state == 0) { - option.style.display = "none"; - } - - option.value = device.id; - option.name = device.label; - option.dataset.type = "screen"; - option.label = device.label; - - const label = document.createElement("label"); - label.for = option.name; - label.innerHTML = " " + device.label; - listele.appendChild(option); - listele.appendChild(label); - - option.onchange = function (event) { - // make sure to clear 'no audio option' if anything else is selected - log("change 4644"); - if (!CtrlPressed) { - document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { - if (!item.value) { - return; - } - if (event.currentTarget.value !== item.value) { - // this shoulnd't happen, but if it does. - item.checked = false; - if (item.dataset.type == "screen") { - item.parentElement.parentElement.removeChild(item.parentElement); - } - while (SelectedAudioInputDevices.indexOf(item.value) > -1) { - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); - } - activatedPreview = false; - grabAudio("#audioSource3"); // exclude item.id - } else { - if (SelectedAudioInputDevices.indexOf(item.value) == -1) { - if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { - SelectedAudioInputDevices = []; - } - SelectedAudioInputDevices.push(item.value); - } - item.checked = true; - activatedPreview = false; - grabAudio("#audioSource3", item.value); // exclude item.id. we will reconnect, even if already connected, as a way to 'reset' a device if it isn't working. - } - }); - } - saveSettings(); - event.stopPropagation(); - return false; - }; - audioInputSelect.appendChild(listele); - getById("audioSourceNoAudio2").checked = false; - } else if (device.kind == "video") { - const videoSelect = getById("videoSource3"); - //const selectors = [ videoSelect]; - //const values = selectors.map(select => select.value); - const option = document.createElement("option"); - option.value = device.id; - option.text = device.label; - option.selected = "true"; - option.label = device.label; - videoSelect.appendChild(option); - } -} - -var gotDevices2AlreadyRan = false; -function gotDevices2(deviceInfos) { - gotDevices2AlreadyRan = true; - log("got devices!2"); - log(deviceInfos); - getById("multiselect-trigger3").dataset.state = "0"; - getById("multiselect-trigger3").classList.add("closed"); - getById("multiselect-trigger3").classList.remove("open"); - getById("chevarrow2").classList.add("bottom"); - - if (!session.streamSrc) { - checkBasicStreamsExist(); - } - - var knownTrack = false; - - try { - const audioInputSelect = getById("audioSource3"); - const videoSelect = getById("videoSource3"); - const audioOutputSelect = getById("outputSource3"); - const selectors = [videoSelect]; - - // Build active audio deviceId and label sets to avoid duplicate selection - const activeAudioIds = new Set(); - const activeAudioLabels = new Set(); - const activeAudioNormLabels = new Set(); - try { - if (session.streamSrc) { - session.streamSrc.getAudioTracks().forEach(function (t) { - try { - if (t.label) { - activeAudioLabels.add(t.label); - activeAudioNormLabels.add(normalizeAudioAliasLabel(t.label)); - } - if (t.getSettings) { - const s = t.getSettings(); - if (s && s.deviceId) { - activeAudioIds.add(s.deviceId); - } - } - } catch (e) { } - }); - } - } catch (e) { } - - // Identify normalized labels that have non-default/communications entries - const nonDefaultNormLabelSet = new Set(); - try { - for (let i = 0; i !== deviceInfos.length; ++i) { - const d = deviceInfos[i]; - if (!d || d.kind !== "audioinput") continue; - const id = (d.deviceId || "").toLowerCase(); - if (id !== "default" && id !== "communications" && d.label) { - nonDefaultNormLabelSet.add(normalizeAudioAliasLabel(d.label)); - } - } - } catch (e) { } - - // Track which normalized labels we've already auto-checked to avoid duplicates - const checkedByNormLabel = new Set(); - // Track deviceIds we've already added to avoid duplicate entries from buggy drivers - const addedDeviceIds = new Set(); - - [audioInputSelect].forEach(select => { - while (select.firstChild) { - select.removeChild(select.firstChild); - } - }); - - const values = selectors.map(select => select.value); - selectors.forEach(select => { - while (select.firstChild) { - select.removeChild(select.firstChild); - } - }); - - [audioOutputSelect].forEach(select => { - while (select.firstChild) { - select.removeChild(select.firstChild); - } - }); - - var counter = 0; - for (let i = 0; i !== deviceInfos.length; ++i) { - const deviceInfo = deviceInfos[i]; - if (deviceInfo == null) { - continue; - } - - if (deviceInfo.kind === "audioinput") { - // Deduplicate by deviceId if possible (defensive against buggy drivers) - try { - if (deviceInfo.deviceId && addedDeviceIds.has(deviceInfo.deviceId)) { - log("Skipping duplicate audio device: " + deviceInfo.label); - continue; - } - if (deviceInfo.deviceId) { - addedDeviceIds.add(deviceInfo.deviceId); - } - } catch (e) { } - - var option = document.createElement("input"); - option.type = "checkbox"; - counter++; - var listele = document.createElement("li"); - listele.style.display = "none"; - - // Auto-check selection based on active track deviceId first, fall back to normalized label - try { - let shouldCheck = false; - const devIdLower = (deviceInfo.deviceId || "").toLowerCase(); - const normLabel = normalizeAudioAliasLabel(deviceInfo.label || ""); - if (activeAudioIds.size && deviceInfo.deviceId && activeAudioIds.has(deviceInfo.deviceId)) { - shouldCheck = true; - } else if (!activeAudioIds.size && deviceInfo.label && activeAudioNormLabels.has(normLabel)) { - // Prefer non-default entries when multiple share a label - const isDefaultish = (devIdLower === "default" || devIdLower === "communications"); - if (checkedByNormLabel.has(normLabel)) { - shouldCheck = false; - } else if (isDefaultish && nonDefaultNormLabelSet.has(normLabel)) { - shouldCheck = false; - } else { - shouldCheck = true; - } - } - if (shouldCheck) { - option.checked = true; - listele.style.display = "inherit"; - if (normLabel) { - checkedByNormLabel.add(normLabel); - } - } - } catch (e) { } - - option.style.display = "none"; - option.value = deviceInfo.deviceId || "default"; - option.name = "multiselecta" + counter; - option.id = "multiselecta" + counter; - option.dataset.label = deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1); - try { option.dataset.norm = normalizeAudioAliasLabel(option.dataset.label); } catch (e) { } - try { option.dataset.groupId = deviceInfo.groupId || ""; } catch (e) { } - - var label = document.createElement("label"); - label.for = option.name; - - label.innerHTML = " " + (deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1)); - - listele.appendChild(option); - listele.appendChild(label); - audioInputSelect.appendChild(listele); - - option.onchange = function (event) { - // make sure to clear 'no audio option' if anything else is selected - log("change 4768"); - if (!CtrlPressed) { - document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { - if (event.currentTarget.value !== item.value) { - item.checked = false; - if (item.dataset.type == "screen") { - item.parentElement.parentElement.removeChild(item.parentElement); - } - while (SelectedAudioInputDevices.indexOf(item.value) > -1) { - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); - } - } else { - item.checked = true; - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { - if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { - SelectedAudioInputDevices = []; - } - SelectedAudioInputDevices.push(event.currentTarget.value); - } - } - }); - } else { - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { - if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { - SelectedAudioInputDevices = []; - } - SelectedAudioInputDevices.push(event.currentTarget.value); - } - getById("audioSourceNoAudio2").checked = false; - } - saveSettings(); - }; - } else if (deviceInfo.kind === "videoinput") { - var option = document.createElement("option"); - option.value = deviceInfo.deviceId || "default"; - option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; - try { - if (!knownTrack && session.canvasSource) { - session.canvasSource.srcObject.getVideoTracks().forEach(function (track) { - if (option.text == track.label) { - option.selected = "true"; - knownTrack = true; - } - }); - } - if (!knownTrack && session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(function (track) { - if (option.text == track.label) { - option.selected = "true"; - knownTrack = true; - } - }); - } - } catch (e) { - errorlog(e); - } - videoSelect.appendChild(option); - } else if (deviceInfo.kind === "audiooutput") { - var option = document.createElement("option"); - if (audioOutputSelect.length === 0) { - option.dataset.default = true; - } else { - option.dataset.default = false; - } - option.value = deviceInfo.deviceId || "default"; - if (option.value == session.sink) { - option.selected = "true"; - } else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices == option.value) { - option.selected = "true"; - session.sink = option.value; // added 8-dec-22, as the director's saved mic wasn't applying otherwise. - } - option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; - audioOutputSelect.appendChild(option); - } else { - log("Some other kind of source/device: ", deviceInfo); - } - } - - if (Firefox && !session.mobile) { - var option = document.createElement("option"); - option.value = "others"; - option.text = getTranslation("show-more-options"); - audioOutputSelect.appendChild(option); - } - - if (audioOutputSelect.childNodes.length == 0) { - var option = document.createElement("option"); - option.value = "default"; - option.text = getTranslation("system-default"); - audioOutputSelect.appendChild(option); - } - - if (videoSelect.childNodes.length <= 1) { - getById("flipcamerabutton").style.display = "none"; // don't show the camera cycle button - getById("flipcamerabutton").dataset.maxndex = videoSelect.childNodes.length; - } else { - getById("flipcamerabutton").style.display = "unset"; - getById("flipcamerabutton").dataset.maxIndex = videoSelect.childNodes.length; - } - - //////////// - session.streamSrc.getAudioTracks().forEach(function (track) { - // add active ScreenShare audio tracks to the list - log("Checking for screenshare audio"); - var matched = false; - for (var i = 0; i !== deviceInfos.length; ++i) { - var deviceInfo = deviceInfos[i]; - if (deviceInfo == null) { - continue; - } - log("---"); - if (track.label == deviceInfo.label) { - matched = true; - continue; - } - } - if (matched == false) { - // Not a gUM device - var listele = document.createElement("li"); - listele.style.display = "block"; - var option = document.createElement("input"); - option.type = "checkbox"; - option.value = track.id; - option.checked = true; - option.style.display = "none"; - option.name = track.label; - option.label = track.label; - option.dataset.type = "screen"; - var label = document.createElement("label"); - label.for = option.name; - label.innerHTML = " " + track.label; - listele.appendChild(option); - listele.appendChild(label); - option.onchange = function (event) { - // make sure to clear 'no audio option' if anything else is selected - log("change 4873"); - var trackid = null; - if (!CtrlPressed) { - document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { - if (event.currentTarget.value !== item.value) { - // this shoulnd't happen, but if it does. - item.checked = false; - if (item.dataset.type == "screen") { - item.parentElement.parentElement.removeChild(item.parentElement); - } - } else { - event.currentTarget.checked = true; - trackid = item.value; - } - }); - } else { - //getById("audioSourceNoAudio2").checked=false; - if (event.currentTarget.dataset.type == "screen") { - event.currentTarget.parentElement.parentElement.removeChild(event.currentTarget.parentElement); - } - } - activatedPreview = false; - grabAudio("#audioSource3", trackid); // exclude item.id. - event.stopPropagation(); - return false; - }; - audioInputSelect.appendChild(listele); - } - }); - - /////////// no video option - var optionss = false; - if (screensharesupport) { - optionss = document.createElement("option"); - optionss.text = "Screen Share (replace camera)"; - optionss.value = "XXX"; - videoSelect.appendChild(optionss); // NO AUDIO OPTION - } - - var option = document.createElement("option"); // no video - option.text = getTranslation("disable-video"); - option.value = "ZZZ"; - videoSelect.appendChild(option); - - if (session.streamSrc.getVideoTracks().length == 0) { - option.selected = "true"; - } else if (knownTrack == false) { - var option = document.createElement("option"); // no video - option.text = session.streamSrc.getVideoTracks()[0].label; - option.value = "YYY"; - videoSelect.appendChild(option); - option.selected = "true"; - } - - if (optionss) { - optionss.lastSelected = videoSelect.selectedIndex; - } - - videoSelect.onchange = function (event) { - try { - if (event.target.options[event.target.options.selectedIndex].value === "XXX") { - videoSelect.selectedIndex = event.target.options[event.target.options.selectedIndex].lastSelected; - if (session.screenShareState == false) { - toggleScreenShare(); - } else { - toggleScreenShare(true); - } - return; - } - } catch (e) { } - activatedPreview = false; - grabVideo(session.quality, "videosource", "select#videoSource3"); - - if (!getById("audioSource3").querySelectorAll("input[data-type='screen']").length) { - if (session.screenShareState) { - session.screenShareState = false; - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - notifyOfScreenShare(); - //session.refreshScale(); - } - getById("screensharebutton").classList.remove("green"); - getById("screensharebutton").ariaPressed = "false"; - } - }; - - ///////////// /// NO AUDIO appended option - - var option = document.createElement("input"); - option.type = "checkbox"; - option.value = "ZZZ"; - option.style.display = "none"; - option.id = "audioSourceNoAudio2"; - - var label = document.createElement("label"); - label.for = option.name; - label.innerHTML = " No Audio"; - var listele = document.createElement("li"); - - if (session.streamSrc.getAudioTracks().length == 0) { - option.checked = true; - } else { - listele.style.display = "none"; - option.checked = false; - } - option.onchange = function (event) { - // make sure to clear 'no audio option' if anything else is selected - log("change 4938"); - if (!CtrlPressed) { - document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { - if (event.currentTarget.value !== item.value) { - item.checked = false; - if (item.dataset.type == "screen") { - item.parentElement.parentElement.removeChild(item.parentElement); - } - while (SelectedAudioInputDevices.indexOf(item.value) > -1) { - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); - } - } else { - item.checked = true; - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { - if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { - SelectedAudioInputDevices = []; - } - SelectedAudioInputDevices.push(event.currentTarget.value); - } - } - }); - } else { - document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { - if (event.currentTarget.value === item.value) { - event.currentTarget.checked = true; - if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { - if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { - SelectedAudioInputDevices = []; - } - SelectedAudioInputDevices.push(event.currentTarget.value); - } - } else { - item.checked = false; - if (item.dataset.type == "screen") { - item.parentElement.parentElement.removeChild(item.parentElement); - } - while (SelectedAudioInputDevices.indexOf(item.value) > -1) { - SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); - } - } - }); - } - saveSettings(); - }; - listele.appendChild(option); - listele.appendChild(label); - audioInputSelect.appendChild(listele); - - //////////// - - //selectors.forEach((select, selectorIndex) => { - // if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) { - // select.value = values[selectorIndex]; - // } - //}); - - audioInputSelect.onchange = function () { - log("Audio OPTION HAS CHANGED? 2"); - activatedPreview = false; - setTimeout(function () { - grabAudio("#audioSource3"); - }, 10); - }; - - getById("refreshVideoButton").onclick = function () { - refreshVideoDevice(); - }; - - if (Firefox && !session.mobile && navigator.mediaDevices) { - audioOutputSelect.onclick = function () { - log("audioOutputSelect.onclick = function() {"); - if (audioOutputSelect.options[audioOutputSelect.selectedIndex].value === "others") { - log("Trying to increase the output device list"); - navigator.mediaDevices.selectAudioOutput().then(device => { - if (device.kind == "audiooutput") { - session.sink = device.deviceId; - try { - var matched = false; - audioOutputSelect.childNodes.forEach(ele => { - if (ele.value === device.deviceId) { - matched = true; - ele.selected = true; - } - }); - if (!matched) { - var option = document.createElement("option"); - option.value = device.deviceId; - option.text = device.label; - audioOutputSelect.appendChild(option); - option.selected = true; - } - saveSettings(); // we're saving because there was an explicit action to change devices - } catch (e) { - errorlog(e); - } - if (!session.sink) { - return; - } // Not sure this would ever happen, but whatever. - resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist. - } - }); - } - }; - } else if (!navigator.mediaDevices) { - console.warn("No navigator.mediaDevices found - try a different browser or check your settings."); - } - - audioOutputSelect.onchange = function () { - log("audioOutputSelect.onchange = function() {"); - - if (iOS || iPad) { - return; - } - - if (Firefox && !session.mobile) { - if (audioOutputSelect.options[audioOutputSelect.selectedIndex].value === "others") { - // we handle this elsewhere - return; - } - } - - try { - session.sink = audioOutputSelect.options[audioOutputSelect.selectedIndex].value; - saveSettings(); - } catch (e) { - errorlog(e); - } - if (!session.sink) { - return; - } - - resetupAudioOut(); - - log("done audioOutputSelect.onchange = function() {"); - }; - } catch (e) { - errorlog(e); - } -} - -function refreshMicrophoneDevice(UUID = false) { - if (session.screenShareState || session.mediafileShare) { - log("can't refresh a screenshare or fileshare"); - - if (UUID) { - var data = {}; - data.UUID = UUID; - data.rejected = "can't refresh mic during screen or file share"; - session.sendMessage(data, data.UUID); - } - - return; - } - log("refreshing microphone.."); - activatedPreview = false; - grabAudio("#audioSource3", null, false, UUID); -} - -function refreshVideoDevice(UUID = false) { - if (session.screenShareState || session.mediafileShare) { - log("can't refresh video during screenshare or fileshare"); - - if (UUID) { - var data = {}; - data.UUID = UUID; - data.rejected = "can't refresh video during screen or file share"; - session.sendMessage(data, data.UUID); - } - - return; - } - log("refreshing video device.."); - activatedPreview = false; - grabVideo(session.quality, "videosource", "select#videoSource3"); -} - -function directRefreshVideo(ele) { - var UUID = ele.dataset.UUID; - if (!UUID) { return; } - - var data = {}; - data.refreshVideo = true; - data.UUID = UUID; - if (session.sendRequest(data, UUID)) { - ele.classList.add("pressed"); - setTimeout((ele) => { - if (ele) { - ele.classList.remove("pressed"); - } - }, 400, ele); - } -} - -function directRefreshConnection(ele) { - var UUID = ele.dataset.UUID; - if (!UUID) { return; } - - var data = {}; - data.refreshConnection = true; - data.UUID = UUID; - if (session.sendRequest(data, UUID)) { - ele.classList.add("pressed"); - setTimeout((ele) => { - if (ele) { - ele.classList.remove("pressed"); - } - }, 400, ele); - } -} - -function directReconnectPeer(guestUUID, peerUUID) { - // Tell a specific guest to reconnect to a specific peer - var data = {}; - data.reconnectPeer = peerUUID; - data.UUID = guestUUID; - session.sendRequest(data, guestUUID); - log("Sent reconnectPeer command to " + guestUUID + " for peer " + peerUUID); -} - -// Mesh diagram action wrappers -function meshRefreshVideo(uuid) { - var data = {}; - data.refreshVideo = true; - data.UUID = uuid; - session.sendRequest(data, uuid); - log("Sent refreshVideo to " + uuid); -} - -function meshRefreshConnection(uuid) { - var data = {}; - data.refreshConnection = true; - data.UUID = uuid; - session.sendRequest(data, uuid); - log("Sent refreshConnection (ICE restart) to " + uuid); -} - -function meshRefreshAll(uuid) { - var data = {}; - data.refreshAll = true; - data.UUID = uuid; - session.sendRequest(data, uuid); - log("Sent refreshAll to " + uuid); -} - -function meshRefreshMic(uuid) { - var data = {}; - data.refreshMicrophone = true; - data.UUID = uuid; - session.sendRequest(data, uuid); - log("Sent refreshMicrophone to " + uuid); -} - -function meshRestartWhip(uuid) { - var data = {}; - data.restartWhip = true; - data.UUID = uuid; - session.sendRequest(data, uuid); - log("Sent restartWhip to " + uuid); -} - -// ============================================ -// MESH NETWORK VISUALIZATION -// ============================================ - -var meshData = { - nodes: {}, // uuid -> node info - edges: [], // connection info between nodes - pendingResponses: 0, - lastRefresh: 0, - modalOpen: false, - patchedConnections: {}, // "uuidA-uuidB" -> {prevStateA: bool, prevStateB: bool} for connections being relayed via mix-minus - whipStatus: null, // {state, url, connected, reconnectAttempts} - WHIP outbound status - whepConnections: {} // uuid -> {state, connected} - WHEP inbound connections -}; - -// Patch a failed P2P connection via mix-minus relay -// Director becomes the audio bridge between two guests -function patchConnectionViaMixMinus(uuidA, uuidB) { - if (!session.mixMinusState) { - session.mixMinusState = {}; - } - - // Initialize mix-minus state for both guests if needed - if (!session.mixMinusState[uuidA]) { - initMixMinusStateForGuest(uuidA); - } - if (!session.mixMinusState[uuidB]) { - initMixMinusStateForGuest(uuidB); - } - - // Record previous state before modifying (for proper restore on unpatch) - var wasAExcludedFromB = session.mixMinusState[uuidB].excludeSources.includes(uuidA); - var wasBExcludedFromA = session.mixMinusState[uuidA].excludeSources.includes(uuidB); - var wasAEnabled = session.mixMinusState[uuidA].enabled || false; - var wasBEnabled = session.mixMinusState[uuidB].enabled || false; - - // Enable mix-minus for both guests (required for patching to work) - session.mixMinusState[uuidA].enabled = true; - session.mixMinusState[uuidB].enabled = true; - - // Remove A from B's excludeSources (so B hears A via director) - var idxAinB = session.mixMinusState[uuidB].excludeSources.indexOf(uuidA); - if (idxAinB > -1) { - session.mixMinusState[uuidB].excludeSources.splice(idxAinB, 1); - } - - // Remove B from A's excludeSources (so A hears B via director) - var idxBinA = session.mixMinusState[uuidA].excludeSources.indexOf(uuidB); - if (idxBinA > -1) { - session.mixMinusState[uuidA].excludeSources.splice(idxBinA, 1); - } - - // Update the mixes - updateMixMinusForGuest(uuidA); - updateMixMinusForGuest(uuidB); - - // Track this patched connection with previous state for proper restore - // Use sorted order so we can correctly restore regardless of call order - var sorted = [uuidA, uuidB].sort(); - var patchKey = sorted.join("-"); - meshData.patchedConnections[patchKey] = { - // Store as: was sorted[0] excluded from sorted[1]'s mix, and vice versa - wasFirstExcludedFromSecond: sorted[0] === uuidA ? wasAExcludedFromB : wasBExcludedFromA, - wasSecondExcludedFromFirst: sorted[0] === uuidA ? wasBExcludedFromA : wasAExcludedFromB, - // Store enabled state for both guests - wasFirstEnabled: sorted[0] === uuidA ? wasAEnabled : wasBEnabled, - wasSecondEnabled: sorted[0] === uuidA ? wasBEnabled : wasAEnabled - }; - - log("Patched connection via mix-minus: " + uuidA + " <-> " + uuidB); -} - -// Unpatch a connection (when P2P recovers or manually) -function unpatchConnection(uuidA, uuidB) { - if (!session.mixMinusState) return; - - var sorted = [uuidA, uuidB].sort(); - var patchKey = sorted.join("-"); - var savedState = meshData.patchedConnections[patchKey]; - - // Restore previous exclude state (only add back if they were excluded before patching) - // sorted[0] = first UUID alphabetically, sorted[1] = second - var firstUUID = sorted[0]; - var secondUUID = sorted[1]; - - if (session.mixMinusState[secondUUID] && savedState && savedState.wasFirstExcludedFromSecond) { - // First was excluded from second's mix before - restore that - if (!session.mixMinusState[secondUUID].excludeSources.includes(firstUUID)) { - session.mixMinusState[secondUUID].excludeSources.push(firstUUID); - } - updateMixMinusForGuest(secondUUID); - } - - if (session.mixMinusState[firstUUID] && savedState && savedState.wasSecondExcludedFromFirst) { - // Second was excluded from first's mix before - restore that - if (!session.mixMinusState[firstUUID].excludeSources.includes(secondUUID)) { - session.mixMinusState[firstUUID].excludeSources.push(secondUUID); - } - updateMixMinusForGuest(firstUUID); - } - - // Restore previous enabled state for both guests - if (savedState) { - if (session.mixMinusState[firstUUID]) { - session.mixMinusState[firstUUID].enabled = savedState.wasFirstEnabled; - updateMixMinusForGuest(firstUUID); - } - if (session.mixMinusState[secondUUID]) { - session.mixMinusState[secondUUID].enabled = savedState.wasSecondEnabled; - updateMixMinusForGuest(secondUUID); - } - } - - // Remove from patched tracking - delete meshData.patchedConnections[patchKey]; - - log("Unpatched connection: " + uuidA + " <-> " + uuidB); -} - -// Auto-patch all failed connections in the mesh -function autoPatchAllFailed() { - var patchCount = 0; - meshData.edges.forEach(function(edge) { - if (edge.state === "failed" || edge.state === "disconnected") { - // Skip edges involving director or viewers - patching only makes sense for guest↔guest - var sourceNode = meshData.nodes[edge.source]; - var targetNode = meshData.nodes[edge.target]; - if (sourceNode && (sourceNode.isDirector || sourceNode.isViewer)) return; - if (targetNode && (targetNode.isDirector || targetNode.isViewer)) return; - - var patchKey = [edge.source, edge.target].sort().join("-"); - if (!meshData.patchedConnections[patchKey]) { - patchConnectionViaMixMinus(edge.source, edge.target); - patchCount++; - } - } - }); - log("Auto-patched " + patchCount + " failed connections via mix-minus"); - if (meshData.modalOpen) { - renderMeshVisualization(); - } - return patchCount; -} - -// Auto-unpatch connections that have recovered -function autoUnpatchRecovered() { - var unpatchCount = 0; - // Collect keys to unpatch first (avoid modifying while iterating) - var toUnpatch = []; - for (var patchKey in meshData.patchedConnections) { - // Find the corresponding edge - var edge = meshData.edges.find(function(e) { return e.id === patchKey; }); - if (edge && edge.state === "connected") { - toUnpatch.push(patchKey); - } - } - // Now unpatch collected connections - toUnpatch.forEach(function(patchKey) { - var uuids = patchKey.split("-"); - unpatchConnection(uuids[0], uuids[1]); - unpatchCount++; - }); - if (unpatchCount > 0) { - log("Auto-unpatched " + unpatchCount + " recovered connections"); - if (meshData.modalOpen) { - renderMeshVisualization(); - } - } - return unpatchCount; -} - -// Check if a connection is currently patched -function isConnectionPatched(uuidA, uuidB) { - var patchKey = [uuidA, uuidB].sort().join("-"); - return !!meshData.patchedConnections[patchKey]; -} - -// UI wrapper for patching from mesh diagram -function meshPatchConnection(uuidA, uuidB) { - patchConnectionViaMixMinus(uuidA, uuidB); - // Refresh the edge details panel - var edgeId = [uuidA, uuidB].sort().join("-"); - var edge = meshData.edges.find(function(e) { return e.id === edgeId; }); - if (edge) { - showEdgeDetails(edge); - } - renderMeshVisualization(); -} - -// UI wrapper for unpatching from mesh diagram -function meshUnpatchConnection(uuidA, uuidB) { - unpatchConnection(uuidA, uuidB); - // Refresh the edge details panel - var edgeId = [uuidA, uuidB].sort().join("-"); - var edge = meshData.edges.find(function(e) { return e.id === edgeId; }); - if (edge) { - showEdgeDetails(edge); - } - renderMeshVisualization(); -} - -function requestMeshData() { - // Request connection maps from all connected guests - meshData.nodes = {}; - meshData.edges = []; - meshData.pendingResponses = 0; - - // Collect WHIP outbound status - meshData.whipStatus = null; - if (session.whipOut) { - meshData.whipStatus = { - state: session.whipOut.connectionState || session.whipOut.iceConnectionState || "unknown", - url: session.whipOutput || "", - connected: session.whipOut.connectionState === 'connected' || - session.whipOut.iceConnectionState === 'connected' || - session.whipOut.iceConnectionState === 'completed', - reconnectAttempts: session.getWhipReconnectAttempts ? session.getWhipReconnectAttempts() : 0 - }; - } - - // Collect WHEP inbound connection statuses - meshData.whepConnections = {}; - for (var uuid in session.rpcs) { - if (session.rpcs[uuid] && session.rpcs[uuid].whep) { - meshData.whepConnections[uuid] = { - state: session.rpcs[uuid].whep.connectionState || session.rpcs[uuid].whep.iceConnectionState || "unknown", - connected: session.rpcs[uuid].whep.connectionState === 'connected' || - session.rpcs[uuid].whep.iceConnectionState === 'connected' || - session.rpcs[uuid].whep.iceConnectionState === 'completed' - }; - } - } - - // Add director as a node - include its connections from rpcs and pcs - var directorConnections = []; - - // Director's rpcs = guests publishing TO director = director receives from them - for (var uuid in session.rpcs) { - if (session.rpcs[uuid]) { - var rpc = session.rpcs[uuid]; - directorConnections.push({ - peerUUID: uuid, - peerStreamID: rpc.streamID || uuid, - direction: "incoming", // Director receives from guest - state: rpc.connectionState || "connected", - bandwidth: rpc.bandwidth || -1, - audioEnabled: true, - videoEnabled: true - }); - } - } - - // Director's pcs = director publishing TO peers (data channels, etc.) - for (var uuid in session.pcs) { - if (session.pcs[uuid]) { - var pc = session.pcs[uuid]; - directorConnections.push({ - peerUUID: uuid, - peerStreamID: pc.streamID || uuid, - direction: "outgoing", // Director sends to guest/scene - state: pc.connectionState || "connected", - bandwidth: -1, - audioEnabled: true, - videoEnabled: true - }); - } - } - - meshData.nodes[session.UUID] = { - uuid: session.UUID, - streamID: session.streamID, - label: "Director", - isDirector: true, - connections: directorConnections, - health: "healthy" - }; - - // Request from all rpcs (guests we're receiving from) - for (var uuid in session.rpcs) { - if (session.rpcs[uuid]) { - var data = { getConnectionMap: true, UUID: uuid }; - session.sendRequest(data, uuid); - meshData.pendingResponses++; - - // Add node placeholder - meshData.nodes[uuid] = { - uuid: uuid, - streamID: session.rpcs[uuid].streamID || uuid, - label: session.rpcs[uuid].label || session.rpcs[uuid].streamID || "Guest", - isDirector: false, - connections: [], - health: "pending" - }; - } - } - - log("Requested mesh data from " + meshData.pendingResponses + " guests"); - - // Set timeout to process after responses come in - setTimeout(function() { - aggregateMeshData(); - renderMeshVisualization(); - }, 2000); -} - -function handleConnectionMapResponse(msg, UUID) { - // Called when a guest responds with their connection map - // UUID = the key from director's rpcs (how director identifies this guest) - // msg.connectionMap.uuid = guest's session.UUID (might differ!) - if (msg.connectionMap) { - var map = msg.connectionMap; - - // Use the UUID parameter (director's key) for node matching, not map.uuid - // This ensures we update the correct placeholder node - var nodeKey = UUID; - - // Store the director's external UUID (as known by this guest) - // This lets us map guest connections to director correctly - if (map.requesterUUID) { - meshData.directorExternalUUID = map.requesterUUID; - } - - // Check if any connections use TURN (relay) - var usingTurn = false; - if (map.connections) { - for (var i = 0; i < map.connections.length; i++) { - if (map.connections[i].candidateType === "relay") { - usingTurn = true; - break; - } - } - } - - // Update node info using director's UUID key - if (meshData.nodes[nodeKey]) { - meshData.nodes[nodeKey].streamID = map.streamID; - meshData.nodes[nodeKey].label = map.label; - meshData.nodes[nodeKey].guestUUID = map.uuid; // Store guest's self-reported UUID - meshData.nodes[nodeKey].connections = map.connections; - meshData.nodes[nodeKey].browser = map.browser || "Unknown"; - meshData.nodes[nodeKey].usingTurn = usingTurn; - } else { - meshData.nodes[nodeKey] = { - uuid: nodeKey, - guestUUID: map.uuid, - streamID: map.streamID, - label: map.label, - isDirector: false, - connections: map.connections, - browser: map.browser || "Unknown", - usingTurn: usingTurn, - health: "healthy" - }; - } - - meshData.pendingResponses--; - log("Received connection map from " + map.label + " (UUID: " + nodeKey + ", " + map.connections.length + " connections)"); - - // Re-render whenever a response arrives and modal is open - // This handles late responses even if some peers never respond - if (meshData.modalOpen) { - aggregateMeshData(); - renderMeshVisualization(); - } - } -} - -function aggregateMeshData() { - // Build edges from all node connections - meshData.edges = []; - var edgeMap = {}; // edgeId -> edge object (to track bidirectionality) - - // Build a lookup from streamID to node UUID for edge matching - var streamIdToUuid = {}; - var directorNodeKey = null; - for (var uuid in meshData.nodes) { - var node = meshData.nodes[uuid]; - if (node.streamID) { - streamIdToUuid[node.streamID] = uuid; - } - if (node.isDirector) { - directorNodeKey = uuid; - } - } - - for (var uuid in meshData.nodes) { - var node = meshData.nodes[uuid]; - var failedCount = 0; - var degradedCount = 0; - - if (node.connections) { - for (var i = 0; i < node.connections.length; i++) { - var conn = node.connections[i]; - - // Try to resolve peer by streamID first (more reliable), then UUID - var peerNodeUuid = conn.peerUUID; - if (conn.peerStreamID && streamIdToUuid[conn.peerStreamID]) { - peerNodeUuid = streamIdToUuid[conn.peerStreamID]; - } - - // If peer doesn't exist as a node and this is an outgoing connection, - // check if it's actually the director (using directorExternalUUID) - if (!meshData.nodes[peerNodeUuid] && conn.direction === "outgoing") { - // Check if this is a connection to the director - if (meshData.directorExternalUUID && conn.peerUUID === meshData.directorExternalUUID) { - // Map to director node instead of creating phantom - peerNodeUuid = directorNodeKey; - } else { - // Create viewer/scene node for other unknown peers - meshData.nodes[peerNodeUuid] = { - uuid: peerNodeUuid, - streamID: conn.peerStreamID || peerNodeUuid, - label: conn.peerStreamID || "Viewer", - isDirector: false, - isViewer: true, - connections: [], - health: "healthy" - }; - if (conn.peerStreamID) { - streamIdToUuid[conn.peerStreamID] = peerNodeUuid; - } - } - } - - // Create unique edge ID (sorted UUIDs for deduplication) - var edgeId = [uuid, peerNodeUuid].sort().join("-"); - - // Track direction: outgoing = publishing TO peer, incoming = receiving FROM peer - var directionKey = conn.direction === "outgoing" ? "hasOutgoing" : "hasIncoming"; - - if (!edgeMap[edgeId]) { - edgeMap[edgeId] = { - id: edgeId, - source: uuid, - target: peerNodeUuid, - sourceStreamID: node.streamID, - targetStreamID: conn.peerStreamID, - state: conn.state, - bandwidth: conn.bandwidth, - candidateType: conn.candidateType, - nackCount: conn.nackCount, - pliCount: conn.pliCount, - hasOutgoing: false, - hasIncoming: false, - bidirectional: false - }; - } - - // Mark this direction as present - edgeMap[edgeId][directionKey] = true; - - // Update bidirectional flag - if (edgeMap[edgeId].hasOutgoing && edgeMap[edgeId].hasIncoming) { - edgeMap[edgeId].bidirectional = true; - } - - // Merge states - keep the worse one - var stateRank = { "failed": 0, "disconnected": 1, "new": 1, "connecting": 1, "closed": 1, "connected": 2 }; - var existingRank = stateRank[edgeMap[edgeId].state] !== undefined ? stateRank[edgeMap[edgeId].state] : 1; - var newRank = stateRank[conn.state] !== undefined ? stateRank[conn.state] : 1; - if (newRank < existingRank) { - edgeMap[edgeId].state = conn.state; - } - - // Track health based on RTCPeerConnection.connectionState - if (conn.state === "failed") { - failedCount++; - } else if (conn.state === "disconnected" || conn.state === "new" || conn.state === "connecting" || conn.state === "closed") { - degradedCount++; - } - } - } - - // Update node health - if (failedCount > 0) { - node.health = "failed"; - } else if (degradedCount > 0) { - node.health = "degraded"; - } else if (node.connections && node.connections.length > 0) { - node.health = "healthy"; - } else if (node.isViewer || node.isDirector) { - // Viewers/scenes and director don't report connections, so no connections is expected - node.health = "healthy"; - } else { - node.health = "isolated"; - } - } - - // Convert edgeMap to array - meshData.edges = Object.values(edgeMap); - - // Calculate summary stats - var totalConnections = meshData.edges.length; - var failedConnections = meshData.edges.filter(e => e.state === "failed").length; - var healthyConnections = meshData.edges.filter(e => e.state === "connected").length; - var bidirectionalCount = meshData.edges.filter(e => e.bidirectional).length; - var onewayCount = totalConnections - bidirectionalCount; - - var viewerCount = Object.values(meshData.nodes).filter(n => n.isViewer).length; - - meshData.summary = { - totalNodes: Object.keys(meshData.nodes).length, - viewerNodes: viewerCount, - totalConnections: totalConnections, - bidirectionalConnections: bidirectionalCount, - onewayConnections: onewayCount, - healthyConnections: healthyConnections, - failedConnections: failedConnections, - degradedConnections: totalConnections - healthyConnections - failedConnections - }; - - meshData.lastRefresh = Date.now(); - log("Aggregated mesh data: " + meshData.summary.totalNodes + " nodes, " + meshData.summary.totalConnections + " connections"); -} - -function getMeshHealthBadge() { - // Returns HTML for the health badge to show in director controls - var s = meshData.summary; - if (!s) return ""; - - var color = "#4CAF50"; // green - var text = s.healthyConnections + "/" + s.totalConnections; - - if (s.failedConnections > 0) { - color = "#F44336"; // red - text = s.failedConnections + " failed"; - } else if (s.degradedConnections > 0) { - color = "#FF9800"; // orange - } - - return '' + text + ''; -} - -function openMeshVisualization() { - if (meshData.modalOpen) return; - meshData.modalOpen = true; - - // Create modal overlay - var modal = document.createElement("div"); - modal.id = "meshModal"; - modal.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:10000;display:flex;flex-direction:column;"; - - // Toolbar - var toolbar = document.createElement("div"); - toolbar.style.cssText = "padding:10px 20px;background:#222;display:flex;align-items:center;gap:10px;border-bottom:1px solid #444;flex-wrap:wrap;"; - toolbar.innerHTML = ` -

    Mesh Network Debug

    - - - - - - - `; - - // SVG container - var svgContainer = document.createElement("div"); - svgContainer.id = "meshSvgContainer"; - svgContainer.style.cssText = "flex:1;overflow:hidden;position:relative;"; - - // Detail panel (hidden by default) - var detailPanel = document.createElement("div"); - detailPanel.id = "meshDetailPanel"; - detailPanel.style.cssText = "position:absolute;right:0;top:0;width:300px;height:100%;background:#1a1a1a;border-left:1px solid #444;padding:20px;display:none;overflow-y:auto;color:#fff;"; - - svgContainer.appendChild(detailPanel); - modal.appendChild(toolbar); - modal.appendChild(svgContainer); - document.body.appendChild(modal); - - // Initialize layout button text to match current mode - var layoutBtn = document.getElementById("meshLayoutBtn"); - if (layoutBtn) { - layoutBtn.textContent = "Layout: " + meshLayoutMode.charAt(0).toUpperCase() + meshLayoutMode.slice(1); - } - - // Add keyboard shortcuts - document.addEventListener("keydown", meshKeyHandler); - - // Request fresh data and render - requestMeshData(); -} - -function closeMeshVisualization() { - var modal = document.getElementById("meshModal"); - if (modal) { - modal.remove(); - } - meshData.modalOpen = false; - document.removeEventListener("keydown", meshKeyHandler); -} - -function meshKeyHandler(e) { - if (!meshData.modalOpen) return; - - switch(e.key) { - case "Escape": - closeMeshVisualization(); - break; - case "r": - case "R": - requestMeshData(); - break; - case "f": - case "F": - var cb = document.getElementById("meshFilterProblems"); - if (cb) cb.checked = !cb.checked; - renderMeshVisualization(); - break; - } -} - -var meshLayoutMode = "circular"; // circular, grid, force - -function cycleMeshLayout() { - if (meshLayoutMode === "force") { - meshLayoutMode = "circular"; - } else if (meshLayoutMode === "circular") { - meshLayoutMode = "grid"; - } else { - meshLayoutMode = "force"; - } - - var btn = document.getElementById("meshLayoutBtn"); - if (btn) { - btn.textContent = "Layout: " + meshLayoutMode.charAt(0).toUpperCase() + meshLayoutMode.slice(1); - } - - renderMeshVisualization(); -} - -function renderMeshVisualization() { - var container = document.getElementById("meshSvgContainer"); - if (!container) return; - - var existingSvg = container.querySelector("svg"); - if (existingSvg) existingSvg.remove(); - - var width = container.clientWidth; - var height = container.clientHeight - 50; // Leave room for status bar - - // Create SVG - var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("width", width); - svg.setAttribute("height", height); - svg.style.display = "block"; - - // Filter problems only? - var filterProblems = document.getElementById("meshFilterProblems")?.checked || false; - - // Calculate node positions based on layout mode - var nodePositions = {}; - var nodeArray = Object.values(meshData.nodes); - var filteredNodes = filterProblems ? nodeArray.filter(n => n.health !== "healthy") : nodeArray; - - if (filteredNodes.length === 0 && filterProblems) { - // Show message - var text = document.createElementNS("http://www.w3.org/2000/svg", "text"); - text.setAttribute("x", width / 2); - text.setAttribute("y", height / 2); - text.setAttribute("fill", "#4CAF50"); - text.setAttribute("text-anchor", "middle"); - text.setAttribute("font-size", "24"); - text.textContent = "All connections healthy!"; - svg.appendChild(text); - container.insertBefore(svg, container.firstChild); - return; - } - - // Calculate positions - var centerX = width / 2; - var centerY = height / 2; - var radius = Math.min(width, height) / 2 - 100; - - if (meshLayoutMode === "circular") { - // Director in center, guests in a ring - var guestNodes = filteredNodes.filter(n => !n.isDirector); - var directorNode = filteredNodes.find(n => n.isDirector); - - // Place director in center - if (directorNode) { - nodePositions[directorNode.uuid] = { x: centerX, y: centerY }; - } - - // Place guests in a ring around director - guestNodes.forEach(function(node, i) { - var angle = (2 * Math.PI * i) / guestNodes.length - Math.PI / 2; - nodePositions[node.uuid] = { - x: centerX + radius * Math.cos(angle), - y: centerY + radius * Math.sin(angle) - }; - }); - } else if (meshLayoutMode === "grid") { - var cols = Math.ceil(Math.sqrt(filteredNodes.length)); - var spacing = Math.min(width, height) / (cols + 1); - filteredNodes.forEach(function(node, i) { - var col = i % cols; - var row = Math.floor(i / cols); - nodePositions[node.uuid] = { - x: spacing + col * spacing, - y: spacing + row * spacing - }; - }); - } else { - // Force-directed: simple random for now - filteredNodes.forEach(function(node) { - nodePositions[node.uuid] = { - x: 100 + Math.random() * (width - 200), - y: 100 + Math.random() * (height - 200) - }; - }); - } - - // Add arrow marker definitions for one-way connections - var defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); - - // Arrow markers for different colors - ["#4CAF50", "#FF9800", "#F44336"].forEach(function(color, idx) { - var marker = document.createElementNS("http://www.w3.org/2000/svg", "marker"); - marker.setAttribute("id", "arrow-" + idx); - marker.setAttribute("markerWidth", "10"); - marker.setAttribute("markerHeight", "10"); - marker.setAttribute("refX", "35"); - marker.setAttribute("refY", "3"); - marker.setAttribute("orient", "auto"); - marker.setAttribute("markerUnits", "strokeWidth"); - - var path = document.createElementNS("http://www.w3.org/2000/svg", "path"); - path.setAttribute("d", "M0,0 L0,6 L9,3 z"); - path.setAttribute("fill", color); - marker.appendChild(path); - defs.appendChild(marker); - }); - svg.appendChild(defs); - - // Draw edges first (so they appear behind nodes) - meshData.edges.forEach(function(edge) { - var sourcePos = nodePositions[edge.source]; - var targetPos = nodePositions[edge.target]; - - if (!sourcePos || !targetPos) return; - - // Filter if needed - if (filterProblems && edge.state === "connected") return; - - // For one-way connections, determine correct arrow direction - // hasOutgoing means source publishes TO target (arrow: source→target) - // hasIncoming only means target publishes TO source (arrow: target→source) - var drawFromPos = sourcePos; - var drawToPos = targetPos; - if (!edge.bidirectional && edge.hasIncoming && !edge.hasOutgoing) { - // Swap direction - target is actually the publisher - drawFromPos = targetPos; - drawToPos = sourcePos; - } - - var line = document.createElementNS("http://www.w3.org/2000/svg", "line"); - line.setAttribute("x1", drawFromPos.x); - line.setAttribute("y1", drawFromPos.y); - line.setAttribute("x2", drawToPos.x); - line.setAttribute("y2", drawToPos.y); - - // Check if this edge is patched via mix-minus - var isPatched = isConnectionPatched(edge.source, edge.target); - - // Style based on state - var strokeColor = "#4CAF50"; // green - var strokeWidth = 2; - var dashArray = ""; - var arrowIdx = 0; - - if (edge.state === "failed") { - strokeColor = "#F44336"; - strokeWidth = 3; - dashArray = "5,5"; - arrowIdx = 2; - } else if (edge.state === "disconnected" || edge.state === "new" || edge.state === "connecting" || edge.state === "closed") { - strokeColor = "#FF9800"; - dashArray = "10,5"; - arrowIdx = 1; - } - - // Override style for patched connections - show as cyan with double-dash - if (isPatched) { - strokeColor = "#00BCD4"; // cyan - matches patch button - strokeWidth = 3; - dashArray = "8,3,2,3"; // distinctive double-dash pattern - } - - line.setAttribute("stroke", strokeColor); - line.setAttribute("stroke-width", strokeWidth); - if (dashArray) line.setAttribute("stroke-dasharray", dashArray); - line.setAttribute("data-edge-id", edge.id); - line.style.cursor = "pointer"; - - // Add arrow for one-way connections (not bidirectional) - if (!edge.bidirectional) { - line.setAttribute("marker-end", "url(#arrow-" + arrowIdx + ")"); - } - - // Click handler for edge - line.onclick = function() { - showEdgeDetails(edge); - }; - - // Hover effect - line.onmouseenter = function() { - this.setAttribute("stroke-width", parseInt(strokeWidth) + 2); - }; - line.onmouseleave = function() { - this.setAttribute("stroke-width", strokeWidth); - }; - - svg.appendChild(line); - }); - - // Draw nodes - filteredNodes.forEach(function(node) { - var pos = nodePositions[node.uuid]; - if (!pos) return; - - var g = document.createElementNS("http://www.w3.org/2000/svg", "g"); - g.setAttribute("transform", "translate(" + pos.x + "," + pos.y + ")"); - g.style.cursor = "pointer"; - - // Node shape - circle for publishers, square for viewers/scenes - var shape; - if (node.isViewer) { - // Square for viewers/scenes - shape = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - shape.setAttribute("x", -25); - shape.setAttribute("y", -25); - shape.setAttribute("width", 50); - shape.setAttribute("height", 50); - shape.setAttribute("rx", 5); - shape.setAttribute("fill", "#222"); - } else { - // Circle for publishers - shape = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - shape.setAttribute("r", 30); - shape.setAttribute("fill", "#333"); - } - - // Border color based on health/type - var borderColor = "#4CAF50"; - if (node.health === "failed") borderColor = "#F44336"; - else if (node.health === "degraded") borderColor = "#FF9800"; - else if (node.health === "isolated") borderColor = "#9E9E9E"; - else if (node.isDirector) borderColor = "#2196F3"; - else if (node.isViewer) borderColor = "#9C27B0"; // Purple for viewers/scenes - - shape.setAttribute("stroke", borderColor); - shape.setAttribute("stroke-width", 3); - - // Label - var text = document.createElementNS("http://www.w3.org/2000/svg", "text"); - text.setAttribute("text-anchor", "middle"); - text.setAttribute("dy", 5); - text.setAttribute("fill", "#fff"); - text.setAttribute("font-size", "12"); - text.textContent = (node.label || "?").substring(0, 8); - - // Badge for special node types - if (node.isDirector) { - var badge = document.createElementNS("http://www.w3.org/2000/svg", "text"); - badge.setAttribute("text-anchor", "middle"); - badge.setAttribute("y", -35); - badge.setAttribute("fill", "#2196F3"); - badge.setAttribute("font-size", "10"); - badge.textContent = "DIRECTOR"; - g.appendChild(badge); - } else if (node.isViewer) { - var badge = document.createElementNS("http://www.w3.org/2000/svg", "text"); - badge.setAttribute("text-anchor", "middle"); - badge.setAttribute("y", -30); - badge.setAttribute("fill", "#9C27B0"); - badge.setAttribute("font-size", "10"); - badge.textContent = "SCENE/VIEW"; - g.appendChild(badge); - } - - g.appendChild(shape); - g.appendChild(text); - - // Click handler - g.onclick = function() { - showNodeDetails(node); - }; - - svg.appendChild(g); - }); - - container.insertBefore(svg, container.firstChild); - - // Add status bar - var statusBar = container.querySelector(".meshStatusBar"); - if (!statusBar) { - statusBar = document.createElement("div"); - statusBar.className = "meshStatusBar"; - statusBar.style.cssText = "position:absolute;bottom:0;left:0;right:0;padding:10px 20px;background:#222;color:#fff;font-size:14px;"; - container.appendChild(statusBar); - } - - var s = meshData.summary || {}; - var nodeInfo = s.totalNodes || 0; - if (s.viewerNodes > 0) { - nodeInfo += " (" + s.viewerNodes + " scenes/viewers)"; - } - var connInfo = ""; - if (s.bidirectionalConnections > 0) { - connInfo += s.bidirectionalConnections + " bidirectional"; - } - if (s.onewayConnections > 0) { - if (connInfo) connInfo += ", "; - connInfo += s.onewayConnections + " one-way→"; - } - - // Build WHIP/WHEP status display - var whipWhepStatus = ""; - if (meshData.whipStatus) { - var whipColor = meshData.whipStatus.connected ? "#4CAF50" : "#F44336"; - var whipIcon = meshData.whipStatus.connected ? "la-broadcast-tower" : "la-exclamation-triangle"; - whipWhepStatus += ''; - whipWhepStatus += ' WHIP: ' + meshData.whipStatus.state; - if (!meshData.whipStatus.connected) { - whipWhepStatus += ' '; - } - if (meshData.whipStatus.reconnectAttempts > 0) { - whipWhepStatus += ' (' + meshData.whipStatus.reconnectAttempts + ' retries)'; - } - whipWhepStatus += ''; - } - - // Count WHEP connection issues - var whepTotal = Object.keys(meshData.whepConnections).length; - var whepFailed = Object.values(meshData.whepConnections).filter(function(w) { return !w.connected; }).length; - if (whepTotal > 0) { - var whepColor = whepFailed === 0 ? "#4CAF50" : "#F44336"; - whipWhepStatus += ''; - whipWhepStatus += ' WHEP: ' + (whepTotal - whepFailed) + '/' + whepTotal + ' connected'; - whipWhepStatus += ''; - } - - statusBar.innerHTML = ` - ${s.healthyConnections || 0} healthy | - ${s.degradedConnections || 0} degraded | - ${s.failedConnections || 0} failed | - ${nodeInfo} nodes | ${connInfo || "0 connections"} - ${whipWhepStatus} - Last refresh: ${meshData.lastRefresh ? new Date(meshData.lastRefresh).toLocaleTimeString() : "Never"} - `; -} - -function showNodeDetails(node) { - var panel = document.getElementById("meshDetailPanel"); - if (!panel) return; - - panel.style.display = "block"; - - var connections = node.connections || []; - var connHtml = connections.map(function(c) { - var stateColor = c.state === "connected" ? "#4CAF50" : c.state === "failed" ? "#F44336" : "#FF9800"; - var turnBadge = c.candidateType === "relay" ? ' TURN' : ''; - return `
    - ${c.peerStreamID || c.peerUUID.substring(0,8)}${turnBadge} - ${c.state} - ${c.bandwidth > 0 ? '
    ' + c.bandwidth + ' kbps' : ''} -
    `; - }).join(""); - - // Action buttons for guest nodes (not director) - recovery focused - var actionButtons = ''; - if (!node.isDirector && !node.isViewer) { - actionButtons = ` -

    Recovery Actions

    -
    - - -
    - - - - `; - } - - // Build browser/TURN info line - var infoLine = ''; - if (node.browser && node.browser !== "Unknown") { - infoLine += '' + node.browser + ''; - } - if (node.usingTurn) { - infoLine += 'TURN'; - } - // Add WHEP status if this node has a WHEP inbound connection - if (meshData.whepConnections && meshData.whepConnections[node.uuid]) { - var whepInfo = meshData.whepConnections[node.uuid]; - var whepColor = whepInfo.connected ? "#4CAF50" : "#F44336"; - var whepBg = whepInfo.connected ? "#1B5E20" : "#B71C1C"; - infoLine += 'WHEP: ' + whepInfo.state + ''; - } - - panel.innerHTML = ` -

    ${node.label}

    -

    UUID: ${node.uuid.substring(0, 12)}...

    -

    Stream ID: ${node.streamID}

    - ${infoLine ? '

    ' + infoLine + '

    ' : ''} -

    Health: ${node.health}

    -

    Connections (${connections.length})

    - ${connHtml || '

    No connections

    '} - ${actionButtons} -
    - -
    - `; -} - -function showEdgeDetails(edge) { - var panel = document.getElementById("meshDetailPanel"); - if (!panel) return; - - panel.style.display = "block"; - - var stateColor = edge.state === "connected" ? "#4CAF50" : edge.state === "failed" ? "#F44336" : "#FF9800"; - - // Describe directionality - var directionText = ""; - if (edge.bidirectional) { - directionText = "↔ Bidirectional (both publish to each other)"; - } else if (edge.hasOutgoing && !edge.hasIncoming) { - directionText = "→ One-way (source publishes to target)"; - } else if (edge.hasIncoming && !edge.hasOutgoing) { - directionText = "← One-way (target publishes to source)"; - } else { - directionText = "Unknown"; - } - - // Check if this connection is patched via mix-minus - var isPatched = isConnectionPatched(edge.source, edge.target); - var patchedStatus = isPatched ? '

    Status: 🔊 Patched via Mix-Minus

    ' : ''; - - // Check if patching is applicable (only guest↔guest, not director/viewer edges) - var sourceNode = meshData.nodes[edge.source]; - var targetNode = meshData.nodes[edge.target]; - var canPatch = !(sourceNode && (sourceNode.isDirector || sourceNode.isViewer)) && - !(targetNode && (targetNode.isDirector || targetNode.isViewer)); - - // Build action buttons based on state - var actionButtons = ''; - if (edge.state === 'failed' || edge.state === 'disconnected') { - actionButtons += ``; - if (canPatch && !isPatched) { - actionButtons += ``; - } else if (isPatched) { - actionButtons += ``; - } - } else if (isPatched) { - // Connection is healthy but still patched - offer to unpatch - actionButtons += ``; - } - - panel.innerHTML = ` -

    Connection Details

    -

    From: ${edge.sourceStreamID || edge.source.substring(0,12)}

    -

    To: ${edge.targetStreamID || edge.target.substring(0,12)}

    -

    State: ${edge.state}

    - ${patchedStatus} -

    Direction: ${directionText}

    - ${edge.bandwidth > 0 ? '

    Bandwidth: ' + edge.bandwidth + ' kbps

    ' : ''} - ${edge.candidateType !== 'unknown' ? '

    Type: ' + edge.candidateType + '

    ' : ''} -

    NACK count: ${edge.nackCount}

    -

    PLI count: ${edge.pliCount}

    -
    - ${actionButtons} - -
    - `; -} - -function reconnectEdge(sourceUUID, targetUUID) { - // Find the edge to determine direction - var edgeId = [sourceUUID, targetUUID].sort().join("-"); - var edgeData = meshData.edges.find(function(e) { return e.id === edgeId; }); - - // Determine who should initiate the reconnect - // For outgoing edges: source publishes to target, so source reconnects - // For incoming-only edges: target publishes to source, so target reconnects - var initiator = sourceUUID; - var peer = targetUUID; - if (edgeData && edgeData.hasIncoming && !edgeData.hasOutgoing) { - // Swap - target is the publisher - initiator = targetUUID; - peer = sourceUUID; - } - - directReconnectPeer(initiator, peer); - - // Visual feedback - var edge = document.querySelector('[data-edge-id="' + edgeId + '"]'); - if (edge) { - edge.setAttribute("stroke", "#FF9800"); - edge.setAttribute("stroke-dasharray", "10,5"); - } - - // Close detail panel - var panel = document.getElementById("meshDetailPanel"); - if (panel) panel.style.display = "none"; - - // Refresh after delay - setTimeout(requestMeshData, 3000); -} - -// ============================================ -// END MESH NETWORK VISUALIZATION -// ============================================ - -function directRefreshMicrophone(ele) { - var UUID = ele.dataset.UUID; - if (!UUID) { return; } - // remoteAudioLabel_ - // '[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]' - - if (getById("remoteAudioLabel_" + UUID).dataset.nomic) { - warnUser("No microphone is enabled for this user\n\nNothing to reresh."); - } - var data = {}; - data.refreshMicrophone = true; - data.UUID = UUID; - if (session.sendRequest(data, UUID)) { // Viewer is requesting the PUBLISHER - ele.classList.add("pressed"); - setTimeout((ele) => { - if (ele) { - ele.classList.remove("pressed"); - } - }, 400, ele); - } -} - -function gotDevicesRemote(deviceInfos, UUID) { - try { - if (document.getElementById("remoteVideoSelect_" + UUID)) { - var videoSelect = document.getElementById("remoteVideoSelect_" + UUID); - var length = videoSelect.options.length; - for (i = length - 1; i >= 0; i--) { - videoSelect.options[i] = null; - } - } else { - var videoSelect = document.createElement("select"); - videoSelect.id = "remoteVideoSelect_" + UUID; - - videoSelect.onchange = function () { - if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) { - getById("requestVideoDevice_" + UUID).innerHTML = ' apply'; - getById("requestVideoDevice_" + UUID).title = "This will update the remote device to the selected one"; - } else { - getById("requestVideoDevice_" + UUID).innerHTML = ' request'; - getById("requestVideoDevice_" + UUID).title = "This will ask the remote guest for permission to change"; - } - }; - - var buttonGO = document.createElement("button"); - buttonGO.innerHTML = ' refresh'; - buttonGO.title = "This will refresh the current device"; - buttonGO.id = "requestVideoDevice_" + UUID; - buttonGO.onclick = function () { - var data = {}; - data.changeCamera = videoSelect.value; - data.UUID = UUID; - session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER - }; - - var videoSelectDiv = document.createElement("div"); - query("#container_" + UUID + " .advancedVideoSettings").appendChild(videoSelectDiv); - videoSelectDiv.appendChild(videoSelect); - videoSelectDiv.appendChild(buttonGO); - } - - if (document.getElementById("remoteAudioSelect_" + UUID)) { - log("remoteAudioSelect_ "); - var audioSelect = document.getElementById("remoteAudioSelect_" + UUID); - var length = audioSelect.options.length; - for (i = length - 1; i >= 0; i--) { - audioSelect.options[i] = null; - } - } else { - var audioSelect = document.createElement("select"); - audioSelect.id = "remoteAudioSelect_" + UUID; - - audioSelect.onchange = function () { - log("ON CHANGE"); - if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) { - getById("requestAudioDevice_" + UUID).innerHTML = ' apply'; - getById("requestAudioDevice_" + UUID).title = "This will update the remote device to the selected one"; - } else { - getById("requestAudioDevice_" + UUID).innerHTML = ' request'; - getById("requestAudioDevice_" + UUID).title = "This will ask the remote guest for permission to change"; - } - }; - - var buttonGO = document.createElement("button"); - buttonGO.innerHTML = ' refresh'; - // buttonGO.style = "padding: 5px;"; - buttonGO.title = "This will refresh the current device"; - buttonGO.id = "requestAudioDevice_" + UUID; - - buttonGO.onclick = function () { - var data = {}; - data.changeMicrophone = audioSelect.value; - data.UUID = UUID; - session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER - }; - var audioSelectDiv = document.createElement("div"); - query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioSelectDiv); - audioSelectDiv.appendChild(audioSelect); - audioSelectDiv.appendChild(buttonGO); - } - - if (document.getElementById("remoteAudioOutputSelect_" + UUID)) { - var audioOutputSelect = document.getElementById("remoteAudioOutputSelect_" + UUID); - var length = audioOutputSelect.options.length; - for (i = length - 1; i >= 0; i--) { - audioOutputSelect.options[i] = null; - } - } else { - var audioOutputSelect = document.createElement("select"); - audioOutputSelect.id = "remoteAudioOutputSelect_" + UUID; - - audioOutputSelect.onchange = function () { - if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) { - getById("requestAudioOutputDevice_" + UUID).innerHTML = ' apply'; - getById("requestAudioOutputDevice_" + UUID).title = "This will update the remote device to the selected one"; - } else { - getById("requestAudioOutputDevice_" + UUID).innerHTML = ' request'; - getById("requestAudioOutputDevice_" + UUID).title = "This will ask the remote guest for permission to change"; - } - }; - - var buttonGO = document.createElement("button"); - buttonGO.innerHTML = ' refresh'; - buttonGO.title = "This will refresh the current device"; - buttonGO.id = "requestAudioOutputDevice_" + UUID; - buttonGO.onclick = function () { - var data = {}; - data.changeSpeaker = audioOutputSelect.value; - data.UUID = UUID; - session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER - }; - - var audioOutputSelectContainer = document.createElement("div"); - query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioOutputSelectContainer); - audioOutputSelectContainer.appendChild(audioOutputSelect); - audioOutputSelectContainer.appendChild(buttonGO); - } - - var matched = false; - var audiomatched = false; - for (let i = 0; i !== deviceInfos.length; ++i) { - const deviceInfo = deviceInfos[i]; - if (deviceInfo == null) { - continue; - } - if (deviceInfo.kind === "videoinput") { - const option = document.createElement("option"); - option.value = deviceInfo.deviceId || "default"; - option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; - if (getById("remoteVideoLabel_" + UUID).innerText == option.text) { - option.selected = "true"; - matched = true; - } - videoSelect.appendChild(option); - } else if (deviceInfo.kind === "audioinput") { - const option = document.createElement("option"); - option.value = deviceInfo.deviceId || "default"; - option.text = deviceInfo.label || `microphone ${audioSelect.length + 1}`; - if (getById("remoteAudioLabel_" + UUID).innerText == option.text) { - option.selected = "true"; - audiomatched = true; - } - audioSelect.appendChild(option); - } else if (deviceInfo.kind === "audiooutput") { - const option = document.createElement("option"); - option.value = deviceInfo.deviceId || "default"; - option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`; - if (getById("remoteAudioOutputSelect_" + UUID).innerText == option.text) { - option.selected = "true"; - } - audioOutputSelect.appendChild(option); - } - } - - if (!matched) { - getById("requestVideoDevice_" + UUID).innerHTML = ' request'; - getById("requestVideoDevice_" + UUID).title = "This will ask the remote guest for permission to change"; - } - - if (!audiomatched) { - getById("requestAudioDevice_" + UUID).innerHTML = ' request'; - getById("requestAudioDevice_" + UUID).title = "This will ask the remote guest for permission to change"; - } - } catch (e) { - errorlog(e); - } - - pokeIframeAPI("remote-devices-info", deviceInfos, UUID); -} - -var timeoutTone = false; -function playtone(screen = false, tonename = "testtone") { - if (timeoutTone) return; - - if (session.beepToNotify && tonename) { - if (session.beepToNotify !== true) { - let val = tonename.split("tone")[0]; - if (!session.beepToNotify.includes(val)) { - return; - } - } - } - - const type = tonename.replace("tone", ""); - if (session.notifyTypes && !session.notifyTypes.includes(type)) return; - - timeoutTone = true; - setTimeout(() => timeoutTone = false, 500); - - const toneEle = document.getElementById(tonename); - if (!toneEle) return; - - if (iOS || iPad) { - toneEle.muted = false; - toneEle.play().catch(e => console.error('Playback failed:', e)); - return; - } - - if (screen && getById("outputSourceScreenshare")) { - session.sink = getById("outputSourceScreenshare").value; - saveSettings(); - } - - if (session.sink) { - toneEle.setSinkId(session.sink) - .then(() => toneEle.play()) - .catch(() => toneEle.play()); - } else { - toneEle.play(); - } -} - -function updateAudioSource(newUrl, name = "testtone") { - var audioElement = document.getElementById(name); - - if (!audioElement) { - audioElement = createAudioElement(); - audioElement.id = name; - getById("testtone").parentNode.insertBefore(audioElement, getById("testtone").nextSibling); - } - - audioElement.src = newUrl; - - var sources = audioElement.getElementsByTagName("source"); - var extension = newUrl.split(".").pop().toLowerCase(); - var mimeType; - - switch (extension) { - case "mp3": - mimeType = "audio/mpeg"; - break; - case "wav": - mimeType = "audio/wav"; - break; - case "ogg": - mimeType = "audio/ogg"; - break; - case "aac": - case "m4a": - mimeType = "audio/aac"; - break; - case "opus": - mimeType = "audio/opus"; - break; - case "flac": - mimeType = "audio/flac"; - break; - case "webm": - mimeType = "audio/webm"; - break; - default: - console.error("Unsupported file type:", extension); - return; - } - if (sources && sources.length === 1) { - sources[0].src = newUrl; - sources[0].type = mimeType; - } else { - audioElement.innerHTML = ""; - var newSource = document.createElement("source"); - newSource.src = newUrl; - newSource.type = mimeType; - audioElement.appendChild(newSource); - } - audioElement.load(); -} - -function showNotification(title, body = "") { - if (!Notification) { - return; - } - if (Notification.permission !== "granted") { - Notification.requestPermission(); - } else { - let icon = "/media/old_icon.png"; //this is a large image may take more time to show notifiction, replace with small size icon - - let notification = new Notification(title, { body, icon }); - - notification.onclick = () => { - notification.close(); - window.parent.focus(); - }; - } -} - -function toFirefoxConstraint(chromeConstraint) { - if (!chromeConstraint || typeof chromeConstraint !== 'object') { - return chromeConstraint; - } - - const result = { ...chromeConstraint }; - - if (result.audio && typeof result.audio === 'object') { - result.audio = flattenConstraints(result.audio); - } - - if (result.video && typeof result.video === 'object') { - result.video = flattenConstraints(result.video); - } - - return result; -} - -function flattenConstraints(constraints) { - const result = { ...constraints }; - - for (const [key, value] of Object.entries(constraints)) { - if (value && typeof value === 'object') { - if ('exact' in value) { - result[key] = value.exact; - } else if ('ideal' in value) { - result[key] = value.ideal; - } - } - } - - return result; -} - -async function getAudioOnly(selector, trackid = null, override = false, requestToken = null) { - var audioSelect = document.querySelector(selector).querySelectorAll("input,option"); - var audioList = []; - var streams = []; - log("getAudioOnly()"); - - // Fast-path: if override includes a specific deviceId, use it directly - if (override && override.audio && (override.audio.deviceId || (override.audio.deviceId && override.audio.deviceId.exact))) { - let o = JSON.parse(JSON.stringify(override)); - if (typeof o.audio.deviceId === "string") { - o.audio.deviceId = { exact: o.audio.deviceId }; - } - o.video = false; - if (Firefox) { - o = toFirefoxConstraint(o); - } - warnlog("navigator.mediaDevices.getUserMedia starting (override)..."); - if (navigator.mediaDevices) { - var stream = await navigator.mediaDevices - .getUserMedia(o) - .then(function (stream2) { - log("get audio sucecss"); - pokeIframeAPI("local-microphone-event"); - return stream2; - }) - .catch(function (err) { - warnlog(err); - return false; - }); - if (stream) { - if (requestToken !== null && requestToken !== getAudioUserMediaRequestID) { - stream.getTracks().forEach(track => track.stop()); - } else { - streams.push(stream); - } - } - } - return streams; - } - for (var i = 0; i < audioSelect.length; i++) { - if (audioSelect[i].value == "ZZZ") { - continue; - } else if (trackid == audioSelect[i].value) { - // skip already excluded - continue; - } else if ("screen" == audioSelect[i].dataset.type) { - // skip already excluded ---------- !!!!!! DOES THIS MAKE SENSE? TODO: CHECK - continue; - } else if (audioSelect[i].checked) { - log(audioSelect[i]); - audioList.push(audioSelect[i]); - } else if (audioSelect[i].selected) { - log(audioSelect[i]); - audioList.push(audioSelect[i]); - } - } - - // Deduplicate selections so the same physical mic is not captured multiple times. - try { - const uniqueSeen = new Set(); - const groupedByBase = new Map(); - const orderedEntries = []; - - for (var i = 0; i < audioList.length; i++) { - const el = audioList[i]; - const rawLabel = (el.dataset && typeof el.dataset.label !== "undefined" && el.dataset.label) ? el.dataset.label : (el.label || el.text || el.textContent || ""); - let normLabel = normalizeAudioAliasLabel(rawLabel); - const deviceIdLower = (el.value || "").toLowerCase(); - const groupId = (el.dataset && el.dataset.groupId) || ""; - const hasGroupId = !!groupId; - const isDefaultish = (deviceIdLower === "default" || deviceIdLower === "communications"); - - if (!normLabel) { - if (!isDefaultish) { - normLabel = rawLabel ? rawLabel.toLowerCase() : (groupId || deviceIdLower); - } - } - - const baseKey = groupId || normLabel || deviceIdLower || rawLabel; - const groupKey = groupId || normLabel || deviceIdLower || baseKey; - const uniqueKey = (isDefaultish && !groupId) - ? (groupKey + "|alias") - : (groupKey + "|" + ((el.value || "").toLowerCase() || normLabel || rawLabel || "")); - - if (uniqueSeen.has(uniqueKey)) { - continue; - } - uniqueSeen.add(uniqueKey); - - const entry = { - element: el, - isDefaultish, - hasGroupId, - groupKey, - baseKey, - index: null - }; - - let groupMap = groupedByBase.get(baseKey); - if (!groupMap) { - groupMap = new Map(); - groupedByBase.set(baseKey, groupMap); - } - - const existing = groupMap.get(groupKey); - if (!existing) { - entry.index = orderedEntries.length; - orderedEntries.push(entry); - groupMap.set(groupKey, entry); - continue; - } - - let replace = false; - if (existing.isDefaultish && !entry.isDefaultish) { - replace = true; - } else if (!existing.isDefaultish && entry.isDefaultish) { - replace = false; - } else if (!existing.hasGroupId && entry.hasGroupId) { - replace = true; - } - - if (replace) { - entry.index = existing.index; - orderedEntries[existing.index] = entry; - groupMap.set(groupKey, entry); - } - } - - audioList = orderedEntries.map(entry => entry.element); - } catch (e) { /* non-fatal */ } - - for (var i = 0; i < audioList.length; i++) { - if (session.echoCancellation !== false && session.autoGainControl !== false && session.noiseSuppression !== false && (session.voiceIsolation !== true)) { - var constraint = { - audio: { - deviceId: { - exact: audioList[i].value - } - } - }; - } else { - // Just trying to avoid problems with some systems that don't support these features - var constraint = { - audio: { - deviceId: { - exact: audioList[i].value - } - } - }; - if (session.echoCancellation === false) { - constraint.audio.echoCancellation = false; - } else { - constraint.audio.echoCancellation = true; - } - if (session.autoGainControl === false) { - constraint.audio.autoGainControl = false; - } else { - constraint.audio.autoGainControl = true; - } - if (session.noiseSuppression === false) { - constraint.audio.noiseSuppression = false; - } else { - constraint.audio.noiseSuppression = true; - } - if (session.voiceIsolation === true) { - constraint.audio.voiceIsolation = true; - } - } - constraint.video = false; - if (override !== false) { - log("Override true"); - constraint = override; - if (constraint.audio && typeof constraint.audio.deviceId === "string") { - constraint.audio.deviceId = { exact: constraint.audio.deviceId }; - } - } - - if (audioList[i].value && SelectedAudioInputDevices) { - if (SelectedAudioInputDevices.indexOf(audioList[i].value) === -1) { - if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { - SelectedAudioInputDevices = []; - } - SelectedAudioInputDevices.push(audioList[i].value); - } - } - - if (session.audioInputChannels) { - if (constraint.audio === true) { - constraint.audio = {}; - constraint.audio.channelCount = session.audioInputChannels; - } else if (constraint.audio) { - constraint.audio.channelCount = session.audioInputChannels; - } - } - - if (session.micSampleRate) { - if (constraint.audio === true) { - constraint.audio = {}; - constraint.audio.sampleRate = parseInt(session.micSampleRate); - } else if (constraint.audio) { - constraint.audio.sampleRate = parseInt(session.micSampleRate); - } - } - - if (session.micSampleSize) { - if (constraint.audio === true) { - constraint.audio = {}; - constraint.audio.sampleSize = parseInt(session.micSampleSize); - } else if (constraint.audio) { - constraint.audio.sampleSize = parseInt(session.micSampleSize); - } - } - - log("CONSTRAINT"); - log(constraint); - - if (Firefox) { - constraint = toFirefoxConstraint(constraint); - } - - warnlog("navigator.mediaDevices.getUserMedia starting..."); - if (navigator.mediaDevices) { - var stream = await navigator.mediaDevices - .getUserMedia(constraint) - .then(function (stream2) { - log("get audio sucecss"); - pokeIframeAPI("local-microphone-event"); - return stream2; - }) - .catch(function (err) { - warnlog(err); - if (!session.cleanOutput) { - if (override !== false) { - if (err.name) { - if (err.constraint) { - warnUser(err["name"] + ": " + err["constraint"]); - } - } - } - } - return false; - }); // More error reporting maybe? - if (stream) { - if (requestToken !== null && requestToken !== getAudioUserMediaRequestID) { - stream.getTracks().forEach(track => track.stop()); - } else { - streams.push(stream); - } - } - } else { - console.warn("navigator.mediaDevices was not found; try a different browser or check your settings"); - } - } - - if (requestToken !== null && requestToken !== getAudioUserMediaRequestID) { - try { - streams.forEach(stream => { - if (!stream) { - return; - } - stream.getTracks().forEach(track => track.stop()); - }); - } catch (e) { } - return []; - } - - return streams; -} - -function applyMirror(mirror) { - // true unmirrors as its already mirrored - if (!session.videoElement) { - return; - } - try { - var transFlip = ""; - var transNorm = ""; - if (document.getElementById("videosource") && session.windowed) { - transFlip = " translate(0, 50%)"; - transNorm = " translate(0, -50%)"; - } - - if (session.mirrored == 2) { - mirror = true; - } else if (session.mirrored === 0) { - mirror = true; - } - - if (!session.videoElement.style) { - session.videoElement.style = ""; - } - - if (session.permaMirrored) { - mirror = !mirror; - } - - if (mirror) { - if (session.mirrored && session.flipped) { - session.videoElement.style.transform = "scaleX(-1) scaleY(-1)" + transFlip; - session.videoElement.classList.add("mirrorControl"); - session.videoElement.dataset.transform = "scaleX(-1) scaleY(-1)"; - } else if (session.mirrored) { - session.videoElement.style.transform = "scaleX(-1)" + transNorm; - session.videoElement.classList.add("mirrorControl"); - session.videoElement.dataset.transform = "scaleX(-1)"; - } else if (session.flipped) { - session.videoElement.style.transform = "scaleY(-1) scaleX(1)" + transFlip; - session.videoElement.classList.remove("mirrorControl"); - session.videoElement.dataset.transform = "scaleX(1) scaleY(-11)"; - } else { - session.videoElement.style.transform = "scaleX(1)" + transNorm; - session.videoElement.classList.remove("mirrorControl"); - session.videoElement.dataset.transform = "scaleX(1)"; - } - } else { - if (session.mirrored && session.flipped) { - session.videoElement.style.transform = "scaleX(1) scaleY(-1)" + transFlip; - session.videoElement.classList.remove("mirrorControl"); - session.videoElement.dataset.transform = "scaleX(1) scaleY(-1)"; - } else if (session.mirrored) { - session.videoElement.style.transform = "scaleX(1)" + transNorm; - session.videoElement.classList.remove("mirrorControl"); - session.videoElement.dataset.transform = "scaleX(1)"; - } else if (session.flipped) { - session.videoElement.style.transform = "scaleY(-1) scaleX(-1)" + transFlip; - session.videoElement.classList.add("mirrorControl"); - session.videoElement.dataset.transform = "scaleX(-1) scaleY(-1)"; - } else { - session.videoElement.style.transform = "scaleX(-1)" + transNorm; - session.videoElement.classList.add("mirrorControl"); - session.videoElement.dataset.transform = "scaleX(-1)"; - } - } - var rotate = 0; - if (session.forceRotate !== false) { - if (session.rotate) { - rotate = session.forceRotate * -1 + parseInt(session.rotate); - } else { - rotate = session.forceRotate * -1; - } - if (session.forceRotate) { - rotate += 180; - } - } else { - rotate = session.rotate; - } - - if (rotate && rotate >= 360) { - rotate -= 360; - } - - session.videoElement.rotated = rotate; - session.videoElement.dataset.rotated = rotate; - - if (document.getElementById("previewWebcam") || document.getElementById("videosource")) { - var eleName = document.getElementById("previewWebcam") || document.getElementById("videosource"); - if (rotate) { - if (eleName.style.transform) { - eleName.style.transform += " rotate(" + rotate + "deg)"; - } else { - eleName.style.transform = "rotate(" + rotate + "deg)"; - } - eleName.classList.add("rotate"); - } else { - eleName.classList.remove("rotate"); - } - } else if (document.getElementById("container")) { - if (rotate == 0) { - document.getElementById("container").classList.remove("rotate"); - document.getElementById("container").style.transform = "unset"; - document.getElementById("container").style.transformOrigin = "unset"; - } else { - document.getElementById("container").style.transform = "rotate(" + rotate + "deg)"; - } - } else if (document.getElementById("minipreview")) { - var eleName = document.getElementById("minipreview"); - if (rotate == 90) { - eleName.style.transform = "rotate(90deg)"; - eleName.style.transformOrigin = "50% 100%"; - eleName.style.height = eleName.style.width; - eleName.style.width = "unset"; - } else if (session.videoElement.rotated == 270) { - eleName.style.transform = "rotate(270deg)"; - eleName.style.transformOrigin = "50% 100%"; - eleName.style.width = "unset"; - eleName.style.height = eleName.style.width; - } else if (session.videoElement.rotated == 180) { - eleName.style.transform = "rotate(180deg)"; - eleName.style.transformOrigin = "unset"; - } else { - eleName.classList.remove("rotate"); - eleName.style.transform = "unset"; - eleName.style.transformOrigin = "unset"; - } - } - // if not one of these, then it's going to be handled by the automixer automatically for us. - } catch (e) { - errorlog(e); - } -} - -function applyMirrorGuest(mirror, videoElement, flip = undefined) { - // true unmirrors as it's already mirrored - try { - const mirrored = !!mirror; - let flipped; - if (flip == null) { // Treats both null and undefined as "preserve existing" - flipped = videoElement.dataset && videoElement.dataset.flipGuest === "true"; - } else { - flipped = !!flip; - } - - if (videoElement.dataset) { - videoElement.dataset.mirrorGuest = mirrored ? "true" : "false"; - videoElement.dataset.flipGuest = flipped ? "true" : "false"; - } - - updateVideoTransform(videoElement); - - if (mirrored) { - videoElement.classList.add("mirrorControl"); - } else { - videoElement.classList.remove("mirrorControl"); - } - } catch (e) { - errorlog(e); - } -} - -// Applies transform (mirror, flip, rotation) to video elements -// Used for BOTH local preview (session.videoElement) AND remote guest videos (session.rpcs[UUID].videoElement) -function updateVideoTransform(videoElement) { - try { - if (!videoElement || !videoElement.style) { - return; - } - const dataset = videoElement.dataset || {}; - let mirrored = false; - let flipped = false; - let hasMirrorControl = false; - let hasFlipControl = false; - - // First priority: explicit dataset flags (from applyMirrorGuest or director controls) - if (dataset.mirrorGuest) { - mirrored = dataset.mirrorGuest === "true"; - hasMirrorControl = true; - } else { - // Second priority: preserve existing inline transform (from applyMirror) - const inlineTransform = videoElement.style.transform || ""; - if (inlineTransform.includes("scaleX(-1)")) { - mirrored = true; - hasMirrorControl = true; - } else if (inlineTransform.includes("scaleX(1)")) { - mirrored = false; - hasMirrorControl = true; - } else { - // Third priority: check computed style to preserve global CSS mirror (from &mirror parameter) - // This ensures rotation doesn't strip CSS-only mirrors - // Uses proper matrix decomposition to handle combined rotate+mirror - try { - const computed = window.getComputedStyle(videoElement); - const matrix = computed.transform; - if (matrix && matrix !== "none") { - const values = matrix.match(/matrix.*\((.+)\)/); - if (values) { - const parts = values[1].split(",").map(parseFloat); - const SCALE_EPS = 0.01; - - if (parts.length === 6) { - // 2D matrix(a, b, c, d, tx, ty) - // Decompose to extract scale with sign, accounting for rotation - const [a, b] = parts; - const scaleX = Math.sign(a || 1) * Math.hypot(a, b); - // Set control if scale differs from 1, or is explicitly -1 - const notOne = Math.abs(Math.abs(scaleX) - 1) > SCALE_EPS; - if (notOne || Math.abs(scaleX + 1) < SCALE_EPS) { - mirrored = scaleX < 0; - hasMirrorControl = true; - } - } else if (parts.length === 16) { - // 3D matrix3d(m00, m01, ..., m15) - // For 3D, scaleX is first column magnitude: sqrt(m00² + m01² + m02²) - const [m00, m01, m02] = parts; - const scaleX = Math.sign(m00 || 1) * Math.hypot(m00, m01, m02); - const notOne = Math.abs(Math.abs(scaleX) - 1) > SCALE_EPS; - if (notOne || Math.abs(scaleX + 1) < SCALE_EPS) { - mirrored = scaleX < 0; - hasMirrorControl = true; - } - } - } - } - } catch (e) { - // Fallback: if computed style fails, leave hasMirrorControl=false - } - } - } - - if (dataset.flipGuest) { - flipped = dataset.flipGuest === "true"; - hasFlipControl = true; - } else { - const inlineTransform = videoElement.style.transform || ""; - if (inlineTransform.includes("scaleY(-1)")) { - flipped = true; - hasFlipControl = true; - } else if (inlineTransform.includes("scaleY(1)")) { - flipped = false; - hasFlipControl = true; - } else { - // Check computed style for flip (scaleY), accounting for rotation - try { - const computed = window.getComputedStyle(videoElement); - const matrix = computed.transform; - if (matrix && matrix !== "none") { - const values = matrix.match(/matrix.*\((.+)\)/); - if (values) { - const parts = values[1].split(",").map(parseFloat); - const SCALE_EPS = 0.01; - - if (parts.length === 6) { - // 2D matrix(a, b, c, d, tx, ty) - // Decompose to extract scaleY with sign, accounting for rotation - const [, , c, d] = parts; - const scaleY = Math.sign(d || 1) * Math.hypot(c, d); - // Set control if scale differs from 1, or is explicitly -1 - const notOne = Math.abs(Math.abs(scaleY) - 1) > SCALE_EPS; - if (notOne || Math.abs(scaleY + 1) < SCALE_EPS) { - flipped = scaleY < 0; - hasFlipControl = true; - } - } else if (parts.length === 16) { - // 3D matrix3d(m00, m01, ..., m15) - // For 3D, scaleY is second column magnitude: sqrt(m04² + m05² + m06²) - const [, , , , m04, m05, m06] = parts; - const scaleY = Math.sign(m05 || 1) * Math.hypot(m04, m05, m06); - const notOne = Math.abs(Math.abs(scaleY) - 1) > SCALE_EPS; - if (notOne || Math.abs(scaleY + 1) < SCALE_EPS) { - flipped = scaleY < 0; - hasFlipControl = true; - } - } - } - } - } catch (e) { - // Fallback: leave hasFlipControl=false - } - } - } - - let rotated = 0; - if (dataset.rotated) { - rotated = parseInt(dataset.rotated) || 0; - } else if (typeof videoElement.rotated !== "undefined" && videoElement.rotated !== false) { - rotated = parseInt(videoElement.rotated) || 0; - } - - const transforms = []; - // Include explicit scaleX/scaleY to preserve: - // 1. Dataset flags (director controls) - // 2. Inline transforms (from applyMirror) - // 3. Computed CSS transforms (from global &mirror) - if (hasMirrorControl) { - if (mirrored) { - transforms.push("scaleX(-1)"); - } else { - transforms.push("scaleX(1)"); - } - } - if (hasFlipControl) { - if (flipped) { - transforms.push("scaleY(-1)"); - } else { - transforms.push("scaleY(1)"); - } - } - if (rotated) { - transforms.push("rotate(" + rotated + "deg)"); - } - - videoElement.style.transform = transforms.join(" "); - } catch (e) { - errorlog(e); - } -} - -function cleanupMediaTracks() { - getUserMediaRequestID += 1; - try { - if (session.streamSrcClone) { - session.streamSrcClone.getTracks().forEach(function (track) { - session.streamSrcClone.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } - if (session.streamSrc) { - session.streamSrc.getTracks().forEach(function (track) { - session.streamSrc.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } else { - checkBasicStreamsExist(); - } - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getTracks().forEach(function (track) { - session.videoElement.srcObject.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } else { - session.videoElement.srcObject = createMediaStream(); - } - activatedPreview = false; - } catch (e) { - errorlog(e); - } - getById("gowebcam").dataset.ready = "false"; - getById("gowebcam").dataset.audioready = "false"; - getById("gowebcam").disabled = true; -} - -/// Detect system changes; handle change or use for debugging -var lastAudioDevice = null; -var lastVideoDevice = null; -var lastPlaybackDevice = null; - -var audioReconnectTimeout = null; -var videoReconnectTimeout = null; -var grabDevicesTimeout = null; -var playbackReconnectTimeout = null; - -function reconnectDevices(event) { - /// TODO: Perhaps change this to only if there is a DISCONNECT; rather than ON NEW DEVICE? - - try { - if (session.firstPlayTriggered && session.audioCtx.state == "suspended") { - session.audioCtx.resume(); - } - } catch (e) { - warnlog("session.audioCtx.resume(); failed"); - } - - warnlog("A media device has changed"); - - if (iOS || iPad) { - // consider adding this back, but if no problem, whatever. - return; - } - - if (document.getElementById("previewWebcam")) { - // rest of the code isn't setup to support pre-connection setup. - clearTimeout(playbackReconnectTimeout); - playbackReconnectTimeout = setTimeout(function () { - if (document.getElementById("previewWebcam")) { - enumerateDevices() - .then(gotDevices) - .then(function () { - if (document.getElementById("previewWebcam")) { - resetupAudioOut(document.getElementById("previewWebcam")); - } - }); - } - }, 1000); - return; - } - - try { - if (!session.streamSrc) { - checkBasicStreamsExist(); - } else { - session.streamSrc.getTracks().forEach(function (track) { - if (track.readyState == "ended") { - if (track.kind == "audio") { - lastAudioDevice = track.label; - } else if (track.kind == "video") { - lastVideoDevice = track.label; - } - session.streamSrc.removeTrack(track); - log("remove ended old track"); - } - }); - - if (session.streamSrcClone) { - session.streamSrcClone.getTracks().forEach(function (track) { - if (track.readyState == "ended") { - log("remove track4"); - session.streamSrcClone.removeTrack(track); - track.stop(); - } - }); - } - - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getTracks().forEach(function (track) { - if (track.readyState == "ended") { - session.videoElement.srcObject.removeTrack(track); - log("remove ended old track"); - } - }); - } - } - /* } else { - clearTimeout(playbackReconnectTimeout); - playbackReconnectTimeout = setTimeout(function() { - enumerateDevices().then(gotDevices2).then(function() { - resetupAudioOut(); - }); - }, 1000); - return; - } */ - } catch (e) { - errorlog(e); - } - - clearTimeout(audioReconnectTimeout); - audioReconnectTimeout = null; - if (lastAudioDevice) { - audioReconnectTimeout = setTimeout(function () { - // only reconnect same audio device. If reconnected, clear the disconnected flag. - enumerateDevices() - .then(gotDevices2) - .then(function () { - // TODO: check to see if any audio is connected? - var streamConnected = false; - var audioSelect = getById("audioSource3").querySelectorAll("input"); - for (var i = 0; i < audioSelect.length; i++) { - if (audioSelect[i].value == "ZZZ") { - continue; - } else if (audioSelect[i].checked) { - log("checked"); - streamConnected = true; - break; - } - } - - if (!streamConnected) { - for (var i = 0; i < audioSelect.length; i++) { - if (audioSelect[i].value == "ZZZ") { - continue; - } - const lastNorm = normalizeAudioAliasLabel(lastAudioDevice || ""); - const currNorm = normalizeAudioAliasLabel(audioSelect[i].dataset.label || ""); - if (lastNorm && lastNorm === currNorm) { - // if the last disconnected device matches. - audioSelect[i].checked = true; - streamConnected = true; - lastAudioDevice = null; - warnlog("DISCONNECTED AUDIO DEVICE RECONNECTED"); - break; - } - } - } - activatedPreview = false; - grabAudio("#audioSource3"); - setTimeout(function () { - enumerateDevices() - .then(gotDevices2) - .then(function () { }); - }, 1000); - }); - }, 2000); - } - - clearTimeout(videoReconnectTimeout); // only reconnect same video device. - videoReconnectTimeout = null; - if (lastVideoDevice) { - videoReconnectTimeout = setTimeout(function () { - enumerateDevices() - .then(gotDevices2) - .then(function () { - var streamConnected = false; - var videoSelect = getById("videoSource3"); - errorlog(videoSelect.value); - - if (videoSelect.value == "ZZZ") { - for (var i = 0; i < videoSelect.options.length; i++) { - try { - if (videoSelect.options[i].innerHTML == lastVideoDevice) { - videoSelect.options[i].selected = "true"; - streamConnected = true; - lastVideoDevice = null; - break; - } - } catch (e) { - errorlog(e); - } - } - } - - if (streamConnected) { - activatedPreview = false; - grabVideo(session.quality, "videosource", "select#videoSource3"); - setTimeout(function () { - enumerateDevices() - .then(gotDevices2) - .then(function () { }); - }, 1000); - } - }); - }, 2000); - } - clearTimeout(playbackReconnectTimeout); - playbackReconnectTimeout = setTimeout(function () { - enumerateDevices() - .then(gotDevices2) - .then(function () { - resetupAudioOut(); - }); - }, 1000); -} - -function handleAudioTrackEnded(event) { - errorlog("Audio track ended unexpectedly"); - - // If there's already a reconnection attempt in progress, don't start another one - if (session.audioReconnectInProgress) { - return; - } - - let modalID = null; - if (!session.cleanOutput) { - warnUser("Your microphone disconnected. Attempting to reconnect...", 3200); - } - - session.audioReconnectInProgress = true; - - // Wait a brief moment to ensure the device has time to be recognized again if it was unplugged/replugged - setTimeout(function () { - activatedPreview = false; - grabAudio("#audioSource3", null, false); - - // Check if reconnection was successful after a delay - setTimeout(function () { - session.audioReconnectInProgress = false; - - closeModal(false, modalID); - - // Check if there are any active audio tracks after reconnection attempt - const hasAudioTracks = session.streamSrc && - session.streamSrc.getAudioTracks && - session.streamSrc.getAudioTracks().length > 0; - - if (!hasAudioTracks) { - // Reconnection failed, open settings menu - if (!session.cleanOutput) { - warnUser("Failed to reconnect your microphone. Please select a different device.", 5000); - } - - // Open the settings menu - if (typeof toggleSettings === 'function') { - toggleSettings(true); // force show the settings - } - } - }, 2000); - }, 1000); -} - -var vingesterFixed = false; -function resetupAudioOut(ele = false, forceReset = false) { - // this re-sets ALL output devices / sources - log("resetupAudioOut"); - if (iOS || iPad || SafariVersion || (ChromiumVersion && session.mobile)) { - // TODO : TEST TO SEE IF THIS WORKS WITH SAFARI? it might. - if (ele) { - return; - } - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement) { - try { - session.rpcs[UUID].videoElement - .pause() - .then(() => { - setTimeout( - function (uuid) { - log("win"); - try { - session.rpcs[uuid].videoElement.play().then(() => { - // updateIncomingAudioElement(uuid); // DO NOT DO THIS; it would cause a loop, as this updateIncomingAudioElement calls reseet - log("toggle pause/play"); - }); - } catch (e) { - errorlog(e); - } - }, - 0, - UUID - ); - }) - .catch(errorlog); - } catch (e) { - warnlog(e); - } - } - } - return; - } - - //if (isVingester){return;} - - var outputSelect = document.getElementById("outputSource") || document.getElementById("outputSource3") || false; - var sinkSet = false; - - try { - // function tries to get vingester working, by listening for its first tweak. - if (!session.sink && !vingesterFixed && document.getElementById("testtone") && document.getElementById("testtone").sinkId && isVingester) { - vingesterFixed = true; // only set the session.sink one time, as vingester on should be needing to set it one time. - session.sink = document.getElementById("testtone").sinkId; - } - } catch (e) { - errorlog(e); - } - - if (outputSelect && outputSelect.options && outputSelect.options.length) { - for (var i = 0; i < outputSelect.options.length; i++) { - if (outputSelect.options[i].value == session.sink) { - outputSelect.options[i].selected = "true"; - sinkSet = true; - } - } - if (sinkSet == false) { - if (outputSelect.options[0]) { - outputSelect.options[0].selected = "true"; - sinkSet = outputSelect.value; - } - } else { - sinkSet = session.sink; - } - } else { - sinkSet = session.sink; - } - - if (ele) { - try { - var eleSink = sinkSet; - if (ele.manualSink) { - eleSink = ele.manualSink; - log("Manual Sink Identified"); - } - if (eleSink) { - if (forceReset) { - ele.setSinkId("default") - .then(() => { - ele.setSinkId(eleSink) - .then(() => { - log("New Output Device"); - }) - .catch(error => { - if (!Firefox) { - warnlog(error); - } - // TODO: If error, then see if I need to add mic support, and grab it if needed. - }); - }) - .catch(error => { - if (!Firefox) { - errorlog(error); - } - ele.setSinkId(eleSink) - .then(() => { - log("New Output Device"); - }) - .catch(error => { - if (!Firefox) { - warnlog(error); - } - // TODO: If error, then see if I need to add mic support, and grab it if needed. - }); - }); - } - - ele.setSinkId(eleSink) - .then(() => { - log("New Output Device for self-preview"); - }) - .catch(error => { - if (!Firefox) { - warnlog("Need to add mic support, and grab it if needed."); - warnlog(error); - } - // TODO: If error, then see if I need to add mic support, and grab it if needed. - }); - } - } catch (e) { - warnlog("can't use setsink"); - } - log("audio sink: " + eleSink); - return; - } - - if (session.videoElement) { - // this would be a preview or videosource - try { - var eleSink = sinkSet; - if (session.videoElement.manualSink) { - eleSink = session.videoElement.manualSink; - } - if (eleSink) { - session.videoElement - .setSinkId(eleSink) - .then(() => { - log("New Output Device for self-preview"); - }) - .catch(error => { - if (!Firefox) { - warnlog(error); - } - // TODO: If error, then see if I need to add mic support, and grab it if needed. - }); - } - } catch (e) { - warnlog("can't use setsink"); - } - } - - for (UUID in session.rpcs) { - try { - if (session.rpcs[UUID].videoElement) { - var eleSink = sinkSet; - if (session.rpcs[UUID].videoElement.manualSink) { - eleSink = session.rpcs[UUID].videoElement.manualSink; - } - if (eleSink) { - session.rpcs[UUID].videoElement - .setSinkId(eleSink) - .then(() => { - log("New Output Device for: " + UUID); - }) - .catch(error => { - if (!Firefox) { - warnlog(error); - } - // TODO: If error, then see if I need to add mic support, and grab it if needed. - }); - } - } - } catch (e) { - warnlog(e); - } - } - log("audio sink 2: " + eleSink); -} - -function obfuscateURL(input) { - if (input.startsWith("https://obs.ninja/")) { - input = input.replace("https://obs.ninja/", "obs.ninja/"); - } else if (input.startsWith("http://obs.ninja/")) { - input = input.replace("http://obs.ninja/", "obs.ninja/"); - } else if (input.startsWith("obs.ninja/")) { - input = input.replace("obs.ninja/", "obs.ninja/"); - } else if (input.startsWith("https://vdo.ninja/")) { - input = input.replace("https://vdo.ninja/", "vdo.ninja/"); - } else if (input.startsWith("http://vdo.ninja/")) { - input = input.replace("http://vdo.ninja/", "vdo.ninja/"); - } else if (input.startsWith("vdo.ninja/")) { - input = input.replace("vdo.ninja/", "vdo.ninja/"); - } - - input = input.replace("&view=", "&v="); - input = input.replace("&view&", "&v&"); - input = input.replace("?view&", "?v&"); - input = input.replace("?view=", "?v="); - - input = input.replace("&videobitrate=", "&vb="); - input = input.replace("?videobitrate=", "?vb="); - input = input.replace("&bitrate=", "&vb="); - input = input.replace("?bitrate=", "?vb="); - - input = input.replace("?audiodevice=", "?ad="); - input = input.replace("&audiodevice=", "&ad="); - - input = input.replace("?label=", "?l="); - input = input.replace("&label=", "&l="); - - input = input.replace("?stereo=", "?s="); - input = input.replace("&stereo=", "&s="); - input = input.replace("&stereo&", "&s&"); - input = input.replace("?stereo&", "?s&"); - - input = input.replace("?webcam&", "?wc&"); - input = input.replace("&webcam&", "&wc&"); - - input = input.replace("?remote=", "?rm="); - input = input.replace("&remote=", "&rm="); - - input = input.replace("?password=", "?p="); - input = input.replace("&password=", "&p="); - - input = input.replace("&maxvideobitrate=", "&mvb="); - input = input.replace("?maxvideobitrate=", "?mvb="); - - input = input.replace("&maxbitrate=", "&mvb="); - input = input.replace("?maxbitrate=", "?mvb="); - - input = input.replace("&height=", "&h="); - input = input.replace("?height=", "?h="); - - input = input.replace("&width=", "&w="); - input = input.replace("?width=", "?w="); - - input = input.replace("&quality=", "&q="); - input = input.replace("?quality=", "?q="); - - input = input.replace("&cleanoutput=", "&clean="); - input = input.replace("?cleanoutput=", "?clean="); - - input = input.replace("&maxviewers=", "&clean="); - input = input.replace("?maxviewers=", "?clean="); - - input = input.replace("&frameRate=", "&fr="); - input = input.replace("?frameRate=", "?fr="); - - input = input.replace("&fps=", "&fr="); - input = input.replace("?fps=", "?fr="); - - input = input.replace("&roomid=", "&r="); - input = input.replace("?roomid=", "?r="); - - input = input.replace("&room=", "&r="); - input = input.replace("?room=", "?r="); - - log(input); - var key = "OBSNINJAFORLIFE"; - var encrypted = CryptoJS.AES.encrypt(input, key); - var output = "https://invite.cam/" + encrypted.toString(); - return output; -} - -function notifyOfScreenShare() { - if (session.notifyScreenShare) { - var data = {}; - data.screenShareState = session.screenShareState; - session.sendMessage(data); - } -} - -var beforeScreenShare = null; // video -var screenShareAudioTrack = null; -async function toggleScreenShare(reload = false) { - /// &sstype=1 - - var quality = session.quality_ss; - - if (quality === false) { - quality = session.roomid ? session.quality_room : session.quality_wb; - } - - if (session.quality !== false) { - quality = session.quality; - } - if (session.screensharequality !== false) { - quality = session.screensharequality; - } - - if (reload) { - // quality = 0, audio = true, videoOnEnd = false) { - await grabScreen(quality, true, true).then(res => { - if (res != false) { - session.screenShareState = true; - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - notifyOfScreenShare(); - - getById("screensharebutton").classList.add("green"); - getById("screensharebutton").ariaPressed = "true"; - enumerateDevices() - .then(gotDevices2) - .then(function () { }); - } - }); - return; - } - if (session.screenShareState == false) { - // adding a screen - await grabScreen(quality, true, true).then(res => { - if (res != false) { - session.screenShareState = true; - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - notifyOfScreenShare(); - - //session.refreshScale(); - - getById("screensharebutton").classList.add("green"); - getById("screensharebutton").ariaPressed = "true"; - enumerateDevices() - .then(gotDevices2) - .then(function () { }); - - //if (session.videoElement.readyState!==4){ - session.videoElement.play().then(() => { - log("start play doublecheck"); - }); - //} - updateMixer(); - pokeIframeAPI("screen-share-state", true); - } - }); - } else { - // removing a screen . session.screenShareState already true true ///////////////////////////////// - - session.screenShareState = false; - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - notifyOfScreenShare(); - - if (!session.streamSrc) { - checkBasicStreamsExist(); - } - - if (screenShareAudioTrack) { - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getAudioTracks().forEach(function (track) { - // previous video track; saving it. Must remove the track at some point. - if (screenShareAudioTrack.id == track.id) { - // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. - log("remove ss track"); - session.videoElement.srcObject.removeTrack(track); - track.stop(); - } - }); - } - - if (session.streamSrcClone) { - // - session.streamSrcClone.getAudioTracks().forEach(function (track) { - if (screenShareAudioTrack.id == track.id) { - // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. - log("remove ss track clone"); - session.streamSrcClone.removeTrack(track); - track.stop(); - } - }); - } - if (session.streamSrc) { - session.streamSrc.getAudioTracks().forEach(function (track) { - // previous video track; saving it. Must remove the track at some point. - if (screenShareAudioTrack.id == track.id) { - // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. - log("remove ss track audio"); - session.streamSrc.removeTrack(track); - track.stop(); - } - }); - } - - session.videoElement.srcObject = outboundAudioPipeline(); // updateREnderOoutput is just for video if videoElement is already activated. - screenShareAudioTrack = null; - senderAudioUpdate(); - } - - var addedAlready = false; - if (session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(function (track) { - if (beforeScreenShare && track.id == beforeScreenShare.id) { - addedAlready = true; - } else { - log("remove ss track 44"); - session.streamSrc.removeTrack(track); - track.stop(); - } - }); - } - - if (session.streamSrcClone) { - session.streamSrcClone.getVideoTracks().forEach(function (track) { - if (beforeScreenShare && track.id == beforeScreenShare.id) { - // - } else { - log("remove ss track 45"); - session.streamSrcClone.removeTrack(track); - track.stop(); - } - }); - } - - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getVideoTracks().forEach(function (track) { - if (beforeScreenShare && track.id == beforeScreenShare.id) { - addedAlready = true; - } else { - log("remove ss track 46"); - session.videoElement.srcObject.removeTrack(track); - track.stop(); - } - }); - } - - getById("screensharebutton").classList.remove("green"); - getById("screensharebutton").ariaPressed = "false"; - - if (beforeScreenShare) { - if (addedAlready == false) { - session.streamSrc.addTrack(beforeScreenShare); // add back in the video track we had before we started screen sharing. It should be NULL if we changed the video track else where (such as via the settings). #TODO: - } - } - - beforeScreenShare = null; - updateRenderOutpipe(); // this syncs the video - - toggleSettings(true); // forceShow - updateMixer(); - } -} - -var ipcRenderer = false; -var ElectronDesktopCapture = false; -var electronAppAudioInstance = null; -var electronAppAudioSupportChecked = false; -var electronAppAudioSupported = false; - -function electronSupportsApplicationAudio() { - if (electronAppAudioSupportChecked) { - return electronAppAudioSupported; - } - electronAppAudioSupportChecked = true; - try { - if (typeof window !== "undefined") { - if (window.WindowAudioStream) { - electronAppAudioSupported = true; - } else if (window.electronApi && typeof window.electronApi.isWindowAudioCaptureAvailable === "function") { - electronAppAudioSupported = !!window.electronApi.isWindowAudioCaptureAvailable(); - } else { - electronAppAudioSupported = false; - } - } - } catch (err) { - console.warn("Failed to determine application audio support:", err); - electronAppAudioSupported = false; - } - return electronAppAudioSupported; -} - -function ensureElectronAppAudioInstance() { - if (!electronSupportsApplicationAudio()) { - return null; - } - if (!electronAppAudioInstance) { - try { - electronAppAudioInstance = new window.WindowAudioStream(); - } catch (err) { - console.error("Failed to create WindowAudioStream instance:", err); - electronAppAudioSupported = false; - electronAppAudioInstance = null; - } - } - return electronAppAudioInstance; -} - -function extractElectronAudioTargetFromSource(source) { - if (!source || !source.id) { - return null; - } - const id = String(source.id); - if (!id.toLowerCase().startsWith("window:")) { - return null; - } - const match = id.match(/window:(\d+)/i); - if (match && match[1]) { - const numericId = parseInt(match[1], 10); - if (!Number.isNaN(numericId) && numericId > 0) { - return numericId; - } - } - return null; -} - -async function attachElectronApplicationAudio(stream, source) { - const targetId = extractElectronAudioTargetFromSource(source); - if (!targetId) { - console.warn("Application audio capture requires a window source."); - return false; - } - const instance = ensureElectronAppAudioInstance(); - if (!instance) { - return false; - } - - try { - const audioStream = await instance.start(targetId); - if (!audioStream) { - return false; - } - - const clonedTracks = []; - audioStream.getAudioTracks().forEach(track => { - const clone = track.clone(); - clonedTracks.push(clone); - stream.addTrack(clone); - }); - - const cleanup = async () => { - clonedTracks.forEach(track => { - try { - track.stop(); - } catch (err) { - console.warn("Failed to stop application audio track:", err); - } - }); - if (instance.isCapturing()) { - try { - await instance.stop(); - } catch (err) { - console.warn("Failed to stop WindowAudioStream:", err); - } - } - }; - - const onceCleanup = () => { - cleanup().catch(err => console.error("Error cleaning up application audio:", err)); - }; - - if (typeof stream.addEventListener === "function") { - stream.addEventListener("inactive", onceCleanup, { once: true }); - } - if (stream && typeof stream.getVideoTracks === "function") { - stream.getVideoTracks().forEach(track => track.addEventListener("ended", onceCleanup, { once: true })); - } - clonedTracks.forEach(track => track.addEventListener("ended", onceCleanup, { once: true })); - - return true; - } catch (err) { - console.error("Failed to attach application audio:", err); - try { - if (instance.isCapturing()) { - await instance.stop(); - } - } catch (stopErr) { - console.warn("Failed to stop WindowAudioStream after error:", stopErr); - } - return false; - } -} - -async function createElectronDesktopAudioStream() { - const new_constraints = { - audio: { - mandatory: { - chromeMediaSource: "desktop" - } - }, - video: { - mandatory: { - chromeMediaSource: "desktop" - } - } - }; - new_constraints.video.mandatory.maxFrameRate = 1; - - const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); - if (stream && typeof stream.getVideoTracks === "function" && stream.getVideoTracks().length) { - const track = stream.getVideoTracks()[0]; - stream.removeTrack(track); - track.stop(); - } - return stream; -} - -if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - // this enables Screen Capture in Electron - try { - if (!ipcRenderer) { - ipcRenderer = require("electron").ipcRenderer; - } - - window.navigator.mediaDevices.getDisplayMedia = (constraints = false) => { - return new Promise(async (resolve, reject) => { - try { - if (session.autostart) { - session.autostart = false; - if (parseInt(session.screenshare) + "" === session.screenshare) { - var sscid = parseInt(session.screenshare) - 1; - if (sscid < 0) { - sscid = 0; - } - //ipcRenderer.sendSync('prompt', {title, val}); - const sources = await ipcRenderer.sendSync("getSources", { types: ["screen"] }); - var new_constraints = { - audio: { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: sources[sscid].id - } - }, - video: { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: sources[sscid].id - } - } - }; - if (session.audioDevice === 0) { - new_constraints.audio = false; - } - try { - if (constraints.video.width.ideal) { - new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal; - } - } catch (e) { } - try { - if (constraints.video.height.ideal) { - new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal; - } - } catch (e) { } - try { - if (constraints.video.frameRate.ideal) { - new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal; - } - } catch (e) { } - /// - //if (Firefox){ // this is electron - // new_constraints = toFirefoxConstraint(new_constraints); - //} - warnlog("navigator.mediaDevices.getUserMedia starting..."); - const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); - resolve(stream); - } else if (session.screenshare && session.screenshare !== true) { - var sscid = null; - const sources = await ipcRenderer.sendSync("getSources", { types: ["window"] }); - for (var i = 0; i < sources.length; i++) { - if (sources[i].name.toLowerCase().startsWith(session.screenshare.toLowerCase())) { - // check if anythign starts with - sscid = i; - break; - } - } - if (sscid === null) { - sscid = 0; // grab first window if nothing. - for (var i = 0; i < sources.length; i++) { - if (sources[i].name.toLowerCase().includes(session.screenshare.toLowerCase())) { - // check if something includes the string; fallback - sscid = i; - break; - } - } - } - /// - var new_constraints = { - audio: { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: sources[sscid].id - } - }, - video: { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: sources[sscid].id - } - } - }; - if (session.audioDevice === 0) { - new_constraints.audio = false; - } - try { - if (constraints.video.width.ideal) { - new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal; - } - } catch (e) { } - try { - if (constraints.video.height.ideal) { - new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal; - } - } catch (e) { } - try { - if (constraints.video.frameRate.ideal) { - new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal; - } - } catch (e) { } - /// - //if (Firefox){ - // new_constraints = toFirefoxConstraint(new_constraints); - //} - warnlog("navigator.mediaDevices.getUserMedia starting..."); - const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); - resolve(stream); - } else { - var sscid = 0; - const sources = await ipcRenderer.sendSync("getSources", { types: ["screen"] }); - /// - var new_constraints = { - audio: { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: sources[sscid].id - } - }, - video: { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: sources[sscid].id - } - } - }; - if (session.audioDevice === 0) { - new_constraints.audio = false; - } - try { - if (constraints.video.width.ideal) { - new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal; - } - } catch (e) { } - try { - if (constraints.video.height.ideal) { - new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal; - } - } catch (e) { } - try { - if (constraints.video.frameRate.ideal) { - new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal; - } - } catch (e) { } - warnlog(new_constraints); - /// - //if (Firefox){ - // new_constraints = toFirefoxConstraint(new_constraints); - //} - warnlog("navigator.mediaDevices.getUserMedia starting..."); - const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); - resolve(stream); - } - } else { - const sources = await ipcRenderer.sendSync("getSources", { types: ["screen", "window"] }); - const selectionElem = document.createElement("div"); - selectionElem.classList = "desktop-capturer-selection"; - if (session.screenshareVideoOnly) { - selectionElem.innerHTML = ` -
    - -
    - `; - } else { - selectionElem.innerHTML = ` -
    - -
    - `; - } - document.body.appendChild(selectionElem); - const systemAudioCheckbox = getById("alsoCaptureAudio"); - const appAudioOption = getById("captureAppAudioParent"); - const appAudioCheckbox = getById("captureAppAudio"); - if (appAudioOption && appAudioCheckbox) { - const supportsAppAudio = !macOS && electronSupportsApplicationAudio(); - if (supportsAppAudio) { - appAudioOption.style.display = "inline-block"; - appAudioCheckbox.addEventListener("change", () => { - if (!systemAudioCheckbox) { - return; - } - if (appAudioCheckbox.checked) { - systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio = systemAudioCheckbox.checked ? "true" : "false"; - if (systemAudioCheckbox.checked) { - systemAudioCheckbox.checked = false; - } - } else { - if (systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio === "true") { - systemAudioCheckbox.checked = true; - } - delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio; - } - }); - } else { - appAudioOption.style.display = "none"; - appAudioCheckbox.checked = false; - } - } - if (systemAudioCheckbox) { - systemAudioCheckbox.addEventListener("change", () => { - if (systemAudioCheckbox.checked && appAudioCheckbox) { - appAudioCheckbox.checked = false; - } - delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio; - }); - } - if (macOS) { - const captureDesktopButton = getById("captureDesktopAudio"); - if (captureDesktopButton) { - captureDesktopButton.style.display = "none"; - } - const alsoCaptureAudioCheckbox = getById("alsoCaptureAudio"); - if (alsoCaptureAudioCheckbox) { - alsoCaptureAudioCheckbox.checked = false; - } - const alsoCaptureAudioParent1 = getById("alsoCaptureAudioParent1"); - if (alsoCaptureAudioParent1) { - alsoCaptureAudioParent1.style.display = "none"; - } - const alsoCaptureAudioParent2 = getById("alsoCaptureAudioParent2"); - if (alsoCaptureAudioParent2) { - alsoCaptureAudioParent2.style.display = "inline-block"; - } - if (appAudioOption) { - appAudioOption.style.display = "none"; - } - if (appAudioCheckbox) { - appAudioCheckbox.checked = false; - } - } - - document.getElementById("cancelscreenshare").addEventListener("click", async () => { - selectionElem.remove(); - reject(null); - }); - document.querySelectorAll(".desktop-capturer-click").forEach(button => { - button.addEventListener("click", async () => { - try { - if (button.id == "captureDesktopAudio") { - const stream = await createElectronDesktopAudioStream(); - resolve(stream); - selectionElem.remove(); - return; - } - const id = button.getAttribute("data-id"); - const source = sources.find(source => source.id === id); - if (!source) { - throw new Error(`Source with id ${id} does not exist`); - } - const systemAudioCheckbox = getById("alsoCaptureAudio"); - const appAudioCheckbox = getById("captureAppAudio"); - const hadSystemAudioPreference = !!(systemAudioCheckbox && (systemAudioCheckbox.checked || systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio === "true")); - const wantsAppAudio = !!(appAudioCheckbox && appAudioCheckbox.checked && electronSupportsApplicationAudio()); - const wantsSystemAudio = !wantsAppAudio && !!(systemAudioCheckbox && systemAudioCheckbox.checked); - - var audioStream = null; - if (wantsSystemAudio) { - audioStream = await createElectronDesktopAudioStream(); - } - - var new_constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: "desktop", - chromeMediaSourceId: source.id - } - } - }; - try { - if (constraints.video.width.ideal) { - new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal; - } - } catch (e) { } - try { - if (constraints.video.height.ideal) { - new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal; - } - } catch (e) { } - try { - if (constraints.video.frameRate.ideal) { - new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal; - } - } catch (e) { } - if (typeof warnlog === "function") { - warnlog(new_constraints); - warnlog("navigator.mediaDevices.getUserMedia starting..."); - } - const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); - let attachedAppAudio = false; - if (wantsAppAudio) { - attachedAppAudio = await attachElectronApplicationAudio(stream, source); - if (!attachedAppAudio && hadSystemAudioPreference) { - if (!audioStream) { - try { - audioStream = await createElectronDesktopAudioStream(); - } catch (audioErr) { - console.warn("Failed to fallback to desktop audio:", audioErr); - } - } - if (audioStream) { - console.warn("Falling back to desktop audio; application audio capture unavailable for selected source."); - if (systemAudioCheckbox) { - systemAudioCheckbox.checked = true; - delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio; - } - if (appAudioCheckbox) { - appAudioCheckbox.checked = false; - } - } - } - } - if (!attachedAppAudio && audioStream && typeof audioStream.getAudioTracks === "function") { - const audioTracks = audioStream.getAudioTracks(); - if (audioTracks.length) { - stream.addTrack(audioTracks[0]); - } - } - resolve(stream); - selectionElem.remove(); - } catch (err) { - errorlog("Error selecting desktop capture source:", err); - reject(err); - } - }); - }); - } - } catch (err) { - errorlog("Error displaying desktop capture sources:", err); - reject(err); - } - }); - }; - ElectronDesktopCapture = true; - } catch (e) { - warnlog("Couldn't load electron's screen capture. Elevate the app's permission to allow it (right-click?)"); - } -} - -async function grabScreen(quality = 0, audio = true, videoOnEnd = false) { - if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) { - if (!session.cleanOutput) { - setTimeout(function () { - if (iOS || iPad) { - warnUser(getTranslation("ios-no-screen-share"), false, false); - } else if (session.mobile) { - warnUser(getTranslation("mobile-no-screen-share"), false, false); - } else if (Firefox && !session.mobile) { - warnUser(getTranslation("no-screen-share-supported-firefox"), false, false); - } else { - warnUser(getTranslation("no-screen-share-supported"), false, false); - } - }, 1); - } - return false; - } - - if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - if (!ElectronDesktopCapture) { - if (!session.cleanOutput) { - warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)"); - } - return false; - } - } - - var video = {}; - - if (quality == -1) { - // unlocked capture resolution - } else if (quality == 0) { - video.width = { - ideal: 1920 - }; - video.height = { - ideal: 1080 - }; - } else if (quality == 1) { - video.width = { - ideal: 1280 - }; - video.height = { - ideal: 720 - }; - } else if (quality == 2) { - video.width = { - ideal: 640 - }; - video.height = { - ideal: 360 - }; - } else if (quality >= 3) { - // lowest - video.width = { - ideal: 320 - }; - video.height = { - ideal: 180 - }; - } - - if (session.width) { - video.width = { - ideal: session.width - }; - } - if (session.height) { - video.height = { - ideal: session.height - }; - } - - var constraints = { - // this part is a bit annoying. Do I use the same settings? I can add custom setting controls here later - audio: { - echoCancellation: false, // For screen sharing, we want it off by default. - autoGainControl: false, - noiseSuppression: false - }, - video: video - //,cursor: {exact: "none"} - }; - - try { - let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); - if (supportedConstraints.cursor) { - if (session.screensharecursor) { - constraints.video.cursor = ["always", "motion"]; - } else { - constraints.video.cursor = "never"; - } - } - if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) { - constraints.audio.suppressLocalAudioPlayback = true; - } - if (session.preferCurrentTab) { - constraints.preferCurrentTab = true; - } - if (session.selfBrowserSurface) { - constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include - } - if (session.surfaceSwitching) { - constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include - } - if (session.systemAudio) { - constraints.systemAudio = session.systemAudio; // exclude or include - } - if (session.displaySurface && supportedConstraints.displaySurface) { - constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser - } - } catch (e) { - warnlog("navigator.mediaDevices.getSupportedConstraints() not supported"); - } - - if (session.echoCancellation === true) { - constraints.audio.echoCancellation = true; - } - if (session.autoGainControl === true) { - constraints.audio.autoGainControl = true; - } - if (session.noiseSuppression === true) { - constraints.audio.noiseSuppression = true; - } - if (session.voiceIsolation === true) { - constraint.audio.voiceIsolation = true; - } - if (audio == false) { - constraints.audio = false; - } - - var overrideFramerate = false; - if (session.screensharefps !== false) { - constraints.video.frameRate = { - ideal: session.screensharefps, - max: session.screensharefps - }; - } else if (session.frameRate !== false && session.maxframeRate != false) { - overrideFramerate = session.frameRate; - constraints.video.frameRate = { - ideal: session.maxframeRate, - max: session.maxframeRate - }; - } else if (session.frameRate !== false) { - constraints.video.frameRate = session.frameRate; - } else if (session.maxframeRate != false) { - constraints.video.frameRate = { - ideal: session.maxframeRate, - max: session.maxframeRate - }; - } else { - constraints.video.frameRate = { - ideal: 60 - }; - } - - if (session.screenshareVideoOnly) { - constraints.audio = false; - } - - if (session.forceAspectRatio) { - // await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - if (constraints.video && constraints.video !== true) { - constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) }; - - if (constraints.video.width && !session.width) { - delete constraints.video.width; - } else if (constraints.video.height && !session.height) { - delete constraints.video.height; - } - } - } - - if (constraints.video !== false && Object.keys(constraints.video).length == 0) { - constraints.video = true; - } - - var wasDisabled = true; - - return navigator.mediaDevices - .getDisplayMedia(constraints) - .then(async function (stream) { - log("adding video tracks 2245"); - - try { - var constraint = {}; - if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) { - constraint.aspectRatio = parseFloat(session.forceAspectRatio); - } else if (session.forceScreenShareAspectRatio) { - constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio); - } - if (overrideFramerate) { - constraint.frameRate = overrideFramerate; - } - if (Object.keys(constraint).length) { - await stream.getVideoTracks()[0].applyConstraints({ - advanced: [constraint] - }); - log({ - advanced: [constraint] - }); - } - } catch (e) { - errorlog(e); - } - - try { - if (session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(function (track) { - //track.stop(); - beforeScreenShare = track; - session.streamSrc.removeTrack(track); - wasDisabled = false; // - log("stopping video track"); - }); - if (session.streamSrcClone) { - session.streamSrcClone.getVideoTracks().forEach(function (track) { - log("remove ss track clone 11"); - session.streamSrcClone.removeTrack(track); - track.stop(); - }); - } - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getVideoTracks().forEach(function (track) { - //track.stop(); - wasDisabled = false; - session.videoElement.srcObject.removeTrack(track); - log("stopping video track 2"); - }); - } else { - checkBasicStreamsExist(); - } - } else { - checkBasicStreamsExist(); // create srcObject + videoElement - } - } catch (e) { - warnlog(e); - } - - try { - stream.getVideoTracks()[0].onended = function (e) { - // if screen share stops, - warnlog(e); - - if (session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(function (track) { - session.streamSrc.removeTrack(track); - track.stop(); - log("stopping video track 3"); - if (beforeScreenShare && beforeScreenShare.id == track.id) { - beforeScreenShare.stop(); - beforeScreenShare = null; - } - }); - } - - if (session.streamSrcClone) { - session.streamSrcClone.getVideoTracks().forEach(function (track) { - session.streamSrcClone.removeTrack(track); - log("remove ss track clone 14"); - track.stop(); - }); - } - - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getVideoTracks().forEach(function (track) { - session.videoElement.srcObject.removeTrack(track); - track.stop(); - log("stopping video track 4"); - }); - } else { - //session.videoElement.srcObject = createMediaStream(); - session.videoElement.srcObject = outboundAudioPipeline(); - } - - if (screenShareAudioTrack) { - if (session.streamSrc) { - session.streamSrc.getAudioTracks().forEach(function (track) { - // previous video track; saving it. Must remove the track at some point. - if (screenShareAudioTrack.id == track.id) { - // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. - session.streamSrc.removeTrack(track); - track.stop(); - } - }); - } - if (session.streamSrcClone) { - session.streamSrcClone.getAudioTracks().forEach(function (track) { - // previous video track; saving it. Must remove the track at some point. - if (screenShareAudioTrack.id == track.id) { - // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. - session.streamSrcClone.removeTrack(track); - log("remove ss track 21"); - track.stop(); - } - }); - } - screenShareAudioTrack = null; - senderAudioUpdate(); - } - - session.screenShareState = false; - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - notifyOfScreenShare(); - - getById("screensharebutton").classList.remove("green"); - getById("screensharebutton").ariaPressed = "false"; - - if (videoOnEnd == true) { - if (beforeScreenShare) { - session.streamSrc.addTrack(beforeScreenShare); // updateRenderOutpipe - beforeScreenShare = null; - } - updateRenderOutpipe(); - toggleSettings(true); // forceshow - } else { - //session.refreshScale(); // since updateREnderOutput already has htis. - } - updateMixer(); - }; - } catch (e) { - log("No Video selected; screensharing?"); - } - - stream.getTracks().forEach(function (track) { - addScreenDevices(track); - session.streamSrc.addTrack(track, stream); // Lets not add the audio to this preview; echo can be annoying - }); - updateRenderOutpipe(); - - if (wasDisabled && stream.getVideoTracks().length && !session.videoMuted) { - var msg = {}; - msg.videoMuted = session.videoMuted; - session.sendMessage(msg); - } - - if (stream.getAudioTracks().length) { - screenShareAudioTrack = stream.getAudioTracks()[0]; - senderAudioUpdate(); - } - - session.applySoloChat(); // mute streams that should be muted if a director - session.applyIsolatedChat(); - - applyMirror(true); - - return true; - }) - .catch(function (err) { - errorlog(err); - errorlog(err.name); - if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") { - // User Stopped it. - if (macOS) { - warnUser(getTranslation("screen-permissions-denied"), false, false); - } - } else { - if (audio == true) { - if (err.name == "NotReadableError") { - if (!session.cleanOutput) { - warnUser(getTranslation("change-audio-output-device"), false, false); - } - setTimeout(function () { - grabScreen(quality, false); - }, 1); - return false; - } else { - setTimeout(function () { - grabScreen(quality, false); - }, 1); - } - } - if (!session.cleanOutput) { - setTimeout( - function (e) { - errorlog(e); - }, - 1, - err - ); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported - } - } - return false; - }); -} -function toggleBufferSettings(UUID) { - getById("bufferSettings").dataset.UUID = UUID; - toggle(getById("bufferSettings")); - if (getById("bufferSettings").style.display == "none") { - getById("modalBackdrop").innerHTML = ""; // Delete modal - getById("modalBackdrop").remove(); - } else { - getById("modalBackdrop").innerHTML = ""; // Delete modal - getById("modalBackdrop").remove(); - zindex = 25; - getById("bufferSettings").style.zIndex = 25; - var modalTemplate = `
    `; - document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - document.getElementById("modalBackdrop").addEventListener("click", toggleBufferSettings); - - var buffer = session.rpcs[UUID].buffer; - if (buffer === false) { - buffer = session.buffer || 0; - } - getById("bufferSettings") - .querySelectorAll("input") - .forEach(ele => { - ele.value = parseInt(buffer); - ele.title = ele.value + " ms"; - //getById("bufferSliderValue").innerText = ele.title - ele.onchange = function (e) { - session.rpcs[UUID].buffer = parseInt(e.target.value); - //getById("bufferSliderValue").innerText = session.rpcs[UUID].buffer + " ms"; - getById("bufferSettings") - .querySelectorAll("input") - .forEach(ele2 => { - if (ele2 !== e.target) { - ele2.value = parseInt(e.target.value); - } - ele2.title = parseInt(e.target.value) + " ms"; - }); - playoutdelay(UUID); // trigger - }; - ele.onkeyup = function (e) { - if (e.key === "Enter") { - session.rpcs[UUID].buffer = parseInt(e.target.value); - //getById("bufferSliderValue").innerText = session.rpcs[UUID].buffer + " ms"; - getById("bufferSettings") - .querySelectorAll("input") - .forEach(ele2 => { - if (ele2 !== e.target) { - ele2.value = parseInt(e.target.value); - } - ele2.title = parseInt(e.target.value) + " ms"; - }); - playoutdelay(UUID); // trigger - } - }; - ele.oninput = function (e) { - getById("bufferSettings") - .querySelectorAll("input") - .forEach(ele2 => { - if (ele2 !== e.target) { - ele2.value = parseInt(e.target.value); - } - ele2.title = parseInt(e.target.value) + " ms"; - }); - }; - }); - } -} - -function togglePTZControls(UUID) { - var modal = getById("ptzControlsModal"); - if (UUID) { - modal.dataset.UUID = UUID; - } - toggle(modal); - - if (modal.style.display == "none") { - if (getById("modalBackdrop")) { - getById("modalBackdrop").innerHTML = ""; - getById("modalBackdrop").remove(); - } - } else { - if (getById("modalBackdrop")) { - getById("modalBackdrop").innerHTML = ""; - getById("modalBackdrop").remove(); - } - zindex = 25; - modal.style.zIndex = 25; - var modalTemplate = `
    `; - document.body.insertAdjacentHTML("beforeend", modalTemplate); - document.getElementById("modalBackdrop").addEventListener("click", function() { - togglePTZControls(); - }); - - var targetUUID = modal.dataset.UUID; - - // Reset sliders to neutral positions - getById("ptzPanSlider").value = 0; - getById("ptzPanValue").innerText = "0"; - getById("ptzTiltSlider").value = 0; - getById("ptzTiltValue").innerText = "0"; - getById("ptzZoomSlider").value = 50; - getById("ptzZoomValue").innerText = "50"; - getById("ptzFocusSlider").value = 0; - getById("ptzFocusValue").innerText = "0"; - - // Pan slider handlers - getById("ptzPanSlider").oninput = function(e) { - getById("ptzPanValue").innerText = e.target.value; - }; - getById("ptzPanSlider").onchange = function(e) { - var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1 - session.requestPanChange(normalizedValue, targetUUID, session.remote, true); - }; - - // Tilt slider handlers - getById("ptzTiltSlider").oninput = function(e) { - getById("ptzTiltValue").innerText = e.target.value; - }; - getById("ptzTiltSlider").onchange = function(e) { - var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1 - session.requestTiltChange(normalizedValue, targetUUID, session.remote, true); - }; - - // Zoom slider handlers - getById("ptzZoomSlider").oninput = function(e) { - getById("ptzZoomValue").innerText = e.target.value; - }; - getById("ptzZoomSlider").onchange = function(e) { - var normalizedValue = parseInt(e.target.value) / 100; // Convert 0..100 to 0..1 - session.requestZoomChange(normalizedValue, targetUUID, session.remote, true); - }; - - // Focus slider handlers - getById("ptzFocusSlider").oninput = function(e) { - getById("ptzFocusValue").innerText = e.target.value; - }; - getById("ptzFocusSlider").onchange = function(e) { - var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1 - session.requestFocusChange(normalizedValue, targetUUID, session.remote, true); - }; - - // Reset Autofocus button handler - getById("ptzResetAutofocusBtn").onclick = function() { - session.requestAutofocusChange(true, targetUUID, session.remote); - }; - } -} - -function toggleRoomSettings() { - toggle(getById("roomSettings")); - if (getById("roomSettings").style.display == "none") { - //getById("modalBackdrop").innerHTML = ''; // Delete modal - //getById("modalBackdrop").remove(); - } else { - //getById("modalBackdrop").innerHTML = ''; // Delete modal - //getById("modalBackdrop").remove(); - zindex = 25; - getById("roomSettings").style.zIndex = 25; - var modalTemplate = `
    `; - // document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end - // document.getElementById("modalBackdrop").addEventListener("click", toggleRoomSettings); - getById("trbSettingInput").value = session.totalRoomBitrate; - getById("trbSettingInputManual").value = session.totalRoomBitrate; - getById("trbSettingInputFeedback").innerHTML = session.totalRoomBitrate; - - if (session.limitTotalBitrate !== false) { - getById("ltbSettingInputManual").value = session.limitTotalBitrate; - getById("ltbSettingInput").value = session.limitTotalBitrate; - getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled"; - } - - // Show auth access control if in auth mode and user is director - if (session.authMode && session.director && window.vdoAuth) { - getById("authAccessControl").style.display = "block"; - loadRoomAccessSettings(); - } - } -} - -function changeLTB(ele) { - session.limitTotalBitrate = parseInt(ele.value); - - getById("ltbSettingInputManual").value = session.limitTotalBitrate; - getById("ltbSettingInput").value = session.limitTotalBitrate; - getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled"; - - pokeIframeAPI("limit-total-bitrate", session.limitTotalBitrate); - session.limitTotalBitrateGuests(); -} - -function changeTRB(ele) { - session.totalRoomBitrate = parseInt(ele.value); - var msg = {}; - msg.directorSettings = {}; - msg.directorSettings.totalRoomBitrate = session.totalRoomBitrate; - session.sendMessage(msg); - pokeIframeAPI("total-room-bitrate", session.totalRoomBitrate); -} - -function sendMediaDevices(UUID) { - enumerateDevices().then(function (deviceInfos) { - var data = {}; - data.UUID = UUID; - data.mediaDevices = deviceInfos; - session.sendMessage(data, data.UUID); - }); -} - -function changeVideoDevice(index, quality = 0) { - enumerateDevices() - .then(gotDevices2) - .then(function () { - activatedPreview = false; - document.getElementById("videoSource3").selectedIndex = index + ""; - grabVideo(quality, "videosource", "#videoSource3"); - }); -} - -function changeAudioDevice(index) { - enumerateDevices() - .then(gotDevices2) - .then(function () { - activatedPreview = false; - var audioSelect = document.getElementById("audioSource3").querySelectorAll("input"); - for (var i = 0; i < audioSelect.length; i++) { - audioSelect[i].checked = false; - } - audioSelect[index - 1].checked = true; - grabAudio("#audioSource3"); - }); -} - -function changeVideoDeviceById(deviceId, UUID = false) { - enumerateDevices() - .then(gotDevices2) - .then(function () { - var opts = document.getElementById("videoSource3").options; - var index = false; - for (var opt, j = 0; (opt = opts[j]); j++) { - if (opt.value == deviceId) { - index = j; - break; - } - } - if (index !== false) { - if (document.getElementById("videoSource3").selectedIndex === index) { - // this is just refreshing the device. - activatedPreview = false; - grabVideo(0, "videosource", "#videoSource3", UUID); - } else if (UUID && !session.consent) { - window.focus(); - confirmAlt("Allow the director to change your video device to:\n\n" + opts[index].text + " ?").then(res => { - if (res) { - document.getElementById("videoSource3").selectedIndex = index; - activatedPreview = false; - grabVideo(0, "videosource", "#videoSource3", UUID); - } else { - try { - var data = {}; - data.UUID = UUID; - data.rejected = "changeCamera"; - session.sendMessage(data, data.UUID); - } catch (e) { } - } - }); - } else { - document.getElementById("videoSource3").selectedIndex = index; - activatedPreview = false; - grabVideo(0, "videosource", "#videoSource3", UUID); - } - } - }); -} - -function changeAudioDeviceById(deviceId, UUID = false) { - enumerateDevices() - .then(gotDevices2) - .then(function () { - var audioSelect = document.getElementById("audioSource3").querySelectorAll("input"); - var matched = false; - var exists = false; - for (var i = 0; i < audioSelect.length; i++) { - if (audioSelect[i].value == deviceId) { - exists = true; - if (audioSelect[i].checked) { - matched = true; - } - } - } - - if (exists) { - if (matched) { - // this is just refreshing the device. - activatedPreview = false; - //grabAudio("#audioSource3", UUID); - grabAudio("#audioSource3", null, false, UUID); - } else if (UUID && !session.consent) { - window.focus(); - confirmAlt("Allow the director to change your audio mic source?").then(res => { - if (res) { - // enumerateDevices().then(gotDevices2).then(function() { - var audioSelect = document.getElementById("audioSource3").querySelectorAll("input"); - for (var i = 0; i < audioSelect.length; i++) { - if (audioSelect[i].value == deviceId) { - audioSelect[i].checked = true; - } else { - audioSelect[i].checked = false; - } - } - activatedPreview = false; - grabAudio("#audioSource3", null, false, UUID); - // }); - } else { - try { - var data = {}; - data.UUID = UUID; - data.rejected = "changeMicrophone"; - session.sendMessage(data, data.UUID); - } catch (e) { } - } - }); - } else { - //enumerateDevices().then(gotDevices2).then(function() { - var audioSelect = document.getElementById("audioSource3").querySelectorAll("input"); - for (var i = 0; i < audioSelect.length; i++) { - if (audioSelect[i].value == deviceId) { - audioSelect[i].checked = true; - } else { - audioSelect[i].checked = false; - } - } - activatedPreview = false; - grabAudio("#audioSource3", null, false, UUID); - // }); - } - } - }); -} - -function changeAudioOutputDeviceById(deviceId, UUID = false) { - // remote control of the speaker output. - warnlog(deviceId); - if (document.getElementById("outputSource3")) { - enumerateDevices() - .then(gotDevices2) - .then(function () { - var index = false; - if (document.getElementById("outputSource3")) { - var opts = document.getElementById("outputSource3").options; - for (var opt, j = 0; (opt = opts[j]); j++) { - if (opt.value == deviceId) { - index = j; - break; - } - } - } - if (index !== false) { - if (document.getElementById("outputSource3").selectedIndex === index) { - // this is just refreshing the device. - session.sink = deviceId; - saveSettings(); - resetupAudioOut(); - } else if (UUID && !session.consent) { - // UUID just lets us inform the requester - window.focus(); - confirmAlt("Allow the director to change your audio's speaker to:\n\n" + opts[index].text + " ?").then(res => { - if (res) { - if (index !== false) { - document.getElementById("outputSource3").selectedIndex = index; - } - session.sink = deviceId; - saveSettings(); - resetupAudioOut(); - var data = {}; - data.UUID = UUID; - sendMediaDevices(data.UUID); - session.sendMessage(data, data.UUID); - } else { - try { - var data = {}; - data.UUID = UUID; - data.rejected = "changeSpeaker"; - session.sendMessage(data, data.UUID); - } catch (e) { } - } - }); - } else { - if (index !== false) { - document.getElementById("outputSource3").selectedIndex = index; - } - session.sink = deviceId; - saveSettings(); - resetupAudioOut(); - } - } - }); - } else { - session.sink = deviceId; - saveSettings(); - resetupAudioOut(); - } -} - -function checkBasicStreamsExist() { - log("checkBasicStreamsExist()"); - if (!session.streamSrc) { - session.streamSrc = createMediaStream(); - } - if (!session.videoElement) { - if (document.getElementById("videosource")) { - session.videoElement = document.getElementById("videosource"); - } else if (document.getElementById("previewWebcam")) { - session.videoElement = document.getElementById("previewWebcam"); - } else { - session.videoElement = createVideoElement(); - } - - session.videoElement.addEventListener( - "playing", - e => { - resetupAudioOut(session.videoElement, true); - }, - { once: true } - ); - - session.videoElement.onpause = event => { - // prevent things from pausing; human or other - if (!(event.ctrlKey || event.metaKey)) { - log("Video paused; auto playing"); - event.currentTarget - .play() - .then(_ => { - log("playing 10"); - }) - .catch(warnlog); - } - }; - - session.videoElement.addEventListener("error", function (event) { - errorlog("video error"); - errorlog(event); - setTimeout(function () { - if (session.videoElement) { - log("Trying to re-load local preview, as it may have crashed"); - session.videoElement.load(); - } - }, 1200); - }); - - //session.videoElement.addEventListener('loadedmetadata', function(event) { - // log("loadedmetadata"); - // log(event); - //}); - } - session.videoElement.srcObject = outboundAudioPipeline(); - toggleMute(true); - return session.videoElement; -} - -var getUserMediaRequestID = 0; -var getAudioUserMediaRequestID = 0; -var grabVideoUserMediaTimeout = null; -var grabVideoTimer = null; - -async function grabVideo(quality = 0, eleName = "previewWebcam", selector = "select#videoSourceSelect", callback = false) { - if (activatedPreview == true) { - log("activated preview return 2"); - return; - } - - if (session.miconly) { - return; - } - - activatedPreview = true; - log("Grabbing video: " + quality); - if (grabVideoTimer) { - clearTimeout(grabVideoTimer); - } - log("element:" + eleName); - - var wasDisabled = true; - try { - if (session.streamSrc) { - if (session.canvasWebGL) { - session.canvasWebGL.remove(); - session.canvasWebGL = null; - } - - if (session.canvasSource) { - session.canvasSource.srcObject.getTracks().forEach(function (trk) { - session.canvasSource.srcObject.removeTrack(trk); - trk.stop(); - wasDisabled = false; - }); - } - if (session.streamSrc) { - session.streamSrc.getVideoTracks().forEach(function (track) { - session.streamSrc.removeTrack(track); - log("remove ss track 9"); - track.stop(); - wasDisabled = false; - }); - } - if (session.streamSrcClone) { - session.streamSrcClone.getVideoTracks().forEach(function (track) { - session.streamSrcClone.removeTrack(track); - log("remove ss track s9"); - track.stop(); - }); - } - } else { - checkBasicStreamsExist(); - log("CREATE NEW STREAM"); - } - - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getVideoTracks().forEach(function (track) { - session.videoElement.srcObject.removeTrack(track); - log("remove ss track 98"); - track.stop(); - session.videoElement.load(); - wasDisabled = false; - }); - } else { - checkBasicStreamsExist(); - } - } catch (e) { - errorlog(e); - } - - session.videoElement.controls = session.showControls || false; - - log("selector: " + selector); - var videoSelect = document.querySelector(selector); // document.querySelector("videoSource3").value == "ZZZ" - log(videoSelect); - var mirror = false; - getById("cameraTip1").classList.add("hidden"); - - if (!videoSelect || videoSelect.value == "ZZZ") { - // if there is no video, or if manually set to audio ready, then do this step. - - clearTimeout(grabVideoUserMediaTimeout); - getUserMediaRequestID += 1; - - warnlog("ZZZ SET - so no VIDEO"); - SelectedVideoInputDevices = []; - saveSettings(); - - if (session.avatar && session.avatar.ready) { - updateRenderOutpipe(); - } else if (session.mobile && needsLegacyWakeLock()) { // OBSOLETE since we now have "WAKE LOCK" API used. - startLegacyKeepAliveLoop(); - } - - if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) { - if (session.autostart) { - publishWebcam(); // no need to mirror as there is no video... - return; - } else { - log("4462"); - updateStats(); - if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").dataset.ready = "true"; - if (document.getElementById("gowebcam").dataset.audioready == "true") { - document.getElementById("gowebcam").disabled = false; - //document.getElementById("gowebcam").innerHTML = getTranslation("start"); - miniTranslate(document.getElementById("gowebcam"), "start"); - document.getElementById("gowebcam").focus(); - } - } - } - } else { - // If they disabled the video but not in preview mode; but actualy live. We will want to remove the stream from the publishing - // we don't want to do this otherwise, as we are "replacing" the track in other cases. - // this does cause a problem, as previous bitrate settings & resolutions might not be applied if switched back.... must test - - if (session.avatar && session.avatar.ready) { - updateRenderOutpipe(); - return; - } - - if (session.chunked) { - for (UUID in session.pcs) { - session.chunkedStream(UUID); // make sure we check that this connection allows video / audio - } - // return; - } - try { - var miscSenders = []; - - if (session.whipOut && session.whipOut.getSenders) { - miscSenders.push(session.whipOut); - } - - miscSenders.forEach(dataRTC => { - if (dataRTC && dataRTC.getSenders) { - dataRTC.getSenders().forEach(sender => { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track && sender.track.kind == "video") { - var trk = getWhipOutCanvasTrack(dataRTC); - if (session.screenShareState && session.screenshareContentHint && trk.kind === "video") { - try { - trk.contentHint = session.screenshareContentHint; - } catch (e) { - errorlog(e); - } - } else if (session.contentHint && trk.kind === "video") { - try { - trk.contentHint = session.contentHint; - } catch (e) { - errorlog(e); - } - } - try { - sender.replaceTrack(trk); // replace may not be supported by all browsers. eek. - } catch (e) { - errorlog(e); - } - } - }); - } - }); - } catch (e) { - errorlog(e); - } - - for (UUID in session.pcs) { - if ("realUUID" in session.pcs[UUID]) { - continue; - } // do not apply to screen shares. - - if (session.chunked && session.pcs[UUID].allowChunked) { - continue; - } - - // for any connected peer, update the video they have if connected with a video already. - var senders = getSenders2(UUID); - senders.forEach(sender => { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track && sender.track.kind == "video") { - sender.track.enabled = false; // I'm not entirely sure if I shoudl be doing this to a video stream... but I suppose new connections won't get a stream, and old connections will just replace it? - getById("mutevideobutton").classList.add("hidden"); // hide the mute button, so they can't unmute while no video. - //session.pcs[UUID].removeTrack(sender); // replace may not be supported by all browsers. eek. - //errorlog("DELETED SENDER"); - } - }); - - } - - var msg = {}; - msg.videoMuted = true; // doesn;t matter if video is actually muted or not; no video is being sent - session.sendMessage(msg); - } - return; - } else { - if (videoSelect && videoSelect.value) { - SelectedVideoInputDevices = [videoSelect.value]; - saveSettings(); - } - - if (session.avatar && session.avatar.timer) { - clearInterval(session.avatar.timer); - session.avatar.timer = null; - } - - var sq = 0; - if (session.quality === false) { - sq = session.roomid ? session.quality_room : session.quality_wb; - } else if (session.quality > 2) { - // 1080, 720, and 360p - sq = 2; // hacking my own code. TODO: ugly, so I need to revisit this. - } else { - sq = session.quality; - } - - if (session.director && quality !== false) { - // URL-based quality won't matter if DIRECTOR; - // quality = quality; - } else if (quality === false || quality < sq) { - quality = sq; // override the user's setting - } - - if ((iOS || iPad) && SafariVersion < 15) { - // iOS will not work correctly at 1080p; likely a h264 codec issue. - if (quality == 0) { - quality = 1; - } - } - - var constraints = { - audio: false, - video: getUserMediaVideoParams(quality, iOS || iPad) - }; - - //if (Firefox){ - // constraints.video.height = constraints.video.height.ideal; - // constraints.video.width = constraints.video.height.ideal; - //} - - log("Quality selected:" + quality); - - if (session.outboundVideoBitrate_userSet === false) { - // default is 2500 - if (session.quality == 0) { // 1080p - session.outboundVideoBitrate = 4000; - } else if (session.quality == -1) { // unlocked - session.outboundVideoBitrate = 4000; - } else if (session.quality == -2) { // 4k - session.outboundVideoBitrate = 8000; - } else if (session.quality == -3) { // 2k - session.outboundVideoBitrate = 6000; - } else { - session.outboundVideoBitrate = false; - } - } - - if (session.facingMode) { - constraints.video.facingMode = { exact: session.facingMode }; // user or environment - } else if (iOS || iPad) { - constraints.video.deviceId = { - exact: videoSelect.value - }; // iPhone 6s compatible ? Needs to be exact for iPhone 6s - } else if (Firefox) { - // is firefox. - constraints.video.deviceId = { - exact: videoSelect.value - }; // Firefox is a dick. Needs it to be exact. - } else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) { - // NDI does not like "EXACT" - constraints.video.deviceId = videoSelect.value; // NDI is fucked up - } else { - constraints.video.deviceId = { - exact: videoSelect.value - }; // Default. Should work for Logitech, etc. - } - - if (session.width) { - constraints.video.width = { - exact: session.width - }; // manually specified - so must be exact - } - if (session.height) { - constraints.video.height = { - exact: session.height - }; - } - - if (session.frameRate) { - constraints.video.frameRate = { - exact: session.frameRate - }; - } else if (session.maxframeRate != false) { - constraints.video.frameRate = { - ideal: session.maxframeRate, - max: session.maxframeRate - }; - } else if ((iOS || iPad) && SafariVersion > 15) { - // iOS supports 720p60, but just 1080p30 : iphone 11 on march 2023 - if (quality === 1) { - // iphone 11 and older - if (!constraints.video.frameRate) { - constraints.video.frameRate = { - ideal: 60, - max: 60 - }; - } - } else if (iPhone12Up && quality < 1) { - // iphone 12 and up? - if (!constraints.video.frameRate) { - try { - if (videoSelect.options[videoSelect.selectedIndex].innerText.startsWith("Back ")) { - // front seems to be limited to 720p60 / 1080p30 - constraints.video.frameRate = { - ideal: 60, - max: 60 - }; - } - } catch (e) { - errorlog(e); - } - } - } - } - - if (session.ptz) { - if (constraints.video && constraints.video !== true) { - if (ChromiumVersion && ChromiumVersion > 80) { - constraints.video.pan = true; - constraints.video.tilt = true; - constraints.video.zoom = true; - } - } - } - - if (session.forceAspectRatio) { - // await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - if (constraints.video && constraints.video !== true) { - constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) }; - - if (constraints.video.width && !session.width) { - delete constraints.video.width; - } else if (constraints.video.height && !session.height) { - delete constraints.video.height; - } - } - } - - var obscam = false; - var mirrorcheck = false; - log(videoSelect.options[videoSelect.selectedIndex].text); - - if (!videoSelect.options[videoSelect.selectedIndex]) { - if (session.mobile) { - mirrorcheck = true; - mirror = false; - } else { - mirror = false; - } - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) { - // OBS Virtualcam - mirror = true; - obscam = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) { - // OBS Virtualcam - mirror = true; - obscam = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Streamlabs ")) { - // OBS Virtualcam - mirror = true; - obscam = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Dummy video device")) { - // Linuxv - mirror = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("vMix Video")) { - // vMix - mirror = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Blackmagic")) { - // Blackmagic devices - mirror = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("screen-capture-recorder")) { - // screen-capture-recorder - mirror = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" back")) { - // Android - mirror = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" rear")) { - // Android - mirror = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) { - // NDI Virtualcam - mirror = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Back Camera")) { - // iPhone and iOS - mirror = true; - } else if (videoSelect.options[videoSelect.selectedIndex].text.toLowerCase().includes("c922")) { - if (session.quality !== 2 && !session.cleanOutput) { - //getById("cameraTipContext1").innerHTML = getTranslation("camera-tip-c922"); - miniTranslate(getById("cameraTipContext1"), "camera-tip-c922"); - getById("cameraTip1").classList.remove("hidden"); - } - } else if (videoSelect.options[videoSelect.selectedIndex].text.toLowerCase().includes("cam link")) { - if (!session.cleanOutput) { - //getById("cameraTipContext1").innerHTML = getTranslation("camera-tip-camlink"); - miniTranslate(getById("cameraTipContext1"), "camera-tip-camlink"); - getById("cameraTip1").classList.remove("hidden"); - } - } else if (session.mobile) { - mirrorcheck = true; - mirror = false; - } else { - mirror = false; - } - - if (SamsungASeries && ChromiumVersion) { - if (!session.cleanOutput) { - //getById("cameraTipContext1").innerHTML = getTranslation("samsung-a-series"); - miniTranslate(getById("cameraTipContext1"), "samsung-a-series"); - getById("cameraTip1").classList.remove("hidden"); - } - } - if (session.nomirror) { - // do not have the camera be mirrored by default, unless using &mirror - session.mirrorExclude = true; - } else { - session.mirrorExclude = mirror; - } - - if (constraints.video && constraints.video !== true && Object.keys(constraints.video).length == 0) { - constraints.video = true; - } else if (constraints.video && constraints.video !== true && Object.keys(constraints.video).length == 1 && "deviceId" in constraints.video && "exact" in constraints.video.deviceId && constraints.video.deviceId.exact === "default") { - constraints.video = true; // solves issues with IOS, where no permission yet given - can't request device ID it seems until permissions is given. - } - - log(constraints); - clearTimeout(grabVideoUserMediaTimeout); - getUserMediaRequestID += 1; - var gumMediaID = getUserMediaRequestID; - var delayStart = 100; - if (ChromiumVersion > 110) { - // aded july 16th; speed up camera switching. - delayStart = 20; - } else if (Firefox) { - delayStart = 500; // cause firefox is buggy as crap - } - grabVideoUserMediaTimeout = setTimeout( - function (gumID, callback2) { - if (getUserMediaRequestID !== gumID) { - return; - } // cancel - - if (Firefox) { - constraints = toFirefoxConstraint(constraints); - } - - warnlog("navigator.mediaDevices.getUserMedia starting..."); - navigator.mediaDevices - .getUserMedia(constraints) - .then(function (stream) { - if (getUserMediaRequestID !== gumID) { - warnlog("GET USER MEDIA CALL HAS EXPIRED"); - stream.getTracks().forEach(function (track) { - stream.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - return; - } - log("adding video tracks 2412"); - stream.getVideoTracks().forEach(async function (track) { - try { - if (mirrorcheck) { - try { - var capabilities = track.getCapabilities(); - } catch (e) { - var capabilities = {}; - } - if ("facingMode" in capabilities) { - if (capabilities.facingMode == "environment") { - session.mirrorExclude = true; - } - } - if ("backgroundBlur" in capabilities) { - // Chrome original trial, until v117, and then??? - query('#effectSelector option[value="13"]').classList.remove("hidden"); - query('#effectSelector option[value="13"]').disabled = null; - query('#effectSelector3 option[value="13"]').classList.remove("hidden"); - query('#effectSelector3 option[value="13"]').disabled = null; - } else { - query('#effectSelector option[value="13"]').disabled = true; - query('#effectSelector3 option[value="13"]').disabled = true; - } - } - } catch (e) { } - session.streamSrc.addTrack(track); // tracks previously removed. - try { - track.onended = function (e) { - // hurrah! - warnlog(e); - refreshVideoDevice(); - }; - } catch (e) { - errorlog(e); - } - if (session.whiteBalance !== false) { - try { - await track.applyConstraints({ advanced: [{ whiteBalanceMode: "manual", colorTemperature: parseInt(session.whiteBalance) }] }); - } catch (e) { - errorlog(e); - try { - await track.applyConstraints({ advanced: [{ whiteBalanceMode: "manual" }] }); - } catch (e) { - warnlog(e); - } - } - } - if (session.exposure !== false) { - try { - await track.applyConstraints({ advanced: [{ exposureMode: "manual", exposureTime: parseInt(session.exposure) }] }); - } catch (e) { - errorlog(e); - try { - await track.applyConstraints({ advanced: [{ exposureMode: "manual" }] }); - } catch (e) { - warnlog(e); - } - } - } - if (session.zoom !== false) { - try { - await track.applyConstraints({ advanced: [{ zoom: parseFloat(session.zoom) }] }); - } catch (e) { - errorlog(e); - } - } - if (session.saturation !== false) { - try { - await track.applyConstraints({ advanced: [{ saturation: parseInt(session.saturation) }] }); - } catch (e) { - errorlog(e); - } - } - if (session.sharpness !== false) { - try { - await track.applyConstraints({ advanced: [{ sharpness: parseInt(session.sharpness) }] }); - } catch (e) { - errorlog(e); - } - } - if (session.contrast !== false) { - try { - await track.applyConstraints({ advanced: [{ contrast: parseInt(session.contrast) }] }); - } catch (e) { - errorlog(e); - } - } - if (session.brightness !== false) { - try { - await track.applyConstraints({ advanced: [{ brightness: parseInt(session.brightness) }] }); - } catch (e) { - errorlog(e); - } - } - if (session.focusDistance !== false) { - try { - await track.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: parseInt(session.focusDistance) }] }); - } catch (e) { - errorlog(e); - try { - await track.applyConstraints({ advanced: [{ focusMode: "manual" }] }); - } catch (e) { - warnlog(e); - } - } - } - if (session.mobile) { - if (!(iPad || iOS || Firefox)) { - try { - applySavedVideoSettings(track); - } catch (e) { - errorlog(e); - } - } - } - }); - if (Firefox && !FirefoxEnumerated) { - if (session.streamSrc && session.streamSrc.getTracks().length) { - FirefoxEnumerated = true; - enumerateDevices().then(gotDevices); - } - } - updateRenderOutpipe(); - // senderAudioUpdate - if (wasDisabled && !session.videoMuted) { - var msg = {}; - msg.videoMuted = session.videoMuted; - session.sendMessage(msg); - } - applyMirror(session.mirrorExclude); - session.videoElement.play().then(() => { - log("play doublecheck completed"); - }); - if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) { - if (session.autostart) { - publishWebcam(); - } else { - log("4620"); - if (document.getElementById("gear_webcam")) { - updateStats(obscam); - } - if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").dataset.ready = "true"; - if (document.getElementById("gowebcam").dataset.audioready == "true") { - document.getElementById("gowebcam").disabled = false; - //document.getElementById("gowebcam").innerHTML = getTranslation("start"); - miniTranslate(document.getElementById("gowebcam"), "start"); - document.getElementById("gowebcam").focus(); - } - } - } - } else if (getById("gear_webcam3").style.display === "inline-block") { - updateStats(obscam); - } - // Once crbug.com/711524 is fixed, we won't need to wait anymore. This is - // currently needed because capabilities can only be retrieved after the - // device starts streaming. This happens after and asynchronously w.r.t. - // getUserMedia() returns. - if (grabVideoTimer) { - clearTimeout(grabVideoTimer); - if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) { - session.videoElement.controls = true; - } - } - if (getById("popupSelector_constraints_video")) { - getById("popupSelector_constraints_video").innerHTML = ""; - } - if (getById("popupSelector_constraints_audio")) { - getById("popupSelector_constraints_audio").innerHTML = ""; - } - if (getById("popupSelector_constraints_loading")) { - getById("popupSelector_constraints_loading").style.display = ""; - } - if (iOS || iPad) { - // TEMPORARY: iOS 15.3 beta fix - toggleSpeakerMute(true); - } - if (!(eleName == "previewWebcam" || document.getElementById("previewWebcam"))) { - updateMixer(); // not with the preview, but after. - } - pokeIframeAPI("local-camera-event"); - let grabVideoPostTimeoutValue = 1000; - if (Firefox || session.mobile) { - // wait longer for these; they are more likely to crash if too quick. - grabVideoPostTimeoutValue = 2000; - } - grabVideoTimer = setTimeout( - async function (callback3, gumid) { - if (getUserMediaRequestID !== gumid) { - // new camera selected in this time. - return; - } - makeImages(true); - if (getById("popupSelector_constraints_loading")) { - getById("popupSelector_constraints_loading").style.display = "none"; - } - if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) { - session.videoElement.controls = true; - try { - var track0 = session.streamSrc.getVideoTracks(); - if (track0.length) { - track0 = track0[0]; - if (track0.getCapabilities) { - session.cameraConstraints = track0.getCapabilities(); - } else { - session.cameraConstraints = {}; - } - log(session.cameraConstraints); - if (track0.getSettings) { - session.currentCameraConstraints = track0.getSettings(); - if (screen && screen.orientation && screen.orientation.type) { - if (screen.orientation.type.includes("portrait")) { - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - } else if (window.matchMedia("(orientation: portrait)").matches) { - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - } else { - session.currentCameraConstraints = {}; - } - log(session.currentCameraConstraints); - } - } catch (e) { - errorlog(e); - } - } else if (toggleSettingsState) { - log("16047"); - updateConstraintSliders(); //listCameraSettings(); - } - if (callback3) { - try { - var data = {}; - data.UUID = callback3; - data.videoOptions = listVideoSettingsPrep(); - sendMediaDevices(data.UUID); - session.sendMessage(data, data.UUID); - } catch (e) { } - } - if (iOS || iPad) { - // TEMPORARY: iOS 15.3 beta fix - toggleSpeakerMute(true); - } - if (session.forceAspectRatio) { - await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - } - updateForceRotate(); // this contains session.setResolution(); - if (iOS || iPad) { - // if we don't do this, portrait videos may be detected as horizontal - if (!document.getElementById("previewWebcam")) { - updateMixer(); // not with the preview, but after. - } - } - try { - if (session.pip3) { - if (!eleName.pip) { - eleName.pip = true; - toggleSystemPip(session.videoElement, true); - } - } - } catch (e) { } - // this will reset scaling for all viewers of this stream. I also call it when aspect ratio, width, or height is changed via applyConstraints - dragElement(session.videoElement); - }, - grabVideoPostTimeoutValue, - callback2, - gumID - ); // focus - log("DONE - found stream"); - }) - .catch(function (e) { - if (getUserMediaRequestID !== gumID) { - warnlog("the previously selected camera attempted failed, but not a big deal, since its now void"); - return; - } - warnlog(e); - if (e.name === "OverconstrainedError") { - warnlog(e.message || e); - log("Resolution or frameRate didn't work"); - } else if (e.name === "NotReadableError") { - if (quality <= 10) { - activatedPreview = false; - grabVideo(quality + 1, eleName, selector); - } else if (session.facingMode) { - session.facingMode = false; - activatedPreview = false; - grabVideo(false, eleName, selector); // restart. - } else { - if (!session.cleanOutput) { - if (iOS) { - warnUser("An error occured. Closing existing tabs in Safari may solve this issue."); - } else { - warnUser("Error: Could not start video source.\n\nTypically this means the Camera is already be in use elsewhere. Most webcams can only be accessed by one program at a time.\n\nTry a different camera or perhaps try re-plugging in the device."); - } - } - activatedPreview = true; - if (getById("gowebcam")) { - getById("gowebcam").innerHTML = "Problem with Camera"; - } - } - return; - } else if (e.name === "NavigatorUserMediaError") { - if (getById("gowebcam")) { - getById("gowebcam").innerHTML = "Problem with Camera"; - } - if (!session.cleanOutput) { - warnUser("Unknown error: 'NavigatorUserMediaError'"); - } - return; - } else if (e.name === "timedOut") { - activatedPreview = true; - if (getById("gowebcam")) { - getById("gowebcam").innerHTML = "Problem with Camera"; - } - if (!session.cleanOutput) { - warnUser(e.message); - } - return; - } else { - errorlog("An unknown camera error occured"); - } - if (quality <= 10) { - activatedPreview = false; - grabVideo(quality + 1, eleName, selector); - } else if (session.facingMode) { - session.facingMode = false; - activatedPreview = false; - grabVideo(false, eleName, selector); // restart. - } else { - errorlog("********Camera failed to work"); - activatedPreview = true; - if (getById("gowebcam")) { - getById("gowebcam").innerHTML = "Problem with Camera"; - } - if (!session.cleanOutput) { - if (session.width || session.height || session.frameRate) { - warnUser(" Camera failed to load.\n\nPlease ensure your camera supports the resolution and frameRate that has been manually specified. Perhaps use &quality=0 instead.", false, false); - } else { - warnUser(" Camera failed to load.\n\nPlease make sure it is not already in use by another application.\n\nPlease make sure you have accepted the camera permissions.", false, false); - } - } - } - }); - }, - delayStart, - gumMediaID, - callback - ); - } -} - -function updateRenderOutpipe() { - // video only. - log("updateRenderOutpipe()"); - - if (session.canvasWebGL) { - session.canvasWebGL.remove(); - session.canvasWebGL = null; - } - - if (session.canvasSource) { - session.canvasSource.srcObject.getTracks().forEach(function (trk) { - session.canvasSource.srcObject.removeTrack(trk); - //trk.stop(); - }); - } - - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getVideoTracks().forEach(function (track) { - session.videoElement.srcObject.removeTrack(track); - log("remove ss track 84"); - //track.stop(); - //session.videoElement.load(); - }); - } else { - checkBasicStreamsExist(); - } - - if (session.streamSrc) { - var tracks = session.streamSrc.getVideoTracks(); - - if (!tracks.length || session.videoMuted) { - tracks = setAvatarImage(tracks); - if (tracks.length) { - if (tracks.length && !session.cleanOutput && !session.cleanish) { - getById("mutevideobutton").classList.remove("hidden"); - } - - tracks.forEach(function (track) { - session.videoElement.srcObject.addTrack(track); - if (session.avatar && session.avatar.tracks) { - var msg = {}; - msg.videoMuted = false; // doesn't matter actual mute state, since its the avatar - session.sendMessage(msg); - } else { - toggleVideoMute(true); - } - pushOutVideoTrack(track); // video only - }); - } else { - var msg = {}; - msg.videoMuted = true; - session.sendMessage(msg); - session.videoElement.load(); - getById("mutevideobutton").classList.add("hidden"); - } - } else if (tracks.length) { - applyMirror(session.mirrorExclude || session.screenShareState); - tracks.forEach(function (track) { - track = applyEffects(track); // updates with the correct track session.streamSrc - session.videoElement.srcObject.addTrack(track); - toggleVideoMute(true); - pushOutVideoTrack(track); // video only - }); - - if (tracks.length && !session.cleanOutput && !session.cleanish) { - getById("mutevideobutton").classList.remove("hidden"); - } - } - } -} - -function pushOutVideoTrack(track) { - log("pushOutVideoTrack"); - - pokeIframeAPI("push-video-track", track.id, false, session.streamID); // (action, value = null, UUID = null, SID=null) - - if (session.chunked) { - for (UUID in session.pcs) { - session.chunkedStream(UUID); // I need to update chunkedStream with the current track? If sstype=3, then skip this - } - } - - if (session.audioContentHint && track.kind === "audio") { - // why am I pushing an audio track? - errorlog("this shouldn't occur, since only video tracks are expected"); - try { - track.contentHint = session.audioContentHint; - } catch (e) { - errorlog(e); - } - } - if (session.screenShareState && session.screenshareContentHint && track.kind === "video") { - // I need to check if this is actually a screenshare before setting the hint (sstype=3) - try { - track.contentHint = session.screenshareContentHint; - } catch (e) { - errorlog(e); - } - } else if (session.contentHint && track.kind === "video") { - try { - track.contentHint = session.contentHint; - } catch (e) { - errorlog(e); - } - } - - if (session.whipOut && session.whipOut.getSenders) { - // should only be 0 or 1 video sender, ever. - //var added = false; - session.whipOut.getSenders().forEach(sender => { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track && sender.track.kind == "video") { - warnlog("Replacing track"); - sender.replaceTrack(track); // replace may not be supported by all browsers. eek. - //sender.track.enabled = true; - //added = true; - } - }); - } - - for (UUID in session.pcs) { - var videoAdded = false; - try { - if ("realUUID" in session.pcs[UUID]) { - continue; - } - if (session.chunked && session.pcs[UUID].allowChunked) { - continue; - } - - if (session.pcs[UUID].guest == true && session.roombitrate === 0) { - log("room rate restriction detected. No videos will be published to other guests"); - } else if (session.pcs[UUID].allowVideo == true) { - // allow - - // for any connected peer, update the video they have if connected with a video already. - var added = false; - var senders = getSenders2(UUID); - senders.forEach(sender => { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (added) { - return; - } - if (sender.track && sender.track.kind == "video") { - sender.replaceTrack(track); // replace may not be supported by all browsers. eek. - log("Track replaced"); - log(track); - sender.track.enabled = true; - added = true; - } - }); - if (added == false) { - videoAdded = true; - session.pcs[UUID].addTrack(track, session.videoElement.srcObject); // can't replace, so adding - setTimeout( - function (uuid) { - session.optimizeBitrate(uuid); - }, - session.rampUpTime, - UUID - ); // 3 seconds lets us ramp up the quality a bit and figure out the total bandwidth quicker - } - } - } catch (e) { - errorlog(e); - } - - if (iOS || iPad) { - ///////// THIS IS A FIX FOR iOS 15.4. When a video is loaded (view/push), the bitrate from iOS devices is stuck low, and resolution needs toggle to fix. - // videoAdded value needs to be deleted from above also - if (SafariVersion && SafariVersion <= 13) { - // - } else if (videoAdded) { - setTimeout( - function (uuid) { - session.setScale(uuid, null); - }, - 2000, - UUID - ); - setTimeout( - function (uuid) { - var scale = 100; - session.setScale; - if (session.pcs[uuid].scale) { - scale = session.pcs[uuid].scale; - } - session.setScale(uuid, scale); - }, - 5000, - UUID - ); - } - } - } - if (track.kind === "audio") { - session.applyIsolatedChat(); - } - session.refreshScale(); -} - -async function grabAudio(selector = "#audioSource", trackid = null, override = false, callbackUUID = false, callback = false) { - // trackid is the excluded track , callback is UUID - - if (activatedPreview == true) { - log("activated preview return 2"); - return; - } - activatedPreview = true; - getAudioUserMediaRequestID += 1; - var gumAudioID = getAudioUserMediaRequestID; - log("TRACK EXCLUDED:" + trackid); - - try { - var baseTest = document.querySelector(selector); - if (!baseTest) { - errorlog("No audio source menu"); - return; - } - if (baseTest && baseTest.tagName == "UL") { - var audioSelect = baseTest.querySelectorAll("input"); - var audioExcludeList = []; - for (var i = 0; i < audioSelect.length; i++) { - try { - if ("screen" == audioSelect[i].dataset.type) { - // skip already excluded ---------- !!!!!! DOES THIS MAKE SENSE? TODO: CHECK - if (audioSelect[i].checked) { - audioExcludeList.push(audioSelect[i]); - } - } - } catch (e) { - errorlog(e); - } - } - } else if (baseTest && baseTest.tagName == "SELECT") { - var audioExcludeList = []; - var audioSelect = baseTest.options; - for (var i = 0; i < audioSelect.length; i++) { - try { - if ("screen" == audioSelect[i].dataset.type) { - // skip already excluded ---------- !!!!!! DOES THIS MAKE SENSE? TODO: CHECK - if (audioSelect[i].selected) { - audioExcludeList.push(audioSelect[i]); - } - } - } catch (e) { - errorlog(e); - } - } - } - } catch (e) { - errorlog(e); - } - - try { - if (session.videoElement && session.videoElement.srcObject) { - session.videoElement.srcObject.getAudioTracks().forEach(function (track) { - // TODO: Confirm that I even need this? - for (var i = 0; i < audioExcludeList.length; i++) { - try { - if (audioExcludeList[i].label == track.label) { - warnlog("DONE"); - return; - } - } catch (e) { - errorlog(e); - } - } - if (trackid && track.id == trackid) { - warnlog("SKIPPED EXCLUDED TRACK?"); - return; - } - session.videoElement.srcObject.removeTrack(track); - log("remove ss track67"); - track.stop(); // remove then stop. - }); - } else { - // if no stream exists - checkBasicStreamsExist(); - } - } catch (e) { - errorlog(e); - } - - try { - if (session.streamSrc) { - session.streamSrc.getAudioTracks().forEach(function (track) { - for (var i = 0; i < audioExcludeList.length; i++) { - try { - if (audioExcludeList[i].label == track.label) { - warnlog("EXCLUDING TRACK; PROBABLY SCREEN SHARE"); - return; - } - } catch (e) { - errorlog(e); - } - } - if (trackid && track.id == trackid) { - warnlog("SKIPPED EXCLUDED TRACK?"); - return; - } - session.streamSrc.removeTrack(track); - track.stop(); - }); - } else { - // if no stream exists - checkBasicStreamsExist(); - } - } catch (e) { - errorlog(e); - } - - try { - if (session.streamSrcClone) { - session.streamSrcClone.getAudioTracks().forEach(function (track) { - for (var i = 0; i < audioExcludeList.length; i++) { - try { - if (audioExcludeList[i].label == track.label) { - warnlog("EXCLUDING TRACK; PROBABLY SCREEN SHARE"); - return; - } - } catch (e) { - errorlog(e); - } - } - if (trackid && track.id == trackid) { - warnlog("SKIPPED EXCLUDED TRACK?"); - return; - } - log("remove ss track 55"); - session.streamSrcClone.removeTrack(track); - track.stop(); - }); - } - } catch (e) { - errorlog(e); - } - - var streams = await getAudioOnly(selector, trackid, override, gumAudioID); // Get audio streams - - if (gumAudioID !== getAudioUserMediaRequestID) { - try { - streams.forEach(stream => { - if (!stream) { - return; - } - stream.getTracks().forEach(track => track.stop()); - }); - } catch (e) { } - activatedPreview = false; - return; - } - - try { - log("STREAMS: " + streams.length); - - for (var i = 0; i < streams.length; i++) { - streams[i].getAudioTracks().forEach(function (track) { - try { - if (gumAudioID !== getAudioUserMediaRequestID) { - track.stop(); - return; - } - session.streamSrc.addTrack(track); // add video track to the preview video - - track.onended = handleAudioTrackEnded; // Add event listener for track end - - log("ok?"); - // applySavedAudioSettings(track); ## this doesn't work as echo-cancellation(+) needs to be applied via getuserMedia only. - } catch (e) { - errorlog(e); - } - }); - } - } catch (e) { - errorlog(e); - } - if (Firefox && !FirefoxEnumerated) { - if (session.streamSrc && session.streamSrc.getTracks().length) { - FirefoxEnumerated = true; - enumerateDevices().then(gotDevices); - } - } - - if (callback) { - callback(); - } - - senderAudioUpdate(callbackUUID); - - try { - if (session.streamSrc && session.streamSrc.getVideoTracks && session.streamSrc.getVideoTracks().length) { - var previewVideoCount = 0; - if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks) { - previewVideoCount = session.videoElement.srcObject.getVideoTracks().length; - } - if (!previewVideoCount && typeof updateRenderOutpipe === "function") { - updateRenderOutpipe(); - } - } - } catch (e) { - errorlog(e); - } -} - -session.toggleSoloChat = function (UUID, event = false) { - // ==> applyIsolatedChat -- this should be trigger by the director only I think - - if (session.director) { - if (!session.directorEnabledPPT) { - warnUser("Enable the director's microphone first.", 2000); - return false; - } - } - - if (Firefox) { - warnlog("Solo talk support for Firefox is currently experimental"); - } - - var msg = {}; - msg.micIsolate = false; - - if (session.soloChatUUID.includes(UUID)) { - // already added, so lets toggle off - session.soloChatUUID.splice(session.soloChatUUID.indexOf(UUID), 1); // Toggles. Adds target to soloChatUUID list - msg.lowerVolume = false; - } else { - session.soloChatUUID.push(UUID); //not added, so lets toggle on - msg.lowerVolume = true; - } - - if (event) { - if (event.ctrlKey || event.metaKey) { - if (session.soloChatUUID.includes(UUID)) { - msg.micIsolate = 1; - } - } - } - - session.sendRequest(msg, UUID); - - log(session.soloChatUUID); - - var ele = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + UUID + '"]'); // [data--u-u-i-d="'+UUID+'"] // this all just updates the buttons - log(ele); - - var ret = 0; - - if (session.soloChatUUID.includes(UUID)) { - if (msg.micIsolate) { - ret = 2; - ele.classList.add("altpress"); // we will do this later. - ele.value = 2; - } else { - ret = 1; - ele.value = 1; - } - } else { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - ele.classList.remove("altpress"); - ele.value = 0; - } - - session.applySoloChat(false); - return ret; -}; -/////////////////////// - -session.togglePrivateChat = function (ele) { - var msg = {}; - warnlog(ele); - if (ele.value == 0) { - msg.micIsolate = true; - ele.value = 1; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } else { - msg.micIsolate = false; - ele.value = 0; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } - session.sendRequest(msg, ele.dataset.UUID); - warnlog(msg); -}; - -// we call this via session.applyIsolatedChat, just in case -session.applyIsolatedVolume = function () { - // mutes outbound mic audio; for guests, and not the director - - var i = session.lowerVolume.length; - while (i--) { - if (!(session.lowerVolume[i] in session.rpcs)) { - // clean up dead connections - session.lowerVolume.splice(i, 1); - } - } - - var soloMode = false; - - /* if (!(session.cleanOutput)){ - if (session.lowerVolume.length){ - getById("header").classList.add('orange'); - getById("head6").classList.remove('hidden'); - } else if (session.audioGain === 0){ - // do nothing. - } else { - getById("header").classList.remove('orange'); - getById("head6").classList.add('hidden'); - } - } */ - - if (session.lowerVolume.length) { - soloMode = true; - } - - if (soloMode) { - for (var UUID in session.rpcs) { - if (session.lowerVolume.includes(UUID)) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume !== false) { - // isolated - session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume; - session.rpcs[UUID].savedVolume = false; - } - continue; - } - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume == false) { - // not isolated - session.rpcs[UUID].savedVolume = session.rpcs[UUID].videoElement.volume; - session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume * 0.25; - } - } - } else { - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume !== false) { - // isolated - session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume; - session.rpcs[UUID].savedVolume = false; - } - } - } -}; - -session.applyIsolatedChat = function (UUID = false) { - // mutes outbound mic audio; for guests, and not the director - log("applyIsolatedChat"); - session.applyIsolatedVolume(); // this toggle the speaker output - - var i = session.micIsolated.length; - while (i--) { - if (!(session.micIsolated[i] in session.pcs) && !(session.micIsolated[i] in session.rpcs)) { - session.micIsolated.splice(i, 1); - } - } - - var muteList = [...session.micIsolated]; // one thing I hate about Javascript. Doesn't actually copy arrays. - var soloMode = false; - - if (session.micIsolatedAutoMute) { - // session.micIsolatedAutoMute - soloMode = true; - session.micIsolatedAutoMute.forEach(uid => { - if (!muteList.includes(uid) && (uid in session.rpcs || uid in session.pcs)) { - muteList.push(uid); - } - }); - } - - if (muteList.length) { - soloMode = true; - } - - if (!session.cleanOutput) { - if (soloMode) { - getById("header").classList.add("orange"); - getById("head6").classList.remove("hidden"); - } else if (session.audioGain === 0) { - // do nothing. - } else { - getById("header").classList.remove("orange"); - getById("head6").classList.add("hidden"); - } - } - - ///// - if (session.directorSpeakerMuted !== null) { - for (var uuid in session.rpcs) { - try { - var receivers = getReceivers2(uuid); //session.rpcs[uuid].getReceivers(); - for (var i = 0; i < receivers.length; i++) { - if (receivers[i].track.kind == "audio") { - receivers[i].track.enabled = true; // Chrome 133+ fix: must enable before disabling - receivers[i].track.enabled = !session.directorSpeakerMuted; - } - } - } catch (e) { - errorlog(e); - } - } - if (session.directorSpeakerMuted) { - getById("videosource").muted = true; - } - } - ////////////// - - if (UUID) { - try { - var senders = getSenders2(UUID); - senders.forEach(sender => { - if (!sender.track) { - return; - } - if (sender.track.kind !== "audio") { - return; - } - - var settings = {}; - if (!soloMode) { - settings.active = true; - session.pcs[UUID].audioMutedOverride = false; - } else if (muteList.indexOf(UUID) >= 0) { - settings.active = true; - session.pcs[UUID].audioMutedOverride = false; - } else { - log("MUTING via session.applyIsolatedChat"); - settings.active = false; - session.pcs[UUID].audioMutedOverride = true; - } - setEncodings(sender, settings); - }); - } catch (e) { - errorlog(e); - } - } else { - for (var UUID in session.pcs) { - try { - var senders = getSenders2(UUID); - senders.forEach(sender => { - if (!sender.track) { - return; - } - if (sender.track.kind !== "audio") { - return; - } - - var settings = {}; - if (!soloMode) { - settings.active = true; - session.pcs[UUID].audioMutedOverride = false; - } else if (muteList.indexOf(UUID) >= 0) { - settings.active = true; - session.pcs[UUID].audioMutedOverride = false; - } else { - log("MUTING via session.applyIsolatedChat"); - settings.active = false; - session.pcs[UUID].audioMutedOverride = true; - } - setEncodings(sender, settings); - }); - } catch (e) { - errorlog(e); - } - } - } -}; - -var FirefoxSenders = {}; - -function setEncodings(sender, settings = null, callback = null, cbarg = null) { - if (!settings) { - if (!sender.encodingsQueue) { - // not set - return; - } else if (!sender.encodingsQueue.length) { - // none left - return; - } - } else if (!("encodingsQueue" in sender)) { - sender.encodingsQueue = [[settings, callback, cbarg]]; - } else { - sender.encodingsQueue.push([settings, callback, cbarg]); - } - - if (sender.encodingsQueueActive) { - return; - } - - try { - sender.encodingsQueueActive = true; // we're now busy. - - var options = sender.encodingsQueue.shift(); - settings = options[0]; - callback = options[1]; - cbarg = options[2]; - - const params = sender.getParameters(); - if (!params.encodings || params.encodings.length == 0) { - params.encodings = [{}]; - } - var changed = false; - for (var setting in settings) { - if (settings[setting] === null) { - if (setting in params.encodings[0]) { - delete params.encodings[0][setting]; - changed = true; - } - } else { - if (setting in params.encodings[0]) { - if (params.encodings[0][setting] !== settings[setting]) { - changed = true; - } - } else { - changed = true; - } - params.encodings[0][setting] = settings[setting]; - } - } - - log(settings); - - // if old Firefox, see if I can do something other than Active? - - if (!changed && !Firefox && !SafariVersion) { - log("SET ENCODINGS MATCH INPUT; skipping"); - if (callback) { - if (cbarg) { - setTimeout(function () { - callback(cbarg); - }, 0); - } else { - setTimeout(function () { - callback(); - }, 0); - } - } - sender.encodingsQueueActive = false; - setEncodings(sender); - return; - } - - if (Firefox && !(Firefox >= 110)) { - // https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpEncodingParameters now supported in v110, but old versions will need this function still - if ("active" in settings) { - warnlog("Firefox does not support track active state. We will use enable/disable for that instead."); - if (FirefoxSenders.sender) { - if (FirefoxSenders.sender.lastState === false) { - FirefoxSenders.sender.activeState = settings.active; - // already set to false, so should stay disabled - } else { - FirefoxSenders.sender.activeState = settings.active; - sender.track.enabled = settings.active; // either true or false - } - } else { - FirefoxSenders.sender = { lastState: sender.track.enabled, activeState: settings.active }; - sender.track.enabled = settings.active; - } - - delete settings.active; - if (!Object.keys(settings).length) { - if (callback) { - if (cbarg) { - setTimeout(function () { - callback(cbarg); - }, 0); - } else { - setTimeout(function () { - callback(); - }, 0); - } - } - log("COMPELTED FIREFOX SET ENCODINGS"); - sender.encodingsQueueActive = false; - setEncodings(sender); - return; - } - } - } else if (Firefox) { - // Firefox , all versions, don't support active state with audio yet?? GAhhhhhhhh! - if ("track" in sender && "kind" in sender.track && sender.track.kind == "audio") { - if ("active" in settings) { - warnlog("Firefox does not support track active state with AUDIO yet... We will use enable/disable for that instead."); - if (FirefoxSenders.sender) { - if (FirefoxSenders.sender.lastState === false) { - FirefoxSenders.sender.activeState = settings.active; - // already set to false, so should stay disabled - } else { - FirefoxSenders.sender.activeState = settings.active; - sender.track.enabled = settings.active; // either true or false - } - } else { - FirefoxSenders.sender = { lastState: sender.track.enabled, activeState: settings.active }; - sender.track.enabled = settings.active; - } - - delete settings.active; - if (!Object.keys(settings).length) { - if (callback) { - if (cbarg) { - setTimeout(function () { - callback(cbarg); - }, 0); - } else { - setTimeout(function () { - callback(); - }, 0); - } - } - log("COMPELTED FIREFOX SET ENCODINGS"); - sender.encodingsQueueActive = false; - setEncodings(sender); - return; - } - } - } - } - - sender - .setParameters(params) - .then(() => { - if (callback) { - if (cbarg) { - setTimeout(function () { - callback(cbarg); - }, 0); - } else { - setTimeout(function () { - callback(); - }, 0); - } - } - sender.encodingsQueueActive = false; - setEncodings(sender); - }) - .catch(e => { - errorlog(e); - sender.encodingsQueueActive = false; - setEncodings(sender); - }); - } catch (e) { - errorlog(e); - sender.encodingsQueueActive = false; - } -} - -session.applySoloChat = function (apply = true) { - // mutes outbound mic audio; ;; does the actual solo chat muting for the director - if (session.director === false) { - session.applyIsolatedChat(); - return; - } else if (!session.directorEnabledPPT) { - return; - } - - log("applySoloChat()"); - - var i = session.soloChatUUID.length; - while (i--) { - if (!(session.soloChatUUID[i] in session.pcs)) { - session.soloChatUUID.splice(i, 1); - log("splicing out: " + i); - } - } - - for (var uuid in session.pcs) { - // not sure what to do here wrt to screen tracks - try { - var senders = getSenders2(uuid); - senders.forEach(sender => { - if (!sender.track) { - return; - } - if (sender.track.kind !== "audio") { - return; - } - - var settings = {}; - - if (session.soloChatUUID.length && session.soloChatUUID.includes(uuid)) { - settings.active = true; - setEncodings( - sender, - settings, - function (uid) { - log("2: " + uid); - var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]'); - if (button) { - button.classList.add("pressed"); - button.ariaPressed = "true"; - button.classList.remove("hint"); - } - }, - uuid - ); - } else if (session.soloChatUUID.length == 0) { - settings.active = true; - setEncodings( - sender, - settings, - function (uid) { - log(uid); - var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]'); - if (button) { - button.classList.remove("pressed"); - button.ariaPressed = "false"; - button.classList.remove("hint"); - } - }, - uuid - ); - } else { - settings.active = false; - setEncodings( - sender, - settings, - function (uid) { - warnlog("muted the output to:" + uid); - var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]'); - if (button) { - button.classList.remove("pressed"); - button.ariaPressed = "false"; - button.classList.add("hint"); - } - }, - uuid - ); - } - }); - } catch (e) { - errorlog(e); - } - } - if (apply == false) { - if (session.soloChatUUID.length) { - session.muted_savedState = session.muted; - session.muted = false; - data = {}; - data.muteState = session.muted; - for (var i = 0; i < session.soloChatUUID.length; i++) { - session.sendMessage(data, session.soloChatUUID[i]); - } - } else { - session.muted = session.muted_savedState; - } - toggleMute(true); - } -}; - -function senderAudioUpdate(callbackUUID = false, videoSource = null) { - try { - let tracks = []; - - if (!videoSource) { - checkBasicStreamsExist(); - videoSource = session.videoElement.srcObject; - } - - tracks = videoSource.getAudioTracks(); - - if (session.audioContentHint && tracks.length) { - tracks.forEach(trk => { - try { - trk.contentHint = session.audioContentHint; - } catch (e) { - errorlog(e); - } - }); - } - - if (session.whipOut && session.whipOut.getSenders && tracks.length) { - // mixMinus won't work with meshcast, so don't bother. - session.whipOut.getSenders().forEach(sender => { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track && sender.track.kind == "audio") { - tracks.forEach(trk => { - sender.replaceTrack(trk); - }); - } - }); - } - - for (UUID in session.pcs) { - if ("realUUID" in session.pcs[UUID]) { - continue; - } // do not process the screen share audio - if (session.chunked && session.pcs[UUID].allowChunked && (session.pcs[UUID].allowChunked !== 2)) { - continue; - } - - if (session.pcs[UUID].allowAudio == true) { - var senders = getSenders2(UUID); - if (session.mixMinus) { - log("mixMinus START .."); - var STRM = mixMinusAudio(UUID); - if (!STRM) { - continue; - } - STRM.getAudioTracks().forEach(trk => { - if (session.audioContentHint) { - trk.contentHint = session.audioContentHint; - } - var added = false; - senders.forEach(sender => { - if (added) { - if (sender.track && sender.track.kind == "audio") { - sender.track.enabled = false; - } - return; - } - if (sender.track && sender.track.kind == "audio") { - sender.replaceTrack(trk); - sender.track.enabled = true; - added = true; - warnlog("ADDED 5"); - } - }); - if (added) { - return; - } - session.pcs[UUID].addTrack(trk, STRM); - }); - continue; - } - senders.forEach(sender => { - var good = false; - if (sender.track && sender.track.id && sender.track.kind == "audio") { - tracks.forEach(function (track) { - // audio also - if (track.id == sender.track.id) { - good = true; - } - }); - } else { - // video or something else; ignore it. - return; - } - if (good) { - return; - } - sender.track.enabled = false; - //session.pcs[UUID].removeTrack(sender); // Apparently removeTrack causes renogiation; also kills send/recv. - }); - - if (tracks.length) { - tracks.forEach(function (track) { - var matched = false; - var senders = getSenders2(UUID); - senders.forEach(sender => { - if (sender.track && sender.track.id && sender.track.kind == "audio") { - warnlog(sender.track.id + " " + track.id); - if (sender.track.id == track.id) { - warnlog("MATCHED 1"); - matched = true; - } - } - }); - if (matched) { - return; - } - var added = false; - var senders = getSenders2(UUID); - senders.forEach(sender => { - if (added) { - return; - } - if (sender.track && sender.track.kind == "audio" && sender.track.enabled == false) { - sender.replaceTrack(track); - sender.track.enabled = true; - added = true; - warnlog("ADDED 2"); - } - }); - if (added) { - return; - } - var sender = session.pcs[UUID].addTrack(track, videoSource); - - }); - } else { - var senders = getSenders2(UUID); - senders.forEach(sender => { - if (sender.track && sender.track.kind == "audio") { - sender.track.enabled = false; // (trying this instead) - //session.pcs[UUID].removeTrack(sender); // Apparently removeTrack causes renogiation; also kills send/recv. - } - }); - } - } - } - if (session.director !== false) { - session.applySoloChat(); // mute streams that should be muted if a director - } - session.applyIsolatedChat(); - - try { - if (toggleSettingsState) { - updateConstraintSliders(); - } - } catch (e) { } - - if (callbackUUID) { - try { - var data = {}; - data.UUID = callbackUUID; - data.audioOptions = listAudioSettingsPrep(); - sendMediaDevices(data.UUID); - session.sendMessage(data, data.UUID); - } catch (e) { } - } - - if (session.twilio) { - session.twilio.updateMixer(); - } - } catch (e) { - errorlog(e); - } - - if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").dataset.audioready = true; - if (document.getElementById("gowebcam").dataset.ready && document.getElementById("gowebcam").dataset.ready == "true") { - document.getElementById("gowebcam").disabled = false; - miniTranslate(document.getElementById("gowebcam"), "start"); - document.getElementById("gowebcam").focus(); - } - } -} - -async function press2talk(clean = false) { - var ele = getById("press2talk"); - ele.style.minWidth = "127px"; - ele.style.padding = "7px"; - getById("settingsbutton").classList.remove("hidden"); - - if (!document.getElementById("controls_director") && session.showDirector) { - createDirectorOnlyBox(); - } - - if (session.taintedSession) { - var msg = {}; - msg.virtualHangup = false; - session.sendMessage(msg); - } - - log("DIRECTOR STREAM SETUP"); - - if (getById("press2talk").dataset.enabled == true) { - log("already enabled"); - return; - } - getById("press2talk").dataset.enabled = true; - - if (session.transcript) { - setTimeout(function () { - setupClosedCaptions(); - }, 1000); - } - getById("press2talk").outerHTML = ""; - getById("mutebutton").classList.remove("hidden"); - getById("hangupbutton2").classList.remove("hidden"); - - if (!session.showDirector && session.recordLocal !== false) { - getById("recordLocalbutton").classList.remove("hidden"); - } - - if (session.screenshareType === 3) { - getById("screenshare3button").className = "float"; - getById("screensharebutton").className = "float hidden"; - getById("screenshare2button").className = "float hidden"; - } else if (session.screenshareType === 1) { - getById("screensharebutton").className = "float"; - getById("screenshare3button").className = "float hidden"; - getById("screenshare2button").className = "float hidden"; - } else if (session.screenshareType === 2) { - getById("screenshare2button").className = "float"; - getById("screensharebutton").className = "float hidden"; - getById("screenshare3button").className = "float hidden"; - } else if (session.broadcast === null) { - // sstype=1, since in self-broadcast mode - getById("screensharebutton").className = "float"; - getById("screenshare2button").className = "float hidden"; - getById("screenshare3button").className = "float hidden"; - } else { - // sstype=3, since not in broadcast mode - getById("screensharebutton").className = "float hidden"; - getById("screenshare2button").className = "float hidden"; - getById("screenshare3button").className = "float"; - } - - checkBasicStreamsExist(); - session.videoElement.id = "videosource"; // could be set to UUID in the future - session.videoElement.dataset.menu = "context-menu-video"; - - if (session.streamID) { - session.videoElement.dataset.sid = session.streamID; - } - - // videosource - session.videoElement.muted = true; - session.videoElement.autoplay = true; - session.videoElement.controls = session.showControls || false; - session.videoElement.setAttribute("playsinline", ""); - - if (document.getElementById("videoContainer_director")) { - getById("videoContainer_director").appendChild(session.videoElement); - } else { - getById("miniPerformer").appendChild(session.videoElement); - } - - if (session.screenShareElement && document.getElementById("videoScreenContainer_director")) { - getById("videoScreenContainer_director").appendChild(session.screenShareElement); - } else if (session.screenShareElement) { - getById("miniPerformer").appendChild(session.screenShareElement); - } - - session.videoElement.title = "This is the preview of the Director's audio and video output."; - - session.videoElement.onpause = event => { - // prevent things from pausing; human or other - - if (!(event.ctrlKey || event.metaKey)) { - log("Video paused; auto playing"); - event.currentTarget - .play() - .then(_ => { - log("playing 9"); - }) - .catch(warnlog); - } - }; - - session.videoElement.addEventListener("click", function (e) { - // show stats of video if double clicked - log("click"); - try { - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - - //////////////////////// - - var [menu, innerMenu] = statsMenuCreator(); - - ////////////////////////////////// - - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - printMyStats(innerMenu); - e.stopPropagation(); - - return false; - } - } catch (e) { - errorlog(e); - } - }); - - updatePushId(); - - /* if (session.directorEnabledPPT){ - enumerateDevices().then(gotDevices).then(async function() { - console.log("done"); - toggleSettings(); - }); - - return; - } */ - //await toggleSettings(); - - var constraint = { video: false, audio: true }; - - if (session.videoDevice) { - constraint.video = true; - } - if (session.audioDevice === 0) { - constraint.audio = false; - } - - requestBasicPermissions(constraint, function () { - log("requestBasicPermissions done"); - enumerateDevices() - .then(gotDevices) - .then(async function () { - log("enumerateDevices+gotDevices complete"); - - pokeIframeAPI("director-share", true, false, session.streamID); // director has started publishing; even if no audio/video. - - log("session.directorEnabledPPT: " + session.directorEnabledPPT); - - if (session.directorEnabledPPT) { - return; - } - - if (session.audioDevice !== 0) { - // change from Auto to Selected Audio Device - log("SETTING AUDIO DEVICE!!"); - activatedPreview = false; - await grabAudio("#audioSource3"); - } - - if (session.videoDevice !== 0) { - activatedPreview = false; - if (session.quality !== false) { - await grabVideo(session.quality, "videosource", "#videoSource3"); - } else { - //session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); - await grabVideo(session.roomid ? session.quality_room : session.quality_wb, "videosource", "#videoSource3"); - } - } - - if (session.videoMutedFlag) { - session.videoMuted = true; - toggleVideoMute(true); - } - - session.directorEnabledPPT = true; - toggleMute(true); - - //await toggleSettings(); - - if (session.autorecord || session.autorecordlocal) { - log("AUTO RECORD START"); - setTimeout( - function (v) { - var videoKbps = session.recordDefault; - if (session.recordLocal !== false) { - videoKbps = session.recordLocal; - } - if (document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']")) { - recordLocalVideoToggle(true); - } else if (v.stopWriter || v.recording) { - } else if (v.startWriter) { - v.startWriter(); - } else { - recordLocalVideo(null, videoKbps, v); - } - }, - 2000, - session.videoElement - ); - } - - log("session.seeding: " + session.seeding); - - if (session.seeding) { - setTimeout(function () { +/* + * Copyright (c) 2026 Steve Seguin. All Rights Reserved. + * + * Use of this source code is governed by the APGLv3 open-source license + * that can be found in the LICENSE file in the root of the source + * tree. Alternative licencing options can be made available on request. + * + */ +/*jshint esversion: 6 */ + +///// For the debug output, uncomment this section. +/* +let lastLogTime = performance.now(); // Initialize with the current time +function getTimeStamp() { + const now = performance.now(); + const timeSinceLastLog = now - lastLogTime; + lastLogTime = now; // Update lastLogTime to the current time + return timeSinceLastLog.toFixed(0); // Return time with three decimals for milliseconds +} +function getStackTrace() { + const obj = {}; + Error.captureStackTrace(obj, getStackTrace); + return obj.stack; +} +function getLineNumber() { + const e = new Error(); + const frame = e.stack.split("\n")[3]; // Change the index if needed + const lineNumber = frame.split(":").reverse()[1]; + return lineNumber; +} +function log(msg) { + const timeStamp = getTimeStamp(); + const lineNumber = getLineNumber(); + console.log(`${timeStamp}ms [Line ${lineNumber}]:`, msg); +} +function warnlog(msg) { + const timeStamp = getTimeStamp(); + const lineNumber = getLineNumber(); + console.warn(`${timeStamp}ms [Line ${lineNumber}]:`, msg); +} +function errorlog(msg) { + const timeStamp = getTimeStamp(); + const lineNumber = getLineNumber(); + console.error(`${timeStamp}ms [Line ${lineNumber}]:`, msg); +} +*/ +/////// + +var formSubmitting = true; +var activatedPreview = false; +var screensharesupport = true; +var FirefoxEnumerated = false; + +var Callbacks = []; +var CtrlPressed = false; // global +var MousePressed = false; // global +var AltPressed = false; +var KeyPressedTimeout = 0; +var PPTKeyPressed = false; + +var translation = false; + +var miscTranslations = { + // i can replace this list from time to time from the generated one in blank.json using translate.js + start: "START", + "new-display-name": "Enter a new Display Name for this stream", + "submit-error-report": "Press OK to submit any error logs to VDO.Ninja. Error logs may contain private information.", + "director-redirect-1": "The director wishes to redirect you to the URL: ", + "director-redirect-2": "\n\nPress OK to be redirected.", + "add-a-label": "Add a label", + "audio-processing-disabled": "Audio processing is disabled with this guest. Can't mute or change volume", + "not-the-director": "You are not the director of this room. You will have limited to no control. See &codirector on how to become a co-director.", + "room-is-claimed": "The room is already claimed by someone else.\n\nOnly the first person to join a room is the assigned director.\n\nRefresh after the first director leaves to claim.", + "token-room-is-claimed": "The room is claimed by someone else.\n\nJoin as a guest or co-director instead.", + "room-is-claimed-codirector": "The room is already claimed by someone else.\n\nTrying to join as a co-director...", + "streamid-already-published": "The stream ID you are publishing to is already in use.\n\nPlease try with a different invite link or refresh to retry again.\n\nYou will now be disconnected.", + "streamid-already-published-obvious": "The stream ID you are publishing to is already in use.\n\nPlease consider using a password or a more varied/unique stream ID to avoid this issue.\n\nYou will now be disconnected.", + "director": "Director", + "unknown-user": "Unknown User", + "room-test-not-good": "The room name 'test' is very commonly used and may not be secure.\n\nAre you sure you wish to proceed?", + "load-previous-session": "Would you like to load your previous session's settings?", + "enter-password": "Please enter the password below: \n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)", + "enter-password-2": "Please enter the password below: \n\n(Note: Passwords are case-sensitive.)", + "enter-director-password": "Please enter the director's password:\n\n(Note: Passwords are case-sensitive and you will not be alerted if it is incorrect.)", + "password-incorrect": "The password was incorrect.\n\nRefresh and try again.", + "enter-display-name": "Please enter your display name:", + "enter-new-display-name": "Enter a new Display Name for this stream", + "what-bitrate": "This remote guest will save the recording directly to their local disk.\n\n - The recording can fail, so have backup recordings going!\n\n - This record option does not use Internet bandwidth and offers a high quality recording\n\n - Guests using iPhones, Androids, or Safari will often have issues - bewarned.", + "what-bitrate-gdrive": "This remote guest will save the recording directly to their local disk, as well as send a copy to your Google Drive (recordings folder).\n\n - The recording can fail however, so have backup recordings going!\n\n - Guests using iPhones, Androids, or Safari will often have issues - bewarned.", + "enter-website": "Enter a website URL to share", + "press-ok-to-record": " - Keep this browser tab active when recording.\n\n - This recording option will record to your local download folder.\n\n - Quality may depend on the Internet connection between you and the guest.\n\n - Recordings may possibly fail; have a backup option!", + "no-streamID-provided": "No streamID was provided; one will be generated randomily.\n\nStream ID: ", + "alphanumeric-only": "Info: Only AlphaNumeric characters should be used for the stream ID.\n\nThe offending characters have been replaced by an underscore", + "stream-id-too-long": "The Stream ID should be less than 64 alPhaNuMeric characters long.\n\nWe will trim it to length.", + "share-with-trusted": "Share only with those you trust", + "pass-recommended": "A password is recommended", + "insecure-room-name": "Insecure room name.", + "allowed-chars": "Allowed chars", + "transfer": "transfer", + "armed": "armed", + "transfer-guest-to-room": "Transfer guests to room:\n\n(Please note: rooms must share the same password)", + "transfer-guest-to-url": "Transfer guests to new website URL.\n\nGuests will be prompted to accept unless they are using &consent", + "change-url": "change URL", + "mute-in-scene": "mute in scene", + "unmute-guest": "unmute guest", + "unmute": "unmute", + "undeafen": "undeafen", + "deafen": "deafen guest", + "unblind": "unblind", + "blind": "blind guest", + "mute-guest": "mute guest", + "mute": "mute", + "unhide": "unhide guest", + "hide-guest": "Hide", + "insecure-stream-id": "⚠️ Insecure stream ID detected\n\nIt is strongly advised that a password is used if using a short non-unique stream ID.\n\nThis is just a warning that can be ignored.", + "confirm-disconnect-users": "Are you sure you wish to disconnect these users?", + "confirm-disconnect-user": "Are you sure you wish to disconnect this user?", + "enter-new-codirector-password": "Enter a co-director password to use", + "control-room-co-director": "Control Room: Co-Director", + "volume-control": "Volume control for local playback only", + "signal-meter": "Video packet loss indicator of video preview; green is good, red is bad. Flame implies CPU is overloaded. May not reflect the packet loss seen by scenes or other guests.", + "waiting-for-the-stream": "Waiting for the stream. Tip: Adding &cleanoutput to the URL will hide this spinner, or click to retry, which will also hide it.", + "main-director": "Main Director", + "co-director": "Co-Director", + "share-a-screen": "Share a screen", + "stop-screen-sharing": "Stop screen sharing", + "you-have-been-transferred": "You've been transferred to a different room", + "you-have-been-activated": "The director has now allowed you to see others in the room", + "you-not-yet-activated": "Please wait until the director brings you into the room", + "you-are-no-longer-a-co-director": "You are no longer a co-director as you were transferred.", + "transferred": "Transferred", + "room-changed": "Your room has changed", + "headphones-tip": "Tip: Use headphones to avoid audio echo issues.", + "camera-tip-c922": "Tip: To achieve 60-fps with a C922 webcam, low-light compensation needs to be turned off, exposure set to auto, and 720p used.", + "camera-tip-camlink": "Tip: A Cam Link may glitch green/purple if accessed elsewhere while already in use.", + "samsung-a-series": "Samsung A-series phones may have issues with Chrome; if so, try Firefox Mobile instead or switch video codecs.", + "screen-permissions-denied": "Permission to capture denied. Ensure your browser has screen record system permissions\n\n1.On your Mac, choose Apple menu > System Preferences, click Security & Privacy , then click Privacy.\n2.Select Screen Recording.\n3.Select the checkbox next to your browser to allow it to record your screen.", + "change-audio-output-device": "Audio could not be captured.\n\nIf you need audio, please make sure you have an audio output device available.\n\nSome gaming headsets (ie: Logitech/Corsair) also may need to be set to 2-channel output to work, as surround sound drivers may cause problems", + "prompt-access-request": " is trying to view your stream. Allow them?", + "confirm-reload-user": "Are you sure you wish to reload this user's browser?", + "webrtc-is-blocked": "⚠ This browser has either blocked WebRTC or does not support it.\n\nThis site will not work without it.\n\nDisable any browser extensions or privacy settings that may be blocking WebRTC, or try a different browser.", + "not-clean-session": "Video effects or canvas rendering failed.\n\nCheck to ensure any remotely hosted images are cross-origin allowed.", + "ios-no-screen-share": "Sorry, but your iOS browser does not support screen-sharing.\n\nPlease see this guide for an alternative method to do so.", + "mobile-no-screen-share": "Sorry, your mobile browser does not support screen-sharing.\n\nThe The native apps do offer basic support for it though.", + "no-screen-share-supported": "Sorry, your browser does not support screen-sharing.\n\nPlease use the desktop versions of Firefox or Chrome instead.", + "no-screen-share-supported-firefox": "Sorry, your browser does not support screen-sharing.\n\nYour Firefox settings may be configured to block it or you've accessed the site insecurely.", + "speech-not-suppoted": "⚠ Speech Recognition is not supported by this browser", + "blue-yeti-tip": "Tip: Blue Yeti microphones may experience issues being overly loud. Please see here for a solution or disable auto-gain in VDO.Ninja.", + "sample-rate-too-high": "Your audio playback device has its sample rate set very high. If having audio issues, try using 48-kHz instead.", + "site-not-responsive": "

    Notice: The system cannot be accessed or is currently slow to respond.

    \nIf a routing issue, try adding &proxy to the URL; you can also try https://proxy.vdo.ninja or a VPN if the service is blocked in your country.\n\nIf the main service is down, a backup version is also available here: https://backup.vdo.ninja\n\nContact steve@seguin.email for added help.\n\nThis service requires the use of Websockets over port 443.", + "no-audio-source-detected": "No audio source was detected.

    Please see the documention for a guide on how to capture application-based audio.", + "viewer-count": "Total outbound p2p connections of this remote stream", + "enter-url-for-widget": "Enter a URL for a page to embed as a sidebar", + "director-password": "Enter the main director's password", + "vision-disabled": "The Director has disabled your vision temporarily

    ", + "invalid-remote-code": "Invalid remote control code.\n\nUse the field below to try again with a different passcode.", + "invalid-remote-code-obs": "Invalid remote control code.\n\nThe remote OBS system needs a matching passcode set using &remote.\n\nSee the documentation for help..", + "request-rejected-obs": "The request was rejected.\n\nThe remote OBS system needs a matching passcode set using &remote.\n\nSee the documentation for help.", + "remote-token-rejected": "The remote request failed; the &remote token did not match or the remote user does not allow remote control.", + "remote-control-failed": "The remote control request failed.", + "remote-peer-connected": "Remote peer connected to video stream.\n\nConnection to handshake server being killed on request. This increases security, but the peer will not be able to reconnect automatically on connection failure.\n\nPress OK to start the stream!", + "director-denied": "⚠️ The main director denied you as a co-director\n\nYou will only be able to preview streams; you will not be able to control or change anything.", + "only-main-director": "Only the main director can transfer this guest", + "request-failed": "The request failed; you can't apply this action", + "tokens-did-not-match": "The remote request failed; the remote token did not match or the remote user does not allow remote control.", + "token-not-director": "The request failed; the remote user did not recognize you as the director.\n\nRefreshing may help in some cases, if you are indeed the director.", + "approved-as-director": "The director approved you as a co-director", + "you-are-a-codirector": "You are a co-director of this room; you have partial director control assigned to you.", + "this-is-you": "This is you, a co-director.
    You are also a performer.", + "preview-meshcast-disabled": "You can't adjust the preview bitrate for Meshcast or WHIP-based streams", + "no-network": "Network connection lost 🤷‍♀️❌📶", + "no-network-details": "Network connection lost. 🤷‍♀️❌📶\n\nHave you lost your Internet connection?", + "enter-password-if-desired": "Enter a password if provided, otherwise just click Cancel", + "your-screenshare": "Your screenshare", + "your-camera": "Your camera", + "accept-inbound-caller": "Accept the inbound telephone caller?", + "disable-video": "Disable Video", + "show-more-options": "Show more options", + "system-default": "System Default" +}; + +function getTranslation(key) { + // when using this, instead of miniTranslate, if the user changes the language, it might not update. Used mainly when you don't want any HTML () being including in the translation + if (translation.innerHTML && key in translation.innerHTML) { + // these are the proper translations + return translation.innerHTML[key]; + } else if (key in miscTranslations) { + // i guess these can be transitioned to innerHTML + return miscTranslations[key]; + } else { + warnlog("misc translation not found"); + return key.replaceAll("-", " "); // + } + +} + +// Extract hostname from TURN server URL for QoS tracking +// Handles formats: turn:host:port, turns:host:port, turn:user@host:port, turns:[ipv6]:port +function extractTurnHostnameFromUrl(turnUrl) { + if (!turnUrl || typeof turnUrl !== "string") return null; + var cleaned = turnUrl.replace(/^turns?:/i, ""); + cleaned = cleaned.split("?")[0]; + var atIndex = cleaned.lastIndexOf("@"); + if (atIndex !== -1) cleaned = cleaned.slice(atIndex + 1); + if (cleaned[0] === "[") { + var end = cleaned.indexOf("]"); + return end !== -1 ? cleaned.slice(1, end) : null; + } + return cleaned.split(":")[0] || null; +} + +// Build QoS allowlist from a list of TURN server configurations +function buildQosTurnAllowlist(turnlist) { + var allowlist = []; + if (!turnlist || !Array.isArray(turnlist)) return allowlist; + turnlist.forEach(function(turn) { + var urls = turn.urls || turn.url || []; + if (typeof urls === "string") urls = [urls]; + urls.forEach(function(u) { + var host = extractTurnHostnameFromUrl(u); + if (host && !allowlist.includes(host)) { + allowlist.push(host); + } + }); + }); + return allowlist; +} + +if (typeof session === "undefined") { + // make sure to init the WebRTC if not exists. + var session = WebRTC.Media; + session.streamID = session.generateStreamID(); + errorlog("Serious error: WebRTC session didn't load in time"); +} + +try { + // this is just in case orientationchange gets removed.. + if (!window.onorientationchange && screen.orientation) { + // onorientationchange is deprecated. + window.onorientationchange = function () { + log("screen.orientation triggered.. but nothing linked"); + }; + screen.orientation.addEventListener("change", window.onorientationchange); + } +} catch (e) { + errorlog(e); +} + +function getSlotColor(index) { + + var slotColorPalette = [ + "#00AAAA", + "#FF0000", + "#0000FF", + "#AA00AA", + "#00FF00", + "#AAAA00", + "#AACC44", + "#CCAA44", + "#CC44AA", + "#44AACC" + ]; + + index = parseInt(index); + if (!isFinite(index) || index < 0) { + index = 0; + } + if (!slotColorPalette[index]) { + var hue = (index * 47) % 360; + slotColorPalette[index] = "hsl(" + hue + ", 65%, 55%)"; + } + return slotColorPalette[index]; +} + +function getSlotColorBySlotNumber(slotNumber) { + var slotIndex = parseInt(slotNumber); + if (!isFinite(slotIndex) || slotIndex <= 0) { + return null; + } + return getSlotColor(slotIndex - 1); +} + +function applySlotColor(element, slotNumber) { + if (!element) { + return; + } + var color = getSlotColorBySlotNumber(slotNumber); + if (color) { + element.style.background = color; + element.style.backgroundColor = color; + } else { + element.style.removeProperty("background"); + element.style.removeProperty("background-color"); + } +} + +function populateSlotPicker(maxSlots) { + var picker = document.getElementById("slotPicker"); + if (!picker) { + return; + } + + var html = "

    Assign to slot:


    "; + html += '
    Unset
    '; + for (var i = 1; i <= session.maxAvailableSlots; i++) { + var color = getSlotColor(i - 1); + html += '
    Slot ' + i + "
    "; + } + picker.innerHTML = html; +} + +function positionAlertModalNearEvent(modal, event) { + if (!modal || !event) { + return; + } + + try { + var margin = 16; + var scrollX = window.pageXOffset || document.documentElement.scrollLeft || 0; + var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0; + var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0; + var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; + + modal.classList.add("alertModal--anchored"); + modal.style.transform = "none"; + + var modalWidth = modal.offsetWidth || 0; + var modalHeight = modal.offsetHeight || 0; + + var desiredLeft = event.pageX - modalWidth / 2; + var desiredTop = event.pageY + 24; + + if (!isFinite(desiredLeft)) { + desiredLeft = scrollX + margin; + } + if (!isFinite(desiredTop)) { + desiredTop = scrollY + margin; + } + + var maxLeft = scrollX + viewportWidth - modalWidth - margin; + var maxTop = scrollY + viewportHeight - modalHeight - margin; + + modal.style.left = Math.max(scrollX + margin, Math.min(maxLeft, desiredLeft)) + "px"; + modal.style.top = Math.max(scrollY + margin, Math.min(maxTop, desiredTop)) + "px"; + } catch (err) { + errorlog(err); + } +} + +(function (w) { + w.URLSearchParams = + w.URLSearchParams || + function (searchString) { + var self = this; + searchString = searchString.replace("??", "?"); + self.searchString = searchString; + self.get = function (name) { + var results = new RegExp("[?&]" + name + "=([^&#]*)").exec(self.searchString); + if (results == null) { + return null; + } else { + return decodeURI(results[1]) || 0; + } + }; + }; +})(window); + +var urlEdited = window.location.search.replace(/\?\?/g, "?"); +urlEdited = urlEdited.replace(/\?/g, "&"); +urlEdited = urlEdited.replace(/\&/, "?"); +var urlParams = new URLSearchParams(urlEdited); + +if (urlParams.has("invite") || urlParams.has("i") || urlParams.has("code")) { + session.decodeInvite(urlParams.get("invite") || urlParams.get("i") || urlParams.get("code")); +} else if (urlParams.has("preset")) { + try { + let preset = urlParams.get("preset") || "1"; // default to preset 1 if none provided + let xhttp = new XMLHttpRequest(); + xhttp.open("GET", "presets.json", false); // blocking + xhttp.setRequestHeader("Content-Type", "application/json"); // expecting json response + xhttp.send(); + if (xhttp.status === 200) { + const response = JSON.parse(xhttp.responseText); + let presetString = ""; + + if (Array.isArray(response)) { + let index = (parseInt(preset) || 1) - 1; + presetString = response[index]; + } else if (typeof response === "object" && response !== null) { + presetString = response[preset]; + } + if (!presetString.startsWith("?") || presetString.startsWith("&")) { + presetString = "?" + presetString; + } + + session.preset = presetString; + + let newURL = presetString + "&" + urlParams.toString(); + newURL = newURL.replace(/\?/g, "&"); + newURL = newURL.replace(/\&/, "?"); + urlParams = new URLSearchParams(newURL); + + if (urlParams.has("invite") || urlParams.has("i") || urlParams.has("code")) { + session.decodeInvite(urlParams.get("invite") || urlParams.get("i") || urlParams.get("code")); + } + } else { + errorlog(xhttp.statusTex); + } + } catch (error) { + errorlog(error); + } +} + +if (session.decrypted) { + session.decrypted = session.decrypted + urlEdited.replace("?", "&"); + session.decrypted = session.decrypted.replace(/\?/g, "&"); + session.decrypted = session.decrypted.replace(/\&/, "?"); + urlParams = new URLSearchParams(session.decrypted); + //session.decrypted = true; +} else if (urlEdited !== window.location.search) { + warnlog(window.location.search + " changed to " + urlEdited); + if (!session.nohistory) { + window.history.pushState({ path: urlEdited.toString() }, "", urlEdited.toString()); + } +} +delete urlEdited; + +var isIFrame = false; +if (parent && window.location !== window.parent.location) { + isIFrame = true; +} + +function mapToAll(targets, callback, parentElement = document) { + // js helper + if (!targets) { + return; + } + if (!parentElement) { + return; + } + const target = parentElement.querySelectorAll(targets); + for (let i = 0; i < target.length; i++) { + callback(target[i]); + } +} + +function changeParam(url, paramName, paramValue) { + paramName = paramName.replace("?", ""); + var qind = url.indexOf("?"); + url = url.replace("?", "&"); + var params = url.substring(qind + 1).split("&"); + var query = ""; + var match = false; + for (var i = 0; i < params.length; i++) { + var tokens = params[i].split("="); + var name = tokens[0]; + var value = ""; + if (tokens.length > 1 && tokens[1] !== "") { + value = tokens[1]; + } + + if (name == paramName) { + if (match) { + continue; + } // already matched the first time. + match = true; + value = paramValue; + } + if (value !== "") { + value = "=" + value; + } + + if (query == "") { + query = "?" + name + value; + } else { + query = query + "&" + name + value; + } + } + return url.substring(0, qind) + query; +} + +function saveRoom(ele) { + //this.title = "Quick load settings stored locally"; + session.sticky = true; + ele.parentNode.removeChild(ele); + setStorage("permission", "yes"); + setStorage("settings", encodeURI(window.location.href), 999); +} + +function updateURL(param, force = false, cleanUrl = false) { + if (session.decrypted) { + return; + } + + param = param.replace("?", ""); + var para = param.split("="); + if (cleanUrl) { + if (history.pushState) { + var href = new URL(cleanUrl); + if (para.length == 1) { + href = changeParam(cleanUrl, para[0], ""); + } else { + href = changeParam(cleanUrl, para[0], para[1]); + } + log("--" + href.toString()); + if (!session.nohistory) { + window.history.pushState({ path: href.toString() }, "", href.toString()); + } + } + } else if (!urlParams.has(para[0])) { + // don't need to replace as it doesn't exist. + if (history.pushState) { + var href = window.location.href; + href = href.replace("??", "?"); + var arr = href.split("?"); + var newurl; + if (arr.length > 1 && arr[1] !== "") { + newurl = href + "&" + param; + } else { + newurl = href + "?" + param; + } + if (!session.nohistory) { + window.history.pushState({ path: newurl.toString() }, "", newurl.toString()); + } + } + } else if (force) { + if (history.pushState) { + var href = new URL(window.location.href); + if (para.length == 1) { + href = changeParam(window.location.href, para[0], ""); + } else { + href = changeParam(window.location.href, para[0], para[1]); + } + log("---" + href.toString()); + if (!session.nohistory) { + window.history.pushState({ path: href.toString() }, "", href.toString()); + } + } + } + if (session.sticky) { + setStorage("settings", encodeURI(window.location.href), 999); + } + + urlParams = new URLSearchParams(window.location.search); + + if (session.preset) { + let newURL = session.preset + "&" + urlParams.toString(); + newURL = newURL.replace(/\?/g, "&"); + newURL = newURL.replace(/\&/, "?"); + urlParams = new URLSearchParams(newURL); + } +} + +/* function changeGuestSettings(ele){ + var eles = ele.querySelectorAll('[data-param]'); + var UUID = ele.dataset.UUID; + var settings = {}; + for (var i = 0;i< eles.length; i++){ + if (eles[i].tagName.toLowerCase() == "input"){ + if (eles[i].checked===true){ + settings[eles[i].dataset.param] = true; + } else if (eles[i].checked===false){ + settings[eles[i].dataset.param] = false; + } else { + settings[eles[i].dataset.param] = eles[i].value; + } + } + } + warnlog(settings); + + if (!settings.changepassword){ + delete settings.password; + } + + delete settings.changepassword; + + if (!settings.changeroom){ + // send Migration message + delete settings.roomid; + } + delete settings.roomid; + delete settings.changeroom; + + warnlog(UUID); + var msg = {}; + msg.changeParams = settings; + session.sendRequest(msg, UUID); + closeModal(); +} */ + +// proper room migration needs to happen; in sync. +// updateMixer after settings changed +// password needs to be special cased +// room shouldn't be sent + +function applyNewParams(changeParams) { + for (var key in changeParams) { + session[key] = changeParams[key]; + log(key); + } + log(changeParams); + updateMixer(); +} + +function submitDebugLog(msg = false) { + try { + if (navigator.userAgent) { + var _, + userAgent = navigator.userAgent; + appendDebugLog({ userAgent: userAgent }); + } + if (navigator.platform) { + appendDebugLog({ userAgent: navigator.platform }); + } + } catch (e) { } + window.focus(); + var res = confirm(getTranslation("submit-error-report")); + if (res) { + var request = new XMLHttpRequest(); + + var recordResults = session.streamID + "_" + parseInt(Date.now()); + request.open("POST", "https://reports.vdo.ninja/?name=" + recordResults); // php, well, whatever. + if (!session.cleanOutput) { + warnUser("Report any details of your bug report to steve@seguin.email, along with the following link: https://reports.vdo.ninja/?name=" + recordResults + "", false, false); + } + console.log("Report any details of your bug report to steve@seguin.email, along with the following ID: " + recordResults); + + request.send(JSON.stringify(errorReport)); + errorReport = []; + if (document.getElementById("reportbutton")) { + getById("reportbutton").classList.add("hidden"); + } + } +} + +function URLFromFiles(files) { + const promises = files.map(file => fetch(file).then(response => response.text())); + + return Promise.all(promises).then(texts => { + const text = texts.join(""); + const blob = new Blob([text], { type: "application/javascript" }); + + return URL.createObjectURL(blob); + }); +} + +function detectCPUSupport() { + let cpuThreads = navigator.hardwareConcurrency; + if (cpuThreads) { + return cpuThreads + " threads"; + } + return false; +} + +function detectGPUSupport() { + try { + const gl = document.createElement("canvas").getContext("webgl"); + + if (!gl) { + return false; + } + + if (!Firefox) { + try { + const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); // chrome + if (debugInfo) { + return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); + } + } catch (e) { } + } + + try { + return gl.getParameter(gl.RENDERER) || false; // firefox + } catch (e) { } + } catch (e) { } + return false; +} + +function isOperaGX() { + return (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(" OPR/75") >= 0; +} + +function isSamsungASeries() { + return navigator.userAgent.includes("; SM-A") || false; +} + +function getChromiumVersion() { + var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + return raw ? parseInt(raw[2], 10) : false; +} + +function getiOSVersion() { + try { + var agent = navigator.userAgent; + var start = agent.indexOf("OS "); + if ((agent.indexOf("iPhone") > -1 || agent.indexOf("iPad") > -1) && start > -1) { + return window.Number(agent.substr(start + 3, 3).replace("_", ".")); + } + return 0; + } catch (e) { + return 0; + } + return 0; +} + +function safariVersion() { + var ver = 0; + try { + ver = navigator.appVersion.split("Version/"); + if (ver.length > 1) { + ver = ver[1].split(" Safari"); + } + if (ver.length > 1) { + ver = ver[0].split("."); + } + if (ver.length > 1) { + ver = parseInt(ver[0]); + } else { + ver = 0; + } + } catch (e) { + return 0; + } + return ver; +} + +function isIntelMac() { + // Check if it's a Mac but not Apple Silicon + if (macOS && navigator.userAgent.indexOf("Intel") >= 0) { + return true; + } + return false; +} + +function judgePerformance() { + try { + if (SafariVersion && SafariVersion >= 17 && (iOS || iPad)) { // iphone xr or newer + return 0; + } + + const cores = typeof navigator.hardwareConcurrency === 'number' ? navigator.hardwareConcurrency : 0; + + if (isIntelMac()) { + if (cores < 6) { // yes. they are that bad. + return 2; + } else { + return 1; + } + } + + if (session.mobile && (cores >= 4)) { // assume hardware encoded acceleration + return 0; + } + + if (!cores) { + return 1; + } else if (cores < 4) { + return 2; + } else if (cores > 8) { + return 0; + } + return 1 + } catch (e) { + return 1; // 99% safe default + } +} + +try { + var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); // used by main.js also + var iPad = navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform); + + var macOS = navigator.userAgent.indexOf("Mac OS X") != -1; + macOS = macOS && !(iOS || iPad); + var Firefox = navigator.userAgent.indexOf("Firefox") >= 0; + if (Firefox) { + Firefox = parseInt(navigator.userAgent.split("irefox/").pop()) || true; + } + var Android = navigator.userAgent.toLowerCase().indexOf("android") > -1; //&& ua.indexOf("mobile"); + var ChromiumVersion = getChromiumVersion(); + var OperaGx = isOperaGX(); + var SafariVersion = safariVersion() || getiOSVersion(); // I should rename this to webkit + + if (iOS || iPad) { + // iOS doesn't yet allow actual browsers, cause it's abusing its duopoly. + if (SafariVersion) { + if (Firefox) { + Firefox = false; // I should rename this to gecko + } + if (ChromiumVersion) { + ChromiumVersion = false; // I should rename this to chromium + } + } + } + var SamsungASeries = isSamsungASeries(); + var isVingester = navigator.userAgent.indexOf("Vingester") >= 0; + + var gpgpuSupport = detectGPUSupport(); // graphics ; not supported on ios + log(gpgpuSupport); + + var cpuSupport = detectCPUSupport(); // thread count ; supported on ios + log(cpuSupport); + + var iPhone12Up = false; + + var isMELD = false; + if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.includes("Meld/")) { + isMELD = true; + } + + if (iOS && !iPad) { + if (window.devicePixelRatio.toFixed(2) >= 3 && window.screen.height > 800 && window.screen.width != 414) { + // for reference, https://www.ios-resolution.com/ + iPhone12Up = true; // iPhone SE is left out. + } + } + session.quality_wb = judgePerformance(); // try to estimate what resolution to use for encoding when not in a room. + + if (session.quality_room < session.quality_wb) { + session.quality_room = session.quality_wb; + } + +} catch (e) { + errorlog(e); +} +const needsLegacyWakeLock = () => { + try { + if ("wakeLock" in navigator) { + if (Firefox) { + return true; + } + if ((iOS || iPad) && SafariVersion < 16.4) { + return true; + } + if (typeof session !== "undefined") { + if (session.forceLegacyWakeLock) { + return true; + } + if (session.wakeLockActive) { + return false; + } + } + return true; + } + } catch (e) { } + + return true; // No Wake Lock API or no active lock; need legacy keep alive for mobile +}; + +function removeLegacyKeepAlivePlayer() { + const keepAlive = document.getElementById("keepAlivePlayer"); + if (keepAlive) { + keepAlive.remove(); + } +} + +function ensureLegacyKeepAlivePlayer(ignoreVideoPresence = false) { + if (typeof session === "undefined" || !session.mobile) { + removeLegacyKeepAlivePlayer(); + return false; + } + + if (!needsLegacyWakeLock()) { + removeLegacyKeepAlivePlayer(); + return false; + } + + if (!session.streamSrc) { + return true; + } + + try { + if ( + !ignoreVideoPresence && + session.streamSrc.getVideoTracks && + session.streamSrc.getVideoTracks().length + ) { + removeLegacyKeepAlivePlayer(); + return false; + } + } catch (e) { + errorlog(e); + } + + if (!document.getElementById("keepAlivePlayer")) { + let fakeElement = document.createElement("video"); + fakeElement.autoplay = true; + fakeElement.loop = true; + fakeElement.muted = true; + fakeElement.src = "./media/micro.mp4"; + fakeElement.style.width = "1px"; + fakeElement.style.height = "1px"; + fakeElement.controls = false; + fakeElement.id = "keepAlivePlayer"; + getById("main").appendChild(fakeElement); + } + return true; +} + +function startLegacyKeepAliveLoop(ignoreVideoPresence = false) { + if (typeof session === "undefined" || !session.mobile) { + return; + } + + if (session.keepAliveInterval) { + if (ignoreVideoPresence) { + session.keepAliveIgnoreVideo = true; + } + return; + } + + session.keepAliveIgnoreVideo = !!ignoreVideoPresence; + + const runner = () => { + const keepRunning = ensureLegacyKeepAlivePlayer(session.keepAliveIgnoreVideo); + if (!keepRunning) { + clearInterval(session.keepAliveInterval); + session.keepAliveInterval = null; + session.keepAliveIgnoreVideo = false; + } + }; + + const shouldContinue = ensureLegacyKeepAlivePlayer(session.keepAliveIgnoreVideo); + if (!shouldContinue) { + session.keepAliveIgnoreVideo = false; + return; + } + + session.keepAliveInterval = setInterval(runner, 4000); +} + +if (session.audioCtx && session.audioCtx.sampleRate && session.audioCtx.sampleRate > 192000) { + console.warn("Your audio playback device has a very high sample-rate set of " + session.audioCtx.sampleRate + "-Hz. If having audio problems, lower to at least 192000-Hz, but preferably 48000-Hz."); + if (!session.cleanOutput) { + miniTranslate(getById("audioTipContextSR"), "sample-rate-too-high"); + getById("audioTipSR").classList.remove("hidden"); + } +} + +if (isVingester) { + console.warn("If Vingester isn't able to capture audio, get a fixed version of Vingester from here: https://github.com/steveseguin/vingester/releases/"); +} + +function isAlphaNumeric(str) { + var code, i, len; + for (i = 0, len = str.length; i < len; i++) { + code = str.charCodeAt(i); + if ( + !(code > 47 && code < 58) && // numeric (0-9) + !(code > 64 && code < 91) && // upper alpha (A-Z) + !(code > 96 && code < 123) + ) { + // lower alpha (a-z) + return false; + } + } + return true; +} + +function convertStringToArrayBufferView(str) { + var bytes = new Uint8Array(str.length); + for (var iii = 0; iii < str.length; iii++) { + bytes[iii] = str.charCodeAt(iii); + } + return bytes; +} + +function toHexString(byteArray) { + return Array.prototype.map + .call(byteArray, function (byte) { + return ("0" + (byte & 0xff).toString(16)).slice(-2); + }) + .join(""); +} +function toByteArray(hexString) { + var result = []; + for (var i = 0; i < hexString.length; i += 2) { + result.push(parseInt(hexString.substr(i, 2), 16)); + } + return new Uint8Array(result); +} + +function playAllVideos() { + if (session.firstPlayTriggered && session.audioCtx.state == "suspended") { + // added oct 9th 2022 + try { + session.audioCtx.resume(); + } catch (e) { + warnlog(e); + } + } + + for (var i in session.rpcs) { + if (session.rpcs[i].whip) { + continue; + } + try { + if (session.rpcs[i].videoElement) { + log("I: " + i); + if (session.rpcs[i].videoElement.paused) { + setTimeout( + function (UUID) { + session.rpcs[UUID].videoElement + .play() + .then(_ => { + log("playing 3 "); + if (session.audioEffects === true || session.pushLoudness) { + log("updateIncomingAudioElement('" + UUID + "')"); + updateIncomingAudioElement(UUID); + } + }) + .catch(errorlog); + }, + 0, + i + ); + } else if (session.audioEffects === true || session.pushLoudness) { + updateIncomingAudioElement(i); + log("updateIncomingAudioElement('" + i + "')"); + } + } + } catch (e) { + errorlog(e); + } + } +} + +var videoElements = Array.from(document.querySelectorAll("video")); +var audioElements = Array.from(document.querySelectorAll("audio")); +var mediaStreamCounter = 0; + +function createMediaStream() { + mediaStreamCounter += 1; + return new MediaStream(); +} + +var deleteOldMediaTimeout = null; +function deleteOldMedia(timed = false) { + if (!timed) { + if (!deleteOldMediaTimeout) { + deleteOldMediaTimeout = setTimeout(function () { + deleteOldMediaTimeout = null; + deleteOldMedia(true); + }, 2000); + } + return; + } + + log("CHECKING FOR OLD MEDIA"); + var i = videoElements.length; + while (i--) { + //if ((videoElements[i].id == "videosource") || (videoElements[i].id == "previewWebcam")){continue;} // exclude this one, for safety reasons. (Also, iOS safari blanks the video if streams are detached and moved between video elements) + if (videoElements[i].isConnected === false) { + if (videoElements[i].srcObject == null || (videoElements[i].srcObject && videoElements[i].srcObject.active === false)) { + if (videoElements[i].dataset && videoElements[i].dataset.UUID) { + if (videoElements[i].dataset.UUID in session.rpcs) { + continue; + } // still active, so lets not delete it. + } + videoElements[i].pause(); + videoElements[i].removeAttribute("id"); + videoElements[i].removeAttribute("src"); // empty source + videoElements[i].load(); + videoElements[i].remove(); + videoElements[i] = null; + videoElements.splice(i, 1); + } + } + } + i = audioElements.length; + while (i--) { + if (audioElements[i].isConnected === false) { + if (audioElements[i].srcObject == null || (audioElements[i].srcObject && audioElements[i].srcObject.active === false)) { + if (audioElements[i].dataset && audioElements[i].dataset.UUID) { + if (audioElements[i].dataset.UUID in session.rpcs) { + continue; + } // still active, so lets not delete it. + } + audioElements[i].pause(); + audioElements[i].id = null; + audioElements[i].removeAttribute("src"); // empty source + audioElements[i].load(); + audioElements[i].remove(); + audioElements[i] = null; + audioElements.splice(i, 1); + } + } + } +} + +function createAudioElement() { + try { + deleteOldMedia(); + } catch (e) { + errorlog(e); + } + var a = document.createElement("audio"); + audioElements.push(a); + return a; +} + +function compare_deltas(a, b) { + var aa = a.delta || 0; + var bb = b.delta || 0; + if (aa > bb) { + return 1; + } + if (aa < bb) { + return -1; + } + return 0; +} + +async function fetchWithTimeout(URL, timeout = 8000) { + // ref: https://dmitripavlutin.com/timeout-fetch-request/ + try { + const controller = new AbortController(); + const timeout_id = setTimeout(() => controller.abort(), timeout); + const response = await fetch(URL, { ...{ timeout: timeout }, signal: controller.signal }); + clearTimeout(timeout_id); + return response; + } catch (e) { + errorlog(e); + return await fetch(URL); // iOS 11.x/12.0 + } +} + +function createVideoElement() { + try { + deleteOldMedia(); + } catch (e) { + errorlog(e); + } + var v = document.createElement("video"); + videoElements.push(v); + if (typeof session.volume == "number") { + v.volume = session.volume; // setting default volume + log("setting volume to manual"); + } + + return v; +} + +function getTimezone() { + if (session.tz !== false) { + return session.tz; + } + const stdTimezoneOffset = () => { + var jan = new Date(0, 1); + var jul = new Date(6, 1); + return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()); + }; + var today = new Date(); + const isDstObserved = today => { + return today.getTimezoneOffset() < stdTimezoneOffset(); + }; + if (isDstObserved(today)) { + return today.getTimezoneOffset() + 60; + } else { + return today.getTimezoneOffset(); + } +} + +function promptUser(eleId, UUID = null) { + if (session.beepToNotify) { + playtone(); + } + if (document.getElementById("modalBackdrop")) { + getById("promptModal").innerHTML = ""; // Delete modal + getById("promptModal").remove(); + getById("modalBackdrop").innerHTML = ""; // Delete modal + getById("modalBackdrop").remove(); + } + + zindex = document.querySelectorAll("#promptModal").length + document.querySelectorAll(".alertModal").length; + modalTemplate = `
    +
    + × + +
    +
    +
    `; + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + + getById("promptModalMessage").innerHTML = getById(eleId).innerHTML; + miniTranslate(getById("promptModal")); + + if (UUID) { + getById("promptModalMessage").dataset.UUID = UUID; + } + + document.getElementById("modalBackdrop").addEventListener("click", closeModal); + + getById("promptModal").addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); +} + +async function delay(ms) { + return await new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); +} + +var Prompts = {}; +async function promptAlt(inputText, block = false, asterix = false, value = false, time = false, recording = false, hotkey = false, field = null) { + var result = null; + if (session.beepToNotify) { + playtone(); + } + await new Promise((resolve, reject) => { + var promptID = "pid_" + Math.random().toString(36).substr(2, 9); + Prompts[promptID] = {}; + Prompts[promptID].resolve = resolve; + Prompts[promptID].reject = reject; + + var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; + + if (block) { + var backdropClass = "opaqueBackdrop"; + } else { + var backdropClass = "modalBackdrop"; + } + + inputText = "" + inputText.replace("\n", "
    ") + ""; + inputText = inputText.replace(/\n/g, "
    "); + var type = "text"; + if (asterix) { + type = "password"; + } + + if (time) { + modalTemplate = ` +
    `; + } else if (recording) { + modalTemplate = ` +
    `; + } else if (field && field.type === "file") { + modalTemplate = ` +
    `; + } else if (hotkey) { + modalTemplate = ` +
    `; + } else if (field && field.type === "select") { + modalTemplate = ` +
    `; + } else if (field && field.placeholder) { + modalTemplate = ` +
    `; + } else { + modalTemplate = ` +
    `; + } + + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + + // Only add select listener if we have a select field + if (field && field.type === "select") { + document.getElementById(`select_${promptID}`).addEventListener("change", (e) => { + const input = document.getElementById(`input_${promptID}`); + if (e.target.value === "[Custom]") { + input.style.display = "block"; + input.focus(); + } else { + input.style.display = "none"; + result = e.target.value; + } + }); + } + + document.getElementById("input_" + promptID).focus(); + + if (value !== false) { + if (time) { + document.getElementById("input_" + promptID).value = parseInt(value / 60); + document.getElementById("input_" + promptID + "_sec").value = parseInt(value) % 60; + } else { + document.getElementById("input_" + promptID).value = value; + } + } + + if (time) { + document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { + if (event.key === "Enter") { + document.getElementById("input_" + promptID + "_sec").focus(); + } + }); + document.getElementById("input_" + promptID + "_sec").addEventListener("keyup", function (event) { + if (event.key === "Enter") { + document.getElementById("submit_" + promptID).focus(); + } + }); + document.getElementById("countup_" + promptID).addEventListener("click", function (event) { + if (document.getElementById("countup_" + promptID).checked) { + document.getElementById("input_" + promptID).disabled = true; + document.getElementById("input_" + promptID + "_sec").disabled = true; + } else { + document.getElementById("input_" + promptID).disabled = false; + document.getElementById("input_" + promptID + "_sec").disabled = false; + delete document.getElementById("input_" + promptID).disabled; + delete document.getElementById("input_" + promptID + "_sec").disabled; + } + }); + } else if (field && field.type === "file") { + document.getElementById(`input_${promptID}`).addEventListener("change", async (e) => { + const file = e.target.files[0]; + if (file) { + result = file; + if (file.type.startsWith('image/')) { + const preview = document.getElementById(`preview_${promptID}`); + const reader = new FileReader(); + reader.onload = (e) => { + preview.innerHTML = ``; + }; + reader.readAsDataURL(file); + } + } + }); + } else { + document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { + if (event.key === "Enter") { + var pid = event.target.dataset.pid; + if (field && field.type === "select") { + const selectEl = document.getElementById(`select_${pid}`); + if (selectEl.value === "[Custom]") { + result = document.getElementById(`input_${pid}`).value; + } else { + result = selectEl.value; + } + } else { + result = document.getElementById("input_" + pid).value; + } + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + } + }); + } + + try { + document.getElementById("submit_" + promptID).addEventListener("click", async function (event) { + var pid = event.target.dataset.pid; + if (time) { + result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60; + if (document.getElementById("countup_" + promptID).checked) { + result = 0; + } + } else if (field && field.type === "select") { + const selectEl = document.getElementById(`select_${pid}`); + if (selectEl.value === "[Custom]") { + result = document.getElementById(`input_${pid}`).value; + } else { + result = selectEl.value; + } + } else if (field && field.type === "file") { + if (result) { + await handleImageUpload(result, field); + } + } else { + result = document.getElementById("input_" + pid).value; + } + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + } catch (e) { } + + try { + document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + } catch (e) { } + + try { + document.getElementById("close_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + } catch (e) { } + + getById("modal_" + promptID).addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + miniTranslate(getById("modal_" + promptID)); + return; + }); + return result; +} + +async function promptRecordingOptions(inputText, block = false, defaultOptions = {}) { + var result = null; + // console.log(defaultOptions); + var defaultVideoBitrate = 6000; + var defaultAudioBitrate = 80; + + if (defaultOptions.audioOnly && !defaultOptions.usePCM) { + defaultAudioBitrate = defaultOptions.bitrate || 80; + } else if (!defaultOptions.audioOnly) { + defaultVideoBitrate = defaultOptions.bitrate || 6000; + } + + if (session.beepToNotify) { + playtone(); + } + + // Helper functions for bitrate controls + function updateBitrateControls(promptID) { + const audioOnly = document.getElementById(`audioOnly_${promptID}`).checked; + const usePCM = document.getElementById(`usePCM_${promptID}`).checked; + const pcmContainer = document.getElementById(`audioFormatContainer_${promptID}`); + const slider = document.getElementById(`bitrateSlider_${promptID}`); + const value = document.getElementById(`bitrateValue_${promptID}`); + const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`); + + // PCM option always available + pcmContainer.style.opacity = '1'; + pcmContainer.style.pointerEvents = 'auto'; + bitrateLabel.parentNode.style.opacity = "1"; + bitrateLabel.parentNode.style.pointerEvents = 'auto'; + //document.getElementById(`usePCM_${promptID}`).disabled = false; + + // Update bitrate controls based on mode + if (audioOnly && usePCM) { + // Audio-only PCM mode + bitrateLabel.textContent = "Uncompressed PCM16 audio"; + bitrateLabel.parentNode.style.opacity = "0.5"; + bitrateLabel.parentNode.style.pointerEvents = 'none'; + slider.disabled = true; + value.disabled = true; + slider.value = ""; + value.value = ""; + } else if (audioOnly && !usePCM) { + // Audio-only OPUS mode + bitrateLabel.textContent = "Audio recording bitrate (OPUS):"; + slider.disabled = false; + value.disabled = false; + slider.min = "1"; + slider.max = "128"; + slider.value = defaultAudioBitrate; + slider.step = "1"; + value.value = defaultAudioBitrate; + } else if (!audioOnly && usePCM) { + // Video + PCM audio mode + bitrateLabel.textContent = "Video bitrate (with PCM16 audio):"; + slider.disabled = false; + value.disabled = false; + slider.min = "50"; + slider.max = "10000"; + slider.value = defaultVideoBitrate; + slider.step = "100"; + value.value = defaultVideoBitrate; + } else { + // Video + OPUS audio mode + bitrateLabel.textContent = "Recording Bitrate:"; + slider.disabled = false; + value.disabled = false; + slider.min = "50"; + slider.max = "10000"; + slider.value = defaultVideoBitrate; + slider.step = "100"; + value.value = defaultVideoBitrate; + } + + updateBitrateQuality(promptID); + } + + function updateBitrateValue(promptID) { + const value = document.getElementById(`bitrateSlider_${promptID}`).value; + document.getElementById(`bitrateValue_${promptID}`).value = value; + updateBitrateQuality(promptID); + } + + function updateBitrateSlider(promptID) { + const value = document.getElementById(`bitrateValue_${promptID}`).value; + document.getElementById(`bitrateSlider_${promptID}`).value = value; + updateBitrateQuality(promptID); + } + + function updateBitrateQuality(promptID) { + const audioOnly = document.getElementById(`audioOnly_${promptID}`).checked; + const usePCM = document.getElementById(`usePCM_${promptID}`).checked; + const value = parseInt(document.getElementById(`bitrateValue_${promptID}`).value); + const qualitySpan = document.getElementById(`bitrateQuality_${promptID}`); + + if (audioOnly && usePCM) { + qualitySpan.textContent = "N/A"; + return; + } + + if (audioOnly) { + if (value < 32) qualitySpan.textContent = "Low"; + else if (value >= 80) qualitySpan.textContent = "High"; + else qualitySpan.textContent = "Medium"; + defaultAudioBitrate = value; + } else { + if (value < 2000) qualitySpan.textContent = "Low"; + else if (value >= 6000) qualitySpan.textContent = "High"; + else qualitySpan.textContent = "Medium"; + defaultVideoBitrate = value; + } + } + + await new Promise((resolve, reject) => { + var promptID = "pid_" + Math.random().toString(36).substr(2, 9); + Prompts[promptID] = {}; + Prompts[promptID].resolve = resolve; + Prompts[promptID].reject = reject; + + var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; + var backdropClass = block ? "opaqueBackdrop" : "modalBackdrop"; + + inputText = "

    Recording setup


    " + inputText.replace(/\n/g, "
    "); + + const modalTemplate = `start record +
    `; + + document.body.insertAdjacentHTML("beforeend", modalTemplate); + + // Set default values if provided + if (defaultOptions.audioOnly) { + document.getElementById(`audioOnly_${promptID}`).checked = true; + updateBitrateControls(promptID); + } + if (defaultOptions.usePCM) { + document.getElementById(`usePCM_${promptID}`).checked = true; + if (defaultOptions.audioOnly) { + const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`); + bitrateLabel.textContent = "Uncompressed PCM16 audio"; + bitrateLabel.parentNode.style.opacity = "0.5"; + bitrateLabel.parentNode.style.pointerEvents = 'none'; + } else { + const bitrateLabel = document.getElementById(`bitrateLabel_${promptID}`); + bitrateLabel.textContent = "Video bitrate (with PCM16 audio):"; + } + + } + + // Add event listeners + document.getElementById(`audioOnly_${promptID}`).addEventListener("change", () => updateBitrateControls(promptID)); + document.getElementById(`usePCM_${promptID}`).addEventListener("change", () => updateBitrateControls(promptID)); + document.getElementById(`bitrateSlider_${promptID}`).addEventListener("input", () => updateBitrateValue(promptID)); + document.getElementById(`bitrateValue_${promptID}`).addEventListener("change", () => updateBitrateSlider(promptID)); + + // Submit handler + document.getElementById(`submit_${promptID}`).addEventListener("click", function (event) { + const pid = event.target.dataset.pid; + result = { + audioOnly: document.getElementById(`audioOnly_${pid}`).checked, + usePCM: document.getElementById(`usePCM_${pid}`).checked, + bitrate: parseInt(document.getElementById(`bitrateValue_${pid}`).value) + }; + document.getElementById(`modal_${pid}`).remove(); + document.getElementById(`modalBackdrop_${pid}`).remove(); + Prompts[pid].resolve(); + }); + + // Cancel handler + document.getElementById(`cancel_${promptID}`).addEventListener("click", function (event) { + const pid = event.target.dataset.pid; + result = null; + document.getElementById(`modal_${pid}`).remove(); + document.getElementById(`modalBackdrop_${pid}`).remove(); + Prompts[pid].resolve(); + }); + + // Close handler + document.getElementById(`close_${promptID}`).addEventListener("click", function (event) { + const pid = event.target.dataset.pid; + result = null; + document.getElementById(`modal_${pid}`).remove(); + document.getElementById(`modalBackdrop_${pid}`).remove(); + Prompts[pid].resolve(); + }); + + // Stop propagation on modal click + document.getElementById(`modal_${promptID}`).addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + + // Translate if needed + miniTranslate(document.getElementById(`modal_${promptID}`)); + }); + + return result; +} + +async function promptAltRecord(inputText, block = false, asterix = false, value = false) { + var result = null; + if (session.beepToNotify) { + playtone(); + } + await new Promise((resolve, reject) => { + var promptID = "pid_" + Math.random().toString(36).substr(2, 9); + Prompts[promptID] = {}; + Prompts[promptID].resolve = resolve; + Prompts[promptID].reject = reject; + + var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; + + if (block) { + var backdropClass = "opaqueBackdrop"; + } else { + var backdropClass = "modalBackdrop"; + } + + inputText = "" + inputText.replace("\n", "
    ") + ""; + inputText = inputText.replace(/\n/g, "
    "); + var type = "text"; + if (asterix) { + type = "password"; + } + + if (time) { + modalTemplate = ` +
    `; + } else if (recording) { + modalTemplate = ` +
    `; + } else if (hotkey) { + modalTemplate = ` +
    `; + } else { + modalTemplate = ` +
    `; + } + + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + + document.getElementById("input_" + promptID).focus(); + + if (value !== false) { + if (time) { + document.getElementById("input_" + promptID).value = parseInt(value / 60); + document.getElementById("input_" + promptID + "_sec").value = parseInt(value) % 60; + } else { + document.getElementById("input_" + promptID).value = value; + } + } + + if (time) { + document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { + if (event.key === "Enter") { + document.getElementById("input_" + promptID + "_sec").focus(); + } + }); + document.getElementById("input_" + promptID + "_sec").addEventListener("keyup", function (event) { + if (event.key === "Enter") { + document.getElementById("submit_" + promptID).focus(); + } + }); + document.getElementById("countup_" + promptID).addEventListener("click", function (event) { + if (document.getElementById("countup_" + promptID).checked) { + document.getElementById("input_" + promptID).disabled = true; + document.getElementById("input_" + promptID + "_sec").disabled = true; + } else { + document.getElementById("input_" + promptID).disabled = false; + document.getElementById("input_" + promptID + "_sec").disabled = false; + delete document.getElementById("input_" + promptID).disabled; + delete document.getElementById("input_" + promptID + "_sec").disabled; + } + }); + } else { + document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { + if (event.key === "Enter") { + var pid = event.target.dataset.pid; + result = document.getElementById("input_" + pid).value; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + } + }); + } + + try { + document.getElementById("submit_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + if (time) { + result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60; + + if (document.getElementById("countup_" + promptID).checked) { + result = 0; + } + } else { + result = document.getElementById("input_" + pid).value; + } + + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + + Prompts[pid].resolve(); + }); + } catch (e) { } + + try { + document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + } catch (e) { } + + try { + document.getElementById("close_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + } catch (e) { } + + getById("modal_" + promptID).addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + miniTranslate(getById("modal_" + promptID)); + return; + }); + return result; +} + +async function promptRecord() { + var result = null; + if (session.beepToNotify) { + playtone(); + } + await new Promise((resolve, reject) => { + var promptID = "pid_" + Math.random().toString(36).substr(2, 9); + Prompts[promptID] = {}; + Prompts[promptID].resolve = resolve; + Prompts[promptID].reject = reject; + + var zindex = 1030 + document.querySelectorAll(".promptModal").length; + + var backdropClass = "modalBackdrop"; + + var inputText = "RECORD SETTINGS"; + + inputText = "" + inputText.replace("\n", "
    ") + ""; + inputText = inputText.replace(/\n/g, "
    "); + var type = "text"; + + modalTemplate = ` +
    `; + + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + + var gdrive = getById(`input_${promptID}_gdrive`); + gdrive.onchange = async function () { + if (this.checked) { + this.uploadLink = await session.gdrive.startResumableUpload(); + } else { + this.uploadLink = null; + } + }; + + document.getElementById("input_" + promptID).focus(); + + document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { + if (event.key === "Enter") { + var pid = event.target.dataset.pid; + result = document.getElementById("input_" + pid).value; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + } + }); + + try { + document.getElementById("submit_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + if (time) { + result = parseInt(document.getElementById("input_" + pid + "_sec").value) + parseInt(document.getElementById("input_" + pid).value) * 60; + + if (document.getElementById("countup_" + promptID).checked) { + result = 0; + } + } else { + result = document.getElementById("input_" + pid).value; + } + + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + + Prompts[pid].resolve(); + }); + } catch (e) { } + + try { + document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + } catch (e) { } + + try { + document.getElementById("close_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + } catch (e) { } + + getById("modal_" + promptID).addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + miniTranslate(getById("modal_" + promptID)); + return; + }); + return result; +} + +async function promptTransfer(value = null, bcmode = null, updateurl = null, queueMode = null) { + var result = { roomid: null }; + if (session.beepToNotify) { + playtone(); + } + await new Promise((resolve, reject) => { + var promptID = "pid_" + Math.random().toString(36).substr(2, 9); + Prompts[promptID] = {}; + Prompts[promptID].resolve = resolve; + Prompts[promptID].reject = reject; + + var zindex = 30 + document.querySelectorAll(".promptModal").length; + var backdropClass = "modalBackdrop"; + + var inputText = "" + getTranslation("transfer-guest-to-room").replace("\n", "
    ") + ""; + inputText = inputText.replace(/\n/g, "
    "); + + modalTemplate = ` +
    `; + + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + + document.getElementById("input_" + promptID).focus(); + + if (value !== null) { + document.getElementById("input_" + promptID).value = value; + } + + if (bcmode !== null) { + document.getElementById("broadcast_" + promptID).checked = bcmode; + } + if (queueMode !== null) { + document.getElementById("queued_" + promptID).checked = queueMode; + } + + if (updateurl !== null) { + document.getElementById("private_" + promptID).checked = updateurl; + } + + document.getElementById("input_" + promptID).addEventListener("keyup", function (event) { + if (event.key === "Enter") { + var pid = event.target.dataset.pid; + var room = document.getElementById("input_" + pid).value; + var updateurl = document.getElementById("private_" + pid).checked; + var broadcast = document.getElementById("broadcast_" + pid).checked; + var queue = document.getElementById("queued_" + pid).checked; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + result = { roomid: room, updateurl: updateurl, broadcast: broadcast, queue: queue }; + } + }); + + document.getElementById("submit_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + var room = document.getElementById("input_" + pid).value; + var updateurl = document.getElementById("private_" + pid).checked; + var broadcast = document.getElementById("broadcast_" + pid).checked; + var queue = document.getElementById("queued_" + pid).checked; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + result = { roomid: room, updateurl: updateurl, broadcast: broadcast, queue: queue }; + }); + + document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + + document.getElementById("close_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + document.getElementById("modal_" + pid).remove(); + document.getElementById("modalBackdrop_" + pid).remove(); + Prompts[pid].resolve(); + }); + + getById("modal_" + promptID).addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + miniTranslate(getById("modal_" + promptID)); + return; + }); + return result; +} + +function youveBeenTransferred() { + getChatMessage(getTranslation("you-have-been-transferred"), (label = false), (director = false), (overlay = true)); // "you-have-been-transferred" + getById("head2").innerHTML = getTranslation("room-changed"); // not sure this is right?? + + miniTranslate(getById("head2"), "room-changed"); + + if (session.director) { + getById("head4").innerHTML = getTranslation("you-are-no-longer-a-co-director"); //"You are no longer a co-director as you were transferred."; // + } + + if (session.label) { + document.title = session.label + " - " + getTranslation("transferred"); + } else { + document.title = getTranslation("transferred"); + } + + hideHomeCheck(); +} + +function youveBeenActivated() { + if (session.queueType == 3 || session.queueType == 4) { + // For both hold modes, publish to any deferred peers upon activation. + // queueType 3: All peers were deferred (needsPublishing=true) + // queueType 4: Only non-director peers were deferred; directors already receiving + closeModal(false, "123"); + for (var UUID in session.pcs) { + if (session.pcs[UUID].needsPublishing) { + session.initialPublish(UUID); + } + } + } + + getChatMessage(getTranslation("you-have-been-activated"), (label = false), (director = false), (overlay = true)); + hideHomeCheck(); +} + +function youreWaitingToBeActivated() { + // getChatMessage( getTranslation("you-not-yet-activated"), label = false, director = false, overlay = true); + warnUser(getTranslation("you-not-yet-activated"), false, false, 123); + hideHomeCheck(); +} + +async function confirmAlt(inputText, block = false, context = null) { + var result = null; + if (session.beepToNotify) { + playtone(); + } + await new Promise((resolve, reject) => { + var promptID = "pid_" + Math.random().toString(36).substr(2, 9); + Prompts[promptID] = {}; + Prompts[promptID].resolve = resolve; + Prompts[promptID].reject = reject; + Prompts[promptID].context = context; + + var zindex = 33 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; + + if (block) { + var backdropClass = "opaqueBackdrop"; + } else { + var backdropClass = "modalBackdrop"; + } + + inputText = "" + inputText.replace("\n", "
    ") + ""; + inputText = inputText.replace(/\n/g, "
    "); + + modalTemplate = ` +
    ]/g, "") : ''}" style="z-index:${zindex + 1}">
    `; + + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + + document.getElementById("submit_" + promptID).focus(); + + document.getElementById("submit_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + result = true; + getById("modalBackdrop_" + pid).remove(); + getById("modal_" + pid).remove(); + Prompts[pid].resolve(); + }); + + document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + getById("modalBackdrop_" + pid).remove(); + getById("modal_" + pid).remove(); + Prompts[pid].resolve(); + }); + + document.getElementById("close_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + getById("modalBackdrop_" + pid).remove(); + getById("modal_" + pid).remove(); + Prompts[pid].resolve(); + }); + + getById("modal_" + promptID).addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + miniTranslate(getById("modal_" + promptID)); + return; + }); + return result; +} + +async function confirmHangupWithBlock(inputText) { + // Similar to confirmAlt but includes a "block from rejoining" checkbox + // Returns: { confirmed: boolean, block: boolean } + var result = { confirmed: false, block: false }; + if (session.beepToNotify) { + playtone(); + } + await new Promise((resolve, reject) => { + var promptID = "pid_" + Math.random().toString(36).substr(2, 9); + Prompts[promptID] = {}; + Prompts[promptID].resolve = resolve; + Prompts[promptID].reject = reject; + + var zindex = 33 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; + var backdropClass = "modalBackdrop"; + + inputText = "" + inputText.replace("\n", "
    ") + ""; + inputText = inputText.replace(/\n/g, "
    "); + + modalTemplate = ` +
    `; + + document.body.insertAdjacentHTML("beforeend", modalTemplate); + + document.getElementById("submit_" + promptID).focus(); + + document.getElementById("submit_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + result.confirmed = true; + result.block = document.getElementById("blockUser_" + pid).checked; + getById("modalBackdrop_" + pid).remove(); + getById("modal_" + pid).remove(); + Prompts[pid].resolve(); + }); + + document.getElementById("cancel_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + getById("modalBackdrop_" + pid).remove(); + getById("modal_" + pid).remove(); + Prompts[pid].resolve(); + }); + + document.getElementById("close_" + promptID).addEventListener("click", function (event) { + var pid = event.target.dataset.pid; + getById("modalBackdrop_" + pid).remove(); + getById("modal_" + pid).remove(); + Prompts[pid].resolve(); + }); + + getById("modal_" + promptID).addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + miniTranslate(getById("modal_" + promptID)); + return; + }); + return result; +} + +var modalTimeout = null; +function warnUser(message, timeout = false, sanitize = true, modalID = false) { + // Allows for multiple alerts to stack better. + // Every modal and backdrop has an increasing z-index + // to block the previous modal + if (!message) { + return; + } + + if (document.getElementById("modalBackdrop")) { + getById("alertModal").innerHTML = ""; // Delete modal + getById("alertModal").remove(); + getById("modalBackdrop").innerHTML = ""; // Delete modal + getById("modalBackdrop").remove(); + } + + zindex = 31 + document.querySelectorAll(".alertModal").length + document.querySelectorAll(".promptModal").length; + try { + if (sanitize) { + message = sanitizeChat(message, 2000); + } + message = message.replace(/\n/g, "
    "); + } catch (e) { + errorlog(message); + } + + if (!modalID) { + modalID = Math.floor(Math.random() * 999) + 1000; + } + + modalTemplate = `
    +
    + × + ${message} +
    +
    +
    `; + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + + document.getElementById("modalBackdrop").addEventListener("click", closeModal); + + clearTimeout(modalTimeout); + if (timeout) { + modalTimeout = setTimeout(closeModal, timeout, false, modalID); + } + getById("alertModal").addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + return modalID; +} +function closeModal(ele = false, modalID = false) { + if (modalID && !ele) { + // Check for alertModal with data-modalID (warnUser modals) + var alertModalMatch = document.querySelector("#alertModal[data-modalID='" + modalID + "']"); + // Check for promptModal with data-context (confirmAlt modals like approval popups) + var ctx = ("" + modalID).replace(/["<>]/g, ""); + var promptModalMatch = document.querySelector('.promptModal[data-context="' + ctx + '"]'); + + if (promptModalMatch) { + // Close the confirmAlt modal by context (programmatic dismissal) + try { + var modalIdAttr = promptModalMatch.id; // e.g., "modal_pid_abc123" + if (modalIdAttr && modalIdAttr.startsWith("modal_")) { + var pid = modalIdAttr.substring(6); // Extract "pid_abc123" + // Remove modal and backdrop + promptModalMatch.remove(); + var backdrop = document.getElementById("modalBackdrop_" + pid); + if (backdrop) { backdrop.remove(); } + // Do NOT resolve the promise - let it stay pending + // Resolving would trigger the denial dialog in promptApproval + if (typeof Prompts !== "undefined" && Prompts[pid]) { + delete Prompts[pid]; // Clean up reference, but don't resolve + } + } else { + promptModalMatch.remove(); + } + } catch (e) { warnlog(e); } + return; + } + + if (!alertModalMatch) { + return; + } + } + + clearTimeout(modalTimeout); + try { + getById("modalBackdrop").innerHTML = ""; // Delete modal + getById("modalBackdrop").remove(); + getById("alertModal").innerHTML = ""; // Delete modal + getById("alertModal").remove(); + getById("promptModal").innerHTML = ""; // Delete modal + getById("promptModal").remove(); + + query(".modalBackdrop").innerHTML = ""; // Delete modal + query(".modalBackdrop").remove(); + + if (ele && ele.innerHTML && ele.remove) { + ele.innerHTML = ""; // Delete specific modal + ele.remove(); + } + } catch (e) { + warnlog(e); + } + if (session.timeoutTriggered) { + session.warnUserTriggered = false; + } +} + +var sanitizeStreamID = function (streamID) { + streamID = streamID.trim(); + + if (streamID.length < 1) { + streamID = session.generateStreamID(8); + if (!session.cleanOutput) { + warnUser(getTranslation("no-streamID-provided") + streamID, false, false); + } + } + var streamID_sanitized = streamID.replace(/[\W]+/g, "_"); + if (streamID !== streamID_sanitized) { + if (!session.cleanOutput) { + warnUser(getTranslation("alphanumeric-only"), false, false); + } + } + if (streamID_sanitized.length > 64) { + streamID_sanitized = streamID_sanitized.substring(0, 70); // leave room for salting + if (!session.cleanOutput) { + warnUser(getTranslation("stream-id-too-long"), false, false); + } + } + return streamID_sanitized; +}; + +var checkStrength = function (string) { + var matcher = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{7,30}$/; + if (string.match(matcher)) { + return true; + } else if (string.length > 20) { + return true; + } else { + return false; + } +}; + +var checkStrengthRoom = function () { + var result1 = checkStrength(getById("videoname1").value); + var result2 = getById("passwordRoom").value.length; + var target = getById("securityLevelRoom"); + target.style.display = "block"; + if (result1) { + if (result2) { + target.innerHTML = "" + getTranslation("share-with-trusted") + ""; + } else { + target.innerHTML = "" + getTranslation("pass-recommended") + ""; + } + } else { + target.innerHTML = "" + getTranslation("insecure-room-name") + " " + getTranslation("allowed-chars") + ": A-Z, a-z, 0-9, _"; + } +}; + +var emojiShortCodes = { + ":joy:": "😂", + ":heart:": "❤️", + ":heart_eyes:": "😍", + ":sob:": "😭", + ":blush:": "😊", + ":unamused:": "😒", + ":two_hearts:": "💕", + ":weary:": "😩", + ":ok_hand:": "👌", + ":pensive:": "😔", + ":smirk:": "😏", + ":grin:": "😁", + ":wink:": "😉", + ":thumbsup:": "👍", + ":pray:": "🙏", + ":relieved:": "😌", + ":notes:": "🎶", + ":flushed:": "😳", + ":raised_hands:": "🙌", + ":see_no_evil:": "🙈", + ":cry:": "😢", + ":sunglasses:": "😎", + ":v:": "✌️", + ":eyes:": "👀", + ":sweat_smile:": "😅", + ":sparkles:": "✨", + ":sleeping:": "😴", + ":smile:": "😄", + ":purple_heart:": "💜", + ":broken_heart:": "💔", + ":blue_heart:": "💙", + ":confused:": "😕", + ":disappointed:": "😞", + ":yum:": "😋", + ":neutral_face:": "😐", + ":sleepy:": "😪", + ":clap:": "👏", + ":cupid:": "💘", + ":heartpulse:": "💗", + ":kiss:": "💋", + ":point_right:": "👉", + ":scream:": "😱", + ":fire:": "🔥", + ":rage:": "😡", + ":smiley:": "😃", + ":tada:": "🎉", + ":tired_face:": "😫", + ":camera:": "📷", + ":rose:": "🌹", + ":muscle:": "💪", + ":skull:": "💀", + ":sunny:": "☀️", + ":yellow_heart:": "💛", + ":triumph:": "😤", + ":laughing:": "😆", + ":sweat:": "😓", + ":point_left:": "👈", + ":grinning:": "😀", + ":mask:": "😷", + ":green_heart:": "💚", + ":wave:": "👋", + ":persevere:": "😣", + ":heartbeat:": "💓", + ":crown:": "👑", + ":innocent:": "😇", + ":headphones:": "🎧", + ":confounded:": "😖", + ":angry:": "😠", + ":grimacing:": "😬", + ":star2:": "🌟", + ":gun:": "🔫", + ":raising_hand:": "🙋", + ":thumbsdown:": "👎", + ":dancer:": "💃", + ":musical_note:": "🎵", + ":no_mouth:": "😶", + ":dizzy:": "💫", + ":fist:": "✊", + ":point_down:": "👇", + ":no_good:": "🙅", + ":boom:": "💥", + ":tongue:": "👅", + ":poop:": "💩", + ":cold_sweat:": "😰", + ":gem:": "💎", + ":ok_woman:": "🙆", + ":pizza:": "🍕", + ":joy_cat:": "😹", + ":leaves:": "🍃", + ":sweat_drops:": "💦", + ":penguin:": "🐧", + ":zzz:": "💤", + ":walking:": "🚶", + ":airplane:": "✈️", + ":balloon:": "🎈", + ":star:": "⭐", + ":ribbon:": "🎀", + ":worried:": "😟", + ":underage:": "🔞", + ":fearful:": "😨", + ":hibiscus:": "🌺", + ":microphone:": "🎤", + ":open_hands:": "👐", + ":ghost:": "👻", + ":palm_tree:": "🌴", + ":nail_care:": "💅", + ":alien:": "👽", + ":bow:": "🙇", + ":cloud:": "☁", + ":soccer:": "⚽", + ":angel:": "👼", + ":dancers:": "👯", + ":snowflake:": "❄️", + ":point_up:": "☝️", + ":rainbow:": "🌈", + ":gift_heart:": "💝", + ":gift:": "🎁", + ":beers:": "🍻", + ":anguished:": "😧", + ":earth_africa:": "🌍", + ":movie_camera:": "🎥", + ":anchor:": "⚓", + ":zap:": "⚡", + ":runner:": "🏃", + ":sunflower:": "🌻", + ":bouquet:": "💐", + ":dog:": "🐶", + ":moneybag:": "💰", + ":herb:": "🌿", + ":couple:": "👫", + ":fallen_leaf:": "🍂", + ":tulip:": "🌷", + ":birthday:": "🎂", + ":cat:": "🐱", + ":coffee:": "☕", + ":dizzy_face:": "😵", + ":point_up_2:": "👆", + ":open_mouth:": "😮", + ":hushed:": "😯", + ":basketball:": "🏀", + ":ring:": "💍", + ":astonished:": "😲", + ":hear_no_evil:": "🙉", + ":dash:": "💨", + ":cactus:": "🌵", + ":hotsprings:": "♨️", + ":telephone:": "☎️", + ":maple_leaf:": "🍁", + ":princess:": "👸", + ":massage:": "💆", + ":love_letter:": "💌", + ":trophy:": "🏆", + ":blossom:": "🌼", + ":lips:": "👄", + ":fries:": "🍟", + ":doughnut:": "🍩", + ":frowning:": "😦", + ":ocean:": "🌊", + ":bomb:": "💣", + ":cyclone:": "🌀", + ":rocket:": "🚀", + ":umbrella:": "☔", + ":couplekiss:": "💏", + ":lollipop:": "🍭", + ":clapper:": "🎬", + ":pig:": "🐷", + ":smiling_imp:": "😈", + ":imp:": "👿", + ":bee:": "🐝", + ":kissing_cat:": "😽", + ":anger:": "💢", + ":santa:": "🎅", + ":earth_asia:": "🌏", + ":football:": "🏈", + ":guitar:": "🎸", + ":panda_face:": "🐼", + ":strawberry:": "🍓", + ":smirk_cat:": "😼", + ":banana:": "🍌", + ":watermelon:": "🍉", + ":snowman:": "⛄", + ":smile_cat:": "😸", + ":eggplant:": "🍆", + ":crystal_ball:": "🔮", + ":calling:": "📲", + ":iphone:": "📱", + ":partly_sunny:": "⛅", + ":warning:": "⚠️", + ":scream_cat:": "🙀", + ":baby:": "👶", + ":feet:": "🐾", + ":footprints:": "👣", + ":beer:": "🍺", + ":wine_glass:": "🍷", + ":video_camera:": "📹", + ":rabbit:": "🐰", + ":smoking:": "🚬", + ":peach:": "🍑", + ":snake:": "🐍", + ":turtle:": "🐢", + ":cherries:": "🍒", + ":kissing:": "😗", + ":frog:": "🐸", + ":milky_way:": "🌌", + ":closed_book:": "📕", + ":candy:": "🍬", + ":hamburger:": "🍔", + ":bear:": "🐻", + ":tiger:": "🐯", + ":icecream:": "🍦", + ":pineapple:": "🍍", + ":ear_of_rice:": "🌾", + ":syringe:": "💉", + ":tv:": "📺", + ":pill:": "💊", + ":octopus:": "🐙", + ":grapes:": "🍇", + ":smiley_cat:": "😺", + ":cd:": "💿", + ":cocktail:": "🍸", + ":cake:": "🍰", + ":video_game:": "🎮", + ":lipstick:": "💄", + ":whale:": "🐳", + ":cookie:": "🍪", + ":dolphin:": "🐬", + ":loud_sound:": "🔊", + ":man:": "👨", + ":monkey:": "🐒", + ":books:": "📚", + ":guardsman:": "💂", + ":loudspeaker:": "📢", + ":scissors:": "✂️", + ":girl:": "👧", + ":mortar_board:": "🎓", + ":baseball:": "⚾️", + ":woman:": "👩", + ":fireworks:": "🎆", + ":stars:": "🌠", + ":mushroom:": "🍄", + ":pouting_cat:": "😾", + ":left_luggage:": "🛅", + ":high_heel:": "👠", + ":dart:": "🎯", + ":swimmer:": "🏊", + ":key:": "🔑", + ":bikini:": "👙", + ":family:": "👪", + ":pencil2:": "✏", + ":elephant:": "🐘", + ":droplet:": "💧", + ":seedling:": "🌱", + ":apple:": "🍎", + ":dollar:": "💵", + ":book:": "📖", + ":haircut:": "💇", + ":computer:": "💻", + ":bulb:": "💡", + ":boy:": "👦", + ":tangerine:": "🍊", + ":sunrise:": "🌅", + ":poultry_leg:": "🍗", + ":shaved_ice:": "🍧", + ":bird:": "🐦", + ":eyeglasses:": "👓", + ":goat:": "🐐", + ":older_woman:": "👵", + ":new_moon:": "🌑", + ":customs:": "🛃", + ":house:": "🏠", + ":full_moon:": "🌕", + ":lemon:": "🍋", + ":baby_bottle:": "🍼", + ":spaghetti:": "🍝", + ":wind_chime:": "🎐", + ":fish_cake:": "🍥", + ":nose:": "👃", + ":pig_nose:": "🐽", + ":fish:": "🐟", + ":koala:": "🐨", + ":ear:": "👂", + ":shower:": "🚿", + ":bug:": "🐛", + ":ramen:": "🍜", + ":tophat:": "🎩", + ":fuelpump:": "⛽", + ":horse:": "🐴", + ":watch:": "⌚", + ":monkey_face:": "🐵", + ":baby_symbol:": "🚼", + ":sparkler:": "🎇", + ":corn:": "🌽", + ":tennis:": "🎾", + ":battery:": "🔋", + ":wolf:": "🐺", + ":moyai:": "🗿", + ":cow:": "🐮", + ":mega:": "📣", + ":older_man:": "👴", + ":dress:": "👗", + ":link:": "🔗", + ":chicken:": "🐔", + ":whale2:": "🐋", + ":bento:": "🍱", + ":pushpin:": "📌", + ":dragon:": "🐉", + ":hamster:": "🐹", + ":golf:": "⛳", + ":surfer:": "🏄", + ":mouse:": "🐭", + ":blue_car:": "🚙", + ":bread:": "🍞", + ":cop:": "👮", + ":tea:": "🍵", + ":bike:": "🚲", + ":rice:": "🍚", + ":radio:": "📻", + ":baby_chick:": "🐤", + ":sheep:": "🐑", + ":lock:": "🔒", + ":green_apple:": "🍏", + ":racehorse:": "🐎", + ":fried_shrimp:": "🍤", + ":volcano:": "🌋", + ":rooster:": "🐓", + ":inbox_tray:": "📥", + ":wedding:": "💒", + ":sushi:": "🍣", + ":ice_cream:": "🍨", + ":tomato:": "🍅", + ":rabbit2:": "🐇", + ":beetle:": "🐞", + ":bath:": "🛀", + ":no_entry:": "⛔", + ":crocodile:": "🐊", + ":dog2:": "🐕", + ":cat2:": "🐈", + ":hammer:": "🔨", + ":meat_on_bone:": "🍖", + ":shell:": "🐚", + ":poodle:": "🐩", + ":stew:": "🍲", + ":jeans:": "👖", + ":honey_pot:": "🍯", + ":unlock:": "🔓", + ":black_nib:": "✒", + ":snowboarder:": "🏂", + ":white_flower:": "💮", + ":necktie:": "👔", + ":womens:": "🚺", + ":ant:": "🐜", + ":city_sunset:": "🌇", + ":dragon_face:": "🐲", + ":snail:": "🐌", + ":dvd:": "📀", + ":shirt:": "👕", + ":game_die:": "🎲", + ":dolls:": "🎎", + ":8ball:": "🎱", + ":bus:": "🚌", + ":custard:": "🍮", + ":camel:": "🐫", + ":curry:": "🍛", + ":hospital:": "🏥", + ":bell:": "🔔", + ":pear:": "🍐", + ":door:": "🚪", + ":saxophone:": "🎷", + ":church:": "⛪", + ":bicyclist:": "🚴", + ":dango:": "🍡", + ":office:": "🏢", + ":rowboat:": "🚣", + ":womans_hat:": "👒", + ":mans_shoe:": "👞", + ":love_hotel:": "🏩", + ":mount_fuji:": "🗻", + ":handbag:": "👜", + ":hourglass:": "⌛", + ":trumpet:": "🎺", + ":school:": "🏫", + ":cow2:": "🐄", + ":toilet:": "🚽", + ":pig2:": "🐖", + ":violin:": "🎻", + ":credit_card:": "💳", + ":ferris_wheel:": "🎡", + ":bowling:": "🎳", + ":barber:": "💈", + ":purse:": "👛", + ":rat:": "🐀", + ":date:": "📅", + ":ram:": "🐏", + ":tokyo_tower:": "🗼", + ":kimono:": "👘", + ":ship:": "🚢", + ":mag_right:": "🔎", + ":mag:": "🔍", + ":fire_engine:": "🚒", + ":police_car:": "🚓", + ":black_joker:": "🃏", + ":package:": "📦", + ":calendar:": "📆", + ":horse_racing:": "🏇", + ":tiger2:": "🐅", + ":boot:": "👢", + ":ambulance:": "🚑", + ":boar:": "🐗", + ":pound:": "💷", + ":ox:": "🐂", + ":rice_ball:": "🍙", + ":sandal:": "👡", + ":tent:": "⛺", + ":seat:": "💺", + ":taxi:": "🚕", + ":briefcase:": "💼", + ":newspaper:": "📰", + ":circus_tent:": "🎪", + ":mens:": "🚹", + ":flashlight:": "🔦", + ":foggy:": "🌁", + ":bamboo:": "🎍", + ":ticket:": "🎫", + ":helicopter:": "🚁", + ":minidisc:": "💽", + ":oncoming_bus:": "🚍", + ":melon:": "🍈", + ":notebook:": "📓", + ":no_bell:": "🔕", + ":oden:": "🍢", + ":flags:": "🎏", + ":blowfish:": "🐡", + ":sweet_potato:": "🍠", + ":ski:": "🎿", + ":construction:": "🚧", + ":satellite:": "📡", + ":euro:": "💶", + ":ledger:": "📒", + ":leopard:": "🐆", + ":truck:": "🚚", + ":sake:": "🍶", + ":railway_car:": "🚃", + ":speedboat:": "🚤", + ":vhs:": "📼", + ":yen:": "💴", + ":mute:": "🔇", + ":wheelchair:": "♿", + ":paperclip:": "📎", + ":atm:": "🏧", + ":telescope:": "🔭", + ":rice_scene:": "🎑", + ":blue_book:": "📘", + ":postbox:": "📮", + ":e-mail:": "📧", + ":mouse2:": "🐁", + ":nut_and_bolt:": "🔩", + ":hotel:": "🏨", + ":wc:": "🚾", + ":green_book:": "📗", + ":tractor:": "🚜", + ":fountain:": "⛲", + ":metro:": "🚇", + ":clipboard:": "📋", + ":no_smoking:": "🚭", + ":slot_machine:": "🎰", + ":bathtub:": "🛁", + ":scroll:": "📜", + ":station:": "🚉", + ":rice_cracker:": "🍘", + ":bank:": "🏦", + ":wrench:": "🔧", + ":bar_chart:": "📊", + ":minibus:": "🚐", + ":tram:": "🚊", + ":microscope:": "🔬", + ":bookmark:": "🔖", + ":pouch:": "👝", + ":fax:": "📠", + ":sound:": "🔉", + ":chart:": "💹", + ":floppy_disk:": "💾", + ":post_office:": "🏣", + ":speaker:": "🔈", + ":japan:": "🗾", + ":mahjong:": "🀄", + ":orange_book:": "📙", + ":restroom:": "🚻", + ":train:": "🚋", + ":trolleybus:": "🚎", + ":postal_horn:": "📯", + ":factory:": "🏭", + ":train2:": "🚆", + ":pager:": "📟", + ":outbox_tray:": "📤", + ":mailbox:": "📫", + ":light_rail:": "🚈", + ":busstop:": "🚏", + ":file_folder:": "📁", + ":card_index:": "📇", + ":monorail:": "🚝", + ":no_bicycles:": "🚳", + ":hugging:": "🤗", + ":thinking:": "🤔", + ":nerd:": "🤓", + ":zipper_mouth:": "🤐", + ":rolling_eyes:": "🙄", + ":upside_down:": "🙃", + ":slight_smile:": "🙂", + ":writing_hand:": "✍", + ":eye:": "👁", + ":man_in_suit:": "🕴", + ":golfer:": "🏌", + ":golfer_woman:": "🏌‍♀", + ":anger_right:": "🗯", + ":coffin:": "⚰", + ":gear:": "⚙", + ":alembic:": "⚗", + ":scales:": "⚖", + ":keyboard:": "⌨", + ":shield:": "🛡", + ":bed:": "🛏", + ":ballot_box:": "🗳", + ":compression:": "🗜", + ":wastebasket:": "🗑", + ":file_cabinet:": "🗄", + ":trackball:": "🖲", + ":printer:": "🖨", + ":joystick:": "🕹", + ":hole:": "🕳", + ":candle:": "🕯", + ":prayer_beads:": "📿", + ":amphora:": "🏺", + ":label:": "🏷", + ":film_frames:": "🎞", + ":level_slider:": "🎚", + ":thermometer:": "🌡", + ":motorway:": "🛣", + ":synagogue:": "🕍", + ":mosque:": "🕌", + ":kaaba:": "🕋", + ":stadium:": "🏟", + ":desert:": "🏜", + ":cityscape:": "🏙", + ":camping:": "🏕", + ":rosette:": "🏵", + ":volleyball:": "🏐", + ":medal:": "🏅", + ":popcorn:": "🍿", + ":champagne:": "🍾", + ":hot_pepper:": "🌶", + ":burrito:": "🌯", + ":taco:": "🌮", + ":hotdog:": "🌭", + ":shamrock:": "☘", + ":comet:": "☄", + ":turkey:": "🦃", + ":scorpion:": "🦂", + ":lion_face:": "🦁", + ":crab:": "🦀", + ":spider_web:": "🕸", + ":spider:": "🕷", + ":chipmunk:": "🐿", + ":fog:": "🌫", + ":chains:": "⛓", + ":pick:": "⛏", + ":stopwatch:": "⏱", + ":ferry:": "⛴", + ":mountain:": "⛰", + ":ice_skate:": "⛸", + ":skier:": "⛷", + ":sad:": "😥", + ":egg:": "🥚", + ":drum:": "🥁" +}; + +function convertShortcodes(string) { + if (string.split(":").length > 2) { + for (var i in emojiShortCodes) { + if (string.includes(i)) { + string = string.replaceAll(i, emojiShortCodes[i]); + } + } + } + return string; +} + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +var sanitizeChat = function (string, maxlength = 500) { + var temp = document.createElement("div"); + temp.innerText = string; + temp.innerText = temp.innerHTML; + temp = temp.textContent || temp.innerText || ""; + temp = temp.substring(0, Math.min(temp.length, maxlength)); + return temp.trim(); +}; + +var sanitizeString = function (str) { + str = str.replace(/[^a-z0-9áéíóúñü \.,_-]/gim, ""); + return str.trim(); +}; + +var sanitizeLabel = function (string) { + let temp = document.createElement("div"); + temp.innerText = string; + temp.innerText = temp.innerHTML; + temp = temp.textContent || temp.innerText || ""; + temp = temp.substring(0, Math.min(temp.length, 100)); + return temp.trim(); +}; + +var sanitizeRoomName = function (roomid) { + roomid = roomid.trim(); + if (roomid === "") { + return roomid; + } else if (roomid === false) { + return roomid; + } + + var sanitized = roomid.replace(/[\W]+/g, "_"); + if (roomid.replace(/ /g, "_") !== sanitized) { + if (!session.cleanOutput) { + warnUser("Info: Only AlphaNumeric characters should be used for the room name.\n\nThe offending characters have been replaced by an underscore"); + } + } + if (sanitized.length > 30) { + sanitized = sanitized.substring(0, 30); + if (!session.cleanOutput) { + warnUser("The Room name should be less than 31 alPhaNuMeric characters long.\n\nWe will trim it to length."); + } + } + return sanitized; +}; + +var sanitizePassword = function (passwrd) { + if (passwrd === "") { + return passwrd; + } else if (passwrd === false) { + return passwrd; + } else if (passwrd === null) { + return passwrd; + } + passwrd = passwrd.trim(); + if (passwrd.length < 1) { + if (!session.cleanOutput) { + warnUser("The password provided was blank."); + } + } + var sanitized = encodeURIComponent(passwrd); //.replace(/[\W]+/g, "_"); + //if (sanitized !== passwrd) { + // if (!(session.cleanOutput)) { + // warnUser("Info: Only AlphaNumeric characters should be used in the password.\n\nThe offending characters have been replaced by an underscore"); + // } + //} + return sanitized; +}; + +function checkConnection() { + if (session.ws === null) { + return; + } + if (!session.cleanOutput) { + if (document.getElementById("qos")) { + // true or false; null might cause problems? + getById("logoname").style.display = "unset"; + if (session.ws && session.ws.readyState === WebSocket.OPEN) { + getById("qos").style.color = "#FFF7"; + } else { + getById("qos").style.color = "red"; + } + } + } +} + +session.obsSceneSync = function () { + if (session.layouts && session.obsSceneTriggers && session.obsState && session.obsState.details && session.obsState.details.currentScene.name && session.obsSceneTriggers.includes(session.obsState.details.currentScene.name)) { + var idx = session.obsSceneTriggers.indexOf(session.obsState.details.currentScene.name); + if (idx >= 0) { + if (session.layouts[idx]) { + var layout = combinedLayout(session.layouts[idx]); + if (layout) { + session.layout = layout; + updateMixer(); + } + } + } + return true; + } + return false; +}; + +session.sceneSync = function (UUID) { + if (!session.rpcs[UUID]) { + return; + } else if (!session.rpcs[UUID].videoElement) { + return; + } // i'll want to consider other things, such as canvas at some point. + + var msg = {}; + msg.sceneDisplay = session.rpcs[UUID].videoElement.style.display != "none"; + msg.sceneMute = session.rpcs[UUID].mutedState; + + if (session.optimize !== false) { + // if not visible in the scene anymore, lets lets optimize. This is outside the scope of OBS + var bandwidth = parseInt(session.rpcs[UUID].targetBandwidth); // wtf is goign on here? + if (msg.sceneDisplay === false) { + if (bandwidth > session.optimize || bandwidth < 0) { + // limit to optimized bitrate + bandwidth = session.optimize; + } + } + if (session.rpcs[UUID].bandwidth !== bandwidth) { + // bandwidth already set correctly. don't resend. + msg.bitrate = bandwidth; + if (session.sendRequest(msg, UUID)) { + session.rpcs[UUID].bandwidth = bandwidth; // this is letting the system know what the actual bandwidth is, even if it isn't the real target. + } else { + errorlog("Unable to set update OBS Visibility"); + } + } else { + session.sendRequest(msg, UUID); + } + } else { + session.sendRequest(msg, UUID); + } +}; + +var TriggerOnNewDetails = false; +session.obsStateSync = function (data2send = false, uid = false) { + if (session.disableOBS) { + return; + } + if (!window.obsstudio) { + return; + } // this isn't OBS + // they can disable remote control via OBS brower source drop-down itself. + + log(data2send); + + if (data2send && data2send == "sourceActive" && session.obsState.sourceActive) { + TriggerOnNewDetails = true; + } else if (data2send && data2send == "details" && session.obsState.sourceActive && TriggerOnNewDetails) { + if (session.obsState.details && session.obsState.details.currentScene && session.obsState.details.currentScene.name) { + session.obsState.details.thisScene = session.obsState.details.currentScene.name; + TriggerOnNewDetails = false; + } + } + + var needOptimize = false; + if (session.obsState.visibility !== null) { + if (session.obsState.visibility === false) { + /////////////////// I need to change tis to .state or whatever, anc catch/handle these events to update the buttons in the pop up menu + needOptimize = true; + } + } + + session.obsSceneSync(); + + for (var UUID in session.rpcs) { + if (uid && uid !== UUID) { + continue; + } // target just a single connection. + + var msg = {}; + if (!data2send) { + msg.obsState = Object.assign({}, session.obsState); // shallow copy to avoid mutating global state + if (session.rpcs[UUID].obsControl === false) { + msg.obsState.details = null; // we don't want to send needless data + } + } else if (data2send in session.obsState) { + if (data2send == "details") { + if (session.rpcs[UUID].obsControl === false) { + continue; // we don't want to send needless data; this isn't a visibility update, so skip. + } + msg.obsState = {}; + msg.obsState[data2send] = session.obsState[data2send]; + } else { + msg.obsState = {}; + msg.obsState[data2send] = session.obsState[data2send]; + } + } + + if (session.filterOBSscenes && msg.obsState && msg.obsState.details && msg.obsState.details.scenes && msg.obsState.details.scenes.length) { + var scenes = []; + msg.obsState.details.scenes.forEach(scene => { + if (session.filterOBSscenes && session.filterOBSscenes.length) { + if (session.filterOBSscenes.includes(scene)) { + scenes.push(scene); + } + } + }); + msg.obsState.details.scenes = scenes; + } + + if (session.optimize !== false) { + var bandwidth = parseInt(session.rpcs[UUID].targetBandwidth); + if (needOptimize) { + if (bandwidth > session.optimize || bandwidth < 0) { + // limit to optimized bitrate + bandwidth = session.optimize; + } + } + if (session.rpcs[UUID].bandwidth !== bandwidth) { + // bandwidth already set correctly. don't resend. + msg.bitrate = bandwidth; + warnlog("Message to be sent: "); + warnlog(msg); + if (session.sendRequest(msg, UUID)) { + session.rpcs[UUID].bandwidth = bandwidth; // this is letting the system know what the actual bandwidth is, even if it isn't the real target. + } else { + errorlog("Unable to set update OBS Visibility"); + } + } else { + warnlog("Message to be sent: "); + warnlog(msg); + session.sendRequest(msg, UUID); + } + } else { + warnlog("Message to be sent: "); + warnlog(msg); + session.sendRequest(msg, UUID); + } + } +}; + +session.getOBSOptimization = function (msg, UUID) { + if (session.obsState) { + msg.obsState = {}; + var needOptimize = false; + if (session.obsState.visibility !== null) { + msg.obsState.visibility = session.obsState.visibility; + if (session.obsState.visibility === false) { + needOptimize = true; + } + } + if (session.obsState.sourceActive !== null) { + msg.obsState.sourceActive = session.obsState.sourceActive; + //if (session.obsState.sourceActive===false){ + // needOptimize=true; + //} + } + if (session.obsState.recording !== null) { + msg.obsState.recording = session.obsState.recording; + } + if (session.obsState.streaming !== null) { + msg.obsState.streaming = session.obsState.streaming; + } + if (session.obsState.virtualcam !== null) { + msg.obsState.virtualcam = session.obsState.virtualcam; + } + } + if (session.optimize !== false) { + msg.optimizedBitrate = parseInt(session.optimize) || 0; // not setting a bitrate; just letting them know what the optimized bitrate is. + if (needOptimize) { + session.rpcs[UUID].bandwidth = msg.optimizedBitrate; + } + } + return msg; +}; + +function getOBSDetails(callbackname = "details") { + if (session.disableOBS) { + return false; + } + if (!window.obsstudio) { + return; + } + + if (!("details" in session.obsState)) { + session.obsState.details = {}; + } + + var readOnlyFuncs = [ + "getControlLevel", + //"getStatus", + "getCurrentScene", + "getScenes" + //"getTransitions", + //"getCurrentTransition", + //"pluginVersion" + ]; + + var promises = {}; + promises.main = true; + + Object.keys(window.obsstudio).forEach(async key => { + try { + if (typeof window.obsstudio[key] === "function") { + if (readOnlyFuncs.includes(key)) { + try { + promises[key] = true; + window.obsstudio[key](function (out) { + var shortkey = key.replace("get", ""); + shortkey = shortkey[0].toLowerCase() + shortkey.slice(1); + session.obsState.details[shortkey] = out; + delete promises[key]; + if (!Object.keys(promises).length) { + session.obsStateSync(callbackname); + } + }); + } catch (e) { + delete promises[key]; + } + } + /* } else if (typeof window.obsstudio[key] === 'object'){ // none of these values I really need right now. + var shortkey = key.replace("get",""); + shortkey = shortkey[0].toLowerCase() + shortkey.slice(1); + session.obsState.details[shortkey] = window.obsstudio[key]; + } else { + var shortkey = key.replace("get",""); + shortkey = shortkey[0].toLowerCase() + shortkey.slice(1); + session.obsState.details[shortkey] = window.obsstudio[key]; */ + } + } catch (e) { + errorlog(e); + } + }); + delete promises.main; + if (!Object.keys(promises).length) { + session.obsStateSync(callbackname); + } +} + +function toggleOBSControls() { + toggle(getById("remoteOBSControl")); + if (getById("remoteOBSControl").style.display == "none") { + getById("modalBackdrop").innerHTML = ""; // Delete modal + getById("modalBackdrop").remove(); + } else { + getById("modalBackdrop").innerHTML = ""; // Delete modal + getById("modalBackdrop").remove(); + var modalTemplate = `
    `; + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + document.getElementById("modalBackdrop").addEventListener("click", toggleOBSControls); + } +} + +function toggleOBSControlsLock(ele, disable = null) { + const element = getById("remoteOBSControlContents"); + + // If disable is not specified, toggle based on current state + // Otherwise, use the provided value + const shouldDisable = disable !== null ? disable : + element.style.pointerEvents !== 'none'; + + element.style.pointerEvents = shouldDisable ? 'none' : 'auto'; + element.style.opacity = shouldDisable ? '0.6' : '1'; + ele.textContent = shouldDisable ? '🔒' : '🔓'; + + return shouldDisable; // returns the new state +} +function requestOBSAction(ele) { + if (session.disableOBS) { + return false; + } +} +function obsSceneChanged(event) { + log(event.detail.name); + getOBSDetails(); // contains obsStateSync +} +function obsVirtualcamStarted(event) { + session.obsState.virtualcam = true; + session.obsStateSync("virtualcam"); +} +function obsVirtualcamStopped(event) { + session.obsState.virtualcam = false; + session.obsStateSync("virtualcam"); +} +function obsStreamingStarted(event) { + session.obsState.streaming = true; + session.obsStateSync("streaming"); +} +function obsStreamingStopped(event) { + session.obsState.streaming = false; + session.obsStateSync("streaming"); +} +function obsRecordingStarted(event) { + session.obsState.recording = true; + session.obsStateSync("recording"); +} +function obsRecordingStopped(event) { + session.obsState.recording = false; + session.obsStateSync("recording"); +} +function obsSourceActiveChanged(event) { + warnlog("obsSourceActiveChanged"); + warnlog(event.detail); + + try { + if (typeof event === "boolean") { + var sourceActive = event; + } else if (typeof event.detail === "boolean") { + var sourceActive = event.detail; + } else if (typeof event.detail.active === "boolean") { + var sourceActive = event.detail.active; + } else { + var sourceActive = event.detail.active; + } + + if (typeof sourceActive === "undefined") { + return; + } // Just fail. + + if (session.obsState.sourceActive !== sourceActive) { + // only move forward if there is a change; the event likes to double fire you see. + session.obsState.sourceActive = sourceActive; + session.obsStateSync("sourceActive"); + } + } catch (e) { + errorlog(e); + } +} + +function obsSourceVisibleChanged(event) { + // accounts for visible in VDO.Ninja scene AND visible in OBS scene + warnlog("obsSourceVisibleChanged"); + warnlog(event.detail); + try { + if (typeof event === "boolean") { + var visibility = event; + } else if (typeof event.detail === "boolean") { + var visibility = event.detail; + } else if (typeof event.detail.visible === "boolean") { + var visibility = event.detail.visible; + } else { + var visibility = event.detail.visible; + } + + if (typeof visibility === "undefined") { + // fall back + if (typeof document.visibilityState !== "undefined") { + visibility = document.visibilityState === "visible"; // modern + } else if (typeof document.hidden !== "undefined") { + visibility = !document.hidden; // legacy + } else { + return; // ... unknown input? fail. + } + } + + if (session.obsState.visibility !== visibility) { + // only move forward if there is a change; the event likes to double fire you see. + session.obsState.visibility = visibility; + session.obsStateSync("visibility"); + } + } catch (e) { + errorlog(e); + } +} + +function manageSceneState(data, UUID) { + // incoming obs details + if (session.disableOBS) { + return; + } + var processNeeded = false; + try { + if ("sceneDisplay" in data) { + processNeeded = true; + session.pcs[UUID].sceneDisplay = data.sceneDisplay; + } + if ("sceneMute" in data) { + processNeeded = true; + session.pcs[UUID].sceneMute = data.sceneMute; + } + + if (data.obsState) { + if ("sourceActive" in data.obsState) { + processNeeded = true; + session.pcs[UUID].obsState.sourceActive = data.obsState.sourceActive; + } + if ("visibility" in data.obsState) { + processNeeded = true; + session.pcs[UUID].obsState.visibility = data.obsState.visibility; + session.optimizeBitrate(UUID); // &optimize flag; sets video bitrate to target value if this flag == HIDDEN (if optimize=0, disables both audio and video) + } + if ("details" in data.obsState) { + //if (Object.keys(data.obsState.details).length){ + processNeeded = true; + session.pcs[UUID].obsState.details = data.obsState.details; + //} + } + if ("streaming" in data.obsState) { + processNeeded = true; + session.pcs[UUID].obsState.streaming = data.obsState.streaming; + } + if ("recording" in data.obsState) { + processNeeded = true; + session.pcs[UUID].obsState.recording = data.obsState.recording; + } + if ("virtualcam" in data.obsState) { + processNeeded = true; + session.pcs[UUID].obsState.virtualcam = data.obsState.virtualcam; + } + } + } catch (e) { + errorlog(e); + } + + if (processNeeded) { + log(data); + applySceneState(); + } else { + return; + } + + if (isIFrame) { + pokeIframeAPI("obs-state", data.obsState, UUID); + } + + if (session.obsControls === false) { + return; + } + + try { + var control = 0; + if (session.pcs[UUID].obsState && session.pcs[UUID].obsState.details) { + control = parseInt(session.pcs[UUID].obsState.details.controlLevel) || 0; //0 for NONE, 1 for READ_OBS (OBS data), 2 for READ_USER (User data), 3 for BASIC, 4 for ADVANCED and 5 for ALL + } + + if (control >= 4) { + if (session.director || !session.roomid) { + if (session.pcs[UUID].remote) { + if (session.obsControls !== false) { + getById("obscontrolbutton").classList.remove("hidden"); // so they get a tip. + } + } + } + } + + var multi = false; + getById("obsControlButtons") + .querySelectorAll("[data-system]") + .forEach(ele => { + if (ele.dataset.system in session.pcs) { + if (ele.dataset.system !== UUID) { + multi = true; + } + } else { + // delete, since no longer active. + ele.remove(); + } + }); + + getById("obsSceneNames") + .querySelectorAll("[data-system]") + .forEach(ele => { + if (ele.dataset.system in session.pcs) { + if (ele.dataset.system !== UUID) { + multi = true; + } + } else { + // delete, since no longer active. + ele.remove(); + } + }); + + if (control == 0) { + var obsControlButtonsBox = getById("obsControlButtons").querySelector("[data-system='" + UUID + "']"); + if (obsControlButtonsBox) { + obsControlButtonsBox.remove(); + } + var obsSceneNamesBox = getById("obsSceneNames").querySelector("[data-system='" + UUID + "']"); // this hides if less than 2, so hide it now. + if (obsSceneNamesBox) { + obsSceneNamesBox.remove(); + } + if (!multi) { + getById("obsControlHelp").classList.remove("hidden"); + } + return; + } + + getById("obsControlHelp").classList.add("hidden"); + + var obsControlButtonsBox = getById("obsControlButtons").querySelector("[data-system='" + UUID + "']"); + if (!obsControlButtonsBox) { + obsControlButtonsBox = document.createElement("div"); + obsControlButtonsBox.dataset.system = UUID; + getById("obsControlButtons").appendChild(obsControlButtonsBox); + } else { + obsControlButtonsBox.innerHTML = ""; + } + + if (multi) { + var h3 = document.createElement("h3"); + h3.innerText = "OBS instance: " + (session.pcs[UUID].label || session.pcs[UUID].scene || UUID); + obsControlButtonsBox.appendChild(h3); + } + + if (session.pcs[UUID].obsState && "streaming" in session.pcs[UUID].obsState) { + var controlButton = document.createElement("button"); + controlButton.dataset.UUID = UUID; + + if (session.pcs[UUID].obsState.streaming) { + controlButton.classList.add("pressed"); + controlButton.ariaPressed = "true"; + controlButton.dataset.obsAction = "stopStreaming"; + controlButton.innerText = "📡 stop streaming"; + controlButton.classList.remove("hidden"); + } else if (session.pcs[UUID].obsState.streaming === false) { + controlButton.classList.remove("hidden"); + controlButton.dataset.obsAction = "startStreaming"; + controlButton.innerText = "📡 start streaming"; + } else { + controlButton.dataset.obsAction = "startStreaming"; + controlButton.innerText = "📡 start streaming"; + controlButton.classList.remove("hidden"); + } + + if (control < 5) { + controlButton.disabled = true; + controlButton.style.cursor = "not-allowed"; + controlButton.title = "Source is lacking required permissions."; + } else { + controlButton.onclick = async function () { + var msg = {}; + msg.obsCommand = {}; + msg.obsCommand.action = this.dataset.obsAction; + msg.UUID = this.dataset.UUID; + if (document.querySelector("#obsRemotePassword>input") && document.querySelector("#obsRemotePassword>input").value) { + msg.remote = document.querySelector("#obsRemotePassword>input").value; + } else { + msg.remote = session.remote; + } + msg = await session.encodeRemote(msg); + session.anysend(msg); // this is neat, but doesn't work with websocket. I need to add + log("action request: " + this.dataset.obsAction); + }; + } + obsControlButtonsBox.appendChild(controlButton); + } + if (session.pcs[UUID].obsState && "recording" in session.pcs[UUID].obsState) { + var controlButton = document.createElement("button"); + controlButton.dataset.UUID = UUID; + + if (session.pcs[UUID].obsState.recording) { + controlButton.classList.add("pressed"); + controlButton.ariaPressed = "true"; + controlButton.dataset.obsAction = "stopRecording"; + controlButton.innerText = "📽 stop recording"; + controlButton.classList.remove("hidden"); + } else if (session.pcs[UUID].obsState.recording === false) { + controlButton.classList.remove("hidden"); + controlButton.dataset.obsAction = "startRecording"; + controlButton.innerText = "📽 start recording"; + } else { + controlButton.classList.remove("hidden"); + controlButton.dataset.obsAction = "startRecording"; + controlButton.innerText = "📽 start recording"; + } + + if (control < 5) { + controlButton.disabled = true; + controlButton.style.cursor = "not-allowed"; + controlButton.title = "Source is lacking required permissions."; + } else { + controlButton.onclick = async function () { + var msg = {}; + msg.obsCommand = {}; + msg.obsCommand.action = this.dataset.obsAction; + msg.UUID = this.dataset.UUID; + if (document.querySelector("#obsRemotePassword>input").value) { + msg.remote = document.querySelector("#obsRemotePassword>input").value; + } else { + msg.remote = session.remote; + } + msg = await session.encodeRemote(msg); + session.anysend(msg); + log("action request: " + this.dataset.obsAction); + }; + } + obsControlButtonsBox.appendChild(controlButton); + } + if (session.pcs[UUID].obsState && "virtualcam" in session.pcs[UUID].obsState) { + var controlButton = document.createElement("button"); + + controlButton.dataset.UUID = UUID; + + if (session.pcs[UUID].obsState.virtualcam) { + controlButton.classList.add("pressed"); + controlButton.ariaPressed = "true"; + controlButton.dataset.obsAction = "stopVirtualcam"; + controlButton.innerText = "💻 stop virtualcam"; + controlButton.classList.remove("hidden"); + } else if (session.pcs[UUID].obsState.virtualcam === false) { + controlButton.classList.remove("hidden"); + controlButton.dataset.obsAction = "startVirtualcam"; + controlButton.innerText = "💻 start virtualcam"; + } else { + controlButton.classList.remove("hidden"); + controlButton.dataset.obsAction = "startVirtualcam"; + controlButton.innerText = "💻 start virtualcam"; + } + + if (control < 5) { + controlButton.disabled = true; + controlButton.style.cursor = "not-allowed"; + controlButton.title = "Source is lacking required permissions."; + } else { + controlButton.onclick = async function () { + var msg = {}; + msg.obsCommand = {}; + msg.obsCommand.action = this.dataset.obsAction; + msg.UUID = this.dataset.UUID; + if (document.querySelector("#obsRemotePassword>input").value) { + msg.remote = document.querySelector("#obsRemotePassword>input").value; + } else { + msg.remote = session.remote; + } + msg = await session.encodeRemote(msg); + session.anysend(msg); + log("action request: " + this.dataset.obsAction); + }; + } + obsControlButtonsBox.appendChild(controlButton); + } + } catch (e) { + errorlog(e); + } // just in case the client has disconnected. + + if (control < 2) { + var obsSceneNamesBox = getById("obsSceneNames").querySelector("[data-system='" + UUID + "']"); + if (obsSceneNamesBox) { + obsSceneNamesBox.remove(); + } + return; + } + + var obsSceneNamesBox = getById("obsSceneNames").querySelectorAll("div[data-system='" + UUID + "']"); + if (!obsSceneNamesBox.length) { + obsSceneNamesBox = document.createElement("div"); + obsSceneNamesBox.dataset.system = UUID; + getById("obsSceneNames").appendChild(obsSceneNamesBox); + } else { + obsSceneNamesBox = obsSceneNamesBox[0]; + obsSceneNamesBox.innerHTML = ""; + } + + if (multi) { + var h3 = document.createElement("h3"); + h3.innerText = "OBS instance: " + (session.pcs[UUID].label || session.pcs[UUID].scene || UUID); + obsSceneNamesBox.appendChild(h3); + } + + if (session.pcs[UUID].obsState.details) { + var details = session.pcs[UUID].obsState.details; + if (details.scenes) { + details.scenes.forEach(scene => { + var sceneButton = document.createElement("button"); + sceneButton.dataset.obsScene = scene; + sceneButton.dataset.UUID = UUID; + sceneButton.innerText = scene; + if (details.currentScene && details.currentScene.name && details.currentScene.name === scene) { + sceneButton.classList.add("pressed"); + sceneButton.ariaPressed = "true"; + } + obsSceneNamesBox.appendChild(sceneButton); + if (control < 4) { + sceneButton.disabled = true; + sceneButton.style.cursor = "not-allowed"; + sceneButton.title = "Source is lacking required permissions."; + } else { + sceneButton.onclick = async function () { + var msg = {}; + msg.obsCommand = { action: "setCurrentScene", value: this.dataset.obsScene }; + msg.UUID = this.dataset.UUID; + if (document.querySelector("#obsRemotePassword>input").value) { + msg.remote = document.querySelector("#obsRemotePassword>input").value; + } else { + msg.remote = session.remote; + } + msg = await session.encodeRemote(msg); + session.anysend(msg); + log("scene change request: " + this.dataset.obsScene); + }; + } + }); + } + } + getById("debugRemoteOBSControl").innerText = JSON.stringify(session.pcs[UUID].obsState); +} + +function processOBSCommand(msg) { + if (session.disableOBS) { + return false; + } else if (!window.obsstudio) { + return false; + } else if (typeof msg.obsCommand !== "object") { + return false; + } else if ("remote" in msg) { + if ((msg.remote === session.remote && session.remote) || session.remote === true) { + // approved + } else { + if (msg.UUID && msg.obsCommand.action) { + var data = {}; + data.rejected = "obsCommand"; + //data.debug = msg.remote; + session.sendRequest(data, msg.UUID); // this skips the server + } + warnlog("Denied access; remote does not match"); + return false; + } + } else { + if (msg.UUID && msg.obsCommand.action) { + var data = {}; + data.rejected = "obsCommand"; + //data.debug = "no remote code provided"; + session.sendRequest(data, msg.UUID); // this skips the server + } + return false; + } + + try { + // {changeScene: this.dataset.obsScene} + if (msg.obsCommand.action && typeof msg.obsCommand.action == "string") { + + if (msg.obsCommand.action == "stopVirtualcam" || msg.obsCommand.action == "startVirtualcam") { + if (session.obsState.virtualcam === false) { + if (msg.UUID) { + var data = {}; + data.rejected = msg.obsCommand.action; + session.sendRequest(data, msg.UUID); // this skips the server + } + return false; + } + } + if (msg.obsCommand.action == "stopRecording" || msg.obsCommand.action == "startRecording") { + if (session.obsState.recording === false) { + if (msg.UUID) { + var data = {}; + data.rejected = msg.obsCommand.action; + session.sendRequest(data, msg.UUID); // this skips the server + } + return false; + } + } + if (msg.obsCommand.action == "stopStreaming" || msg.obsCommand.action == "startStreaming") { + if (session.obsState.streaming === false) { + if (msg.UUID) { + var data = {}; + data.rejected = msg.obsCommand.action; + session.sendRequest(data, msg.UUID); // this skips the server + } + return false; + } + } + + if (msg.obsCommand.value && typeof msg.obsCommand.value == "string") { + if (msg.obsCommand.action == "setCurrentScene" && session.filterOBSscenes && session.filterOBSscenes.length) { + try { + if (!session.filterOBSscenes.includes(msg.obsCommand.value)) { + return false; + } + } catch (e) { + errorlog(e); + return false; + } + } + window.obsstudio[msg.obsCommand.action](msg.obsCommand.value); + } else { + window.obsstudio[msg.obsCommand.action](); + } + } + } catch (e) { + errorlog(e); + return false; + } + return true; +} + +function applySceneState() { + // guest side; tally light, etc. + + if (document.getElementById("videosource")) { + var visibility = false; + var ondeck = false; + var recording = false; + var tallyStyle = session.tallyStyle; + if (!tallyStyle && session.tallyStyleDefault) { + tallyStyle = session.tallyStyleDefault; + } + + if (session.tallyOverride !== false) { + if (session.tallyOverride == 1) { + recording = true; + } else if (session.tallyOverride == 2) { + ondeck = true; + visibility = false; + recording = false; + } else if (session.tallyOverride == 3) { + visibility = true; + recording = false; + } else if (session.tallyOverride == 0) { + ondeck = false; + visibility = false; + recording = false; + } else { + // maybe its a custom message or default? + } + if (!session.cleanOutput) { + getById("obsState").classList.remove("hidden"); + } + } else if (!session.disableOBS) { + for (var uid in session.pcs) { + if (session.pcs[uid].obsState.sourceActive !== false && session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) { + visibility = true; + } else if (session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) { + ondeck = true; + } + if ((session.pcs[uid].obsState.recording || session.pcs[uid].obsState.streaming) && session.pcs[uid].obsState.sourceActive !== false && session.pcs[uid].obsState.visibility && session.pcs[uid].sceneDisplay !== false) { + // the scene that is recording must be visible also. + recording = true; + } + } + if (!session.cleanOutput) { + getById("obsState").classList.remove("hidden"); + } + } else { + return; + } + + if (recording) { + getById("obsState").classList.remove("ondeck"); + getById("obsState").classList.add("recording"); // TODO: this needs to check all peers to make sure it's valid + getById("obsState").innerHTML = "ON AIR"; + + if (tallyStyle) { + getById("main").classList.remove("ondeck"); + getById("main").classList.add("recording"); + } + } else if (visibility) { + getById("obsState").classList.remove("recording"); + getById("obsState").classList.remove("ondeck"); + getById("obsState").innerHTML = "ACTIVE"; + + if (tallyStyle) { + // only show active if its tally is enabled manually + getById("main").classList.remove("recording"); + getById("main").classList.remove("ondeck"); + } else { + getById("obsState").classList.add("hidden"); // most people don't care about being active + } + } else if (ondeck) { + getById("obsState").classList.remove("recording"); + getById("obsState").classList.add("ondeck"); // TODO: this needs to check all peers to make sure it's valid + getById("obsState").innerHTML = "STAND BY"; + + if (tallyStyle) { + getById("main").classList.remove("recording"); + getById("main").classList.add("ondeck"); + } + } else { + getById("obsState").classList.remove("recording"); + getById("obsState").classList.remove("ondeck"); + getById("obsState").innerHTML = "INACTIVE"; + getById("obsState").classList.add("hidden"); // I don't think most people care to see inactive. + + if (tallyStyle) { + getById("main").classList.remove("recording"); + getById("main").classList.remove("ondeck"); + } + } + + //miniTranslate(getById("obsState")); + + if (visibility) { + // BASIC TALLY LIGHT (on deck disabled) + getById("obsState").classList.add("onair"); // LIVE + if (tallyStyle) { + getById("main").classList.add("onair"); + } + } else { + getById("obsState").classList.remove("onair"); + if (tallyStyle) { + getById("main").classList.remove("onair"); + } + } + + if (session.automute) { + if (!visibility) { + session.micIsolatedAutoMute = []; + if (session.automute !== "2") { + for (var uid in session.pcs) { + if (session.directorList.indexOf(uid) >= 0) { + // allow validated directors to hear the guest + session.micIsolatedAutoMute.push(uid); + } + } + } + } else { + session.micIsolatedAutoMute = false; + } + session.applyIsolatedChat(); + } + } +} + +function compare_vids(a, b) { + var aa = a.order || 0; + var bb = b.order || 0; + if (aa < bb) { + return 1; + } + if (aa > bb) { + return -1; + } + return 0; +} + +function compare_vids_sid(a, b) { + var aa = a.dataset.sid || 0; + var bb = b.dataset.sid || 0; + if (aa > bb) { + return 1; + } + if (aa < bb) { + return -1; + } + return 0; +} +function compare_vids_label(a, b) { + if (a.dataset.UUID && session.rpcs[a.dataset.UUID] && session.rpcs[a.dataset.UUID].label) { + var aa = session.rpcs[a.dataset.UUID].label.toLowerCase(); + } else { + var aa = 0; + } + + if (b.dataset.UUID && session.rpcs[b.dataset.UUID] && session.rpcs[b.dataset.UUID].label) { + var bb = session.rpcs[b.dataset.UUID].label.toLowerCase(); + } else { + var bb = 0; + } + + if (aa > bb) { + return 1; + } + if (aa < bb) { + return -1; + } + return 0; +} + +function sortByZ(mediaPool, layout) { + function sortABZ(a, b) { + if (layout[a.dataset.sid]) { + var aa = layout[a.dataset.sid].zIndex || layout[a.dataset.sid].z || 0; + } else { + var aa = 0; + } + if (layout[b.dataset.sid]) { + var bb = layout[b.dataset.sid].zIndex || layout[b.dataset.sid].z || 0; + } else { + var bb = 0; + } + if (aa < bb) { + return -1; + } + if (aa > bb) { + return 1; + } + return 0; + } + mediaPool.sort(sortABZ); + return mediaPool; +} + +window.onpopstate = function () { + if (session.firstPlayTriggered) { + window.location.reload(true); // deprecated, but it seems to work, so w/e + } +}; + +var miniPerformerX = null; +var miniPerformerY = null; +function makeMiniDraggableElement(elmnt) { + if (session.disableMouseEvents) { + return; + } + + try { + elmnt.dragElement = false; + // elmnt.style.bottom = "auto"; + elmnt.style.cursor = "grab"; + + elmnt.stashonmouseup = null; + elmnt.stashonmousemove = null; + } catch (e) { + errorlog(e); + return; + } + + var pos1 = 0; + var pos2 = 0; + var pos3 = 0; + var pos4 = 0; + + var timestamp = false; + + function elementDrag(e) { + // ON DRAG + timestamp = false; + if (session.infocus) { + return; + } + try { + e = e || window.event; + + if (e.type !== "touchmove") { + if ("buttons" in e && e.buttons !== 1) { + closeDragElement(e); + return; + } + e.preventDefault(); + } + e.stopPropagation(); + + elmnt.dragElement = true; + + if (e.type === "touchmove") { + pos1 = pos3 - e.touches[0].clientX; + pos2 = pos4 - e.touches[0].clientY; + pos3 = e.touches[0].clientX; + pos4 = e.touches[0].clientY; + } else { + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + } + + var topDrag = elmnt.offsetTop - pos2; + if (topDrag > -3 + (window.innerHeight - elmnt.clientHeight)) { + topDrag = -3 + (window.innerHeight - elmnt.clientHeight); + } + + miniPerformerY = topDrag; + miniPerformerX = elmnt.offsetLeft - pos1; + + if (miniPerformerY > window.innerHeight - elmnt.clientHeight) { + miniPerformerY = window.innerHeight - elmnt.clientHeight; + } + if (miniPerformerX > window.innerWidth - elmnt.clientWidth) { + miniPerformerX = window.innerWidth - elmnt.clientWidth; + } + + miniPerformerX = (100 * miniPerformerX) / window.innerWidth; + miniPerformerY = (100 * miniPerformerY) / window.innerHeight; + + if (session.widget && !session.leftMiniPreview) { + if (miniPerformerX > 74) { + miniPerformerX = 74; + } + } + + if (miniPerformerY < 0) { + miniPerformerY = 0; + } else if (miniPerformerY > 100) { + miniPerformerY = 100; + } + if (miniPerformerX < 0) { + miniPerformerX = 0; + } else if (miniPerformerX > 100) { + miniPerformerX = 100; + } + + elmnt.style.right = "unset"; + elmnt.style.top = miniPerformerY + "%"; + elmnt.style.left = miniPerformerX + "%"; + } catch (e) { + errorlog(e); + } + } + + function closeDragElement(e) { + // TOUCH END + e = e || window.event; + + if (e.type !== "touchend") { + if (e.button !== 0) { + return; + } + document.onmouseup = elmnt.stashonmouseup; + document.onmousemove = elmnt.stashonmousemove; + elmnt.onmouseleave = null; + } + + if (session.infocus) { + return; + } + e.preventDefault(); + + if (timestamp && Date.now() - timestamp > 500) { + // long hold, so this is a drag + e.stopPropagation(); + if (e.type === "touchend") { + if (session.infocus === true) { + session.infocus = false; + } else { + session.infocus = true; + log("session: myself"); + } + setTimeout(() => updateMixer(), 10); + } + } else if (timestamp && e.type !== "touchend") { + if (session.infocus === true) { + session.infocus = false; + } else { + session.infocus = true; + log("session: myself"); + } + setTimeout(() => updateMixer(), 10); + } + } + + function dragMouseDown(e) { + ////// TOUCH START + + if (event.ctrlKey || event.metaKey) { + return; + } + + timestamp = Date.now(); + + e = e || window.event; + if (session.infocus) { + return; + } + + e.preventDefault(); + if (e.type === "touchstart") { + pos3 = e.touches[0].clientX; + pos4 = e.touches[0].clientY; + + elmnt.ontouchend = closeDragElement; + elmnt.ontouchmove = elementDrag; + } else { + if (e.button !== 0) { + return; + } + pos3 = e.clientX; + pos4 = e.clientY; + elmnt.stashonmouseup = document.onmouseup; // I don't want to interfere with other drag events. + elmnt.stashonmousemove = document.onmousemove; + + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + elmnt.onmouseleave = function (event) { + closeDragElement(event); + }; + } + } + + elmnt.onmousedown = dragMouseDown; + elmnt.ontouchstart = dragMouseDown; +} + +function makeDraggableElement(element) { + if (session.disableMouseEvents) { + return; + } // this is here for a reason. :P + if (!element) { + return; + } + element.initialX; + element.initialY; + element.currentX; + element.xOffset = 0; + element.currentY; + element.yOffset = 0; + element.isDragging = false; + element.dragElement = true; + + element.addEventListener("mousedown", dragStart); + + function dragStart(e) { + element.initialX = e.clientX - element.xOffset; + element.initialY = e.clientY - element.yOffset; + + document.addEventListener("mousemove", drag); + document.addEventListener("mouseup", dragEnd); + document.addEventListener("onmouseleave", dragEnd); + document.addEventListener("onmouseenter", dragEnd); + + element.isDragging = true; + } + + function dragEnd(e) { + element.initialX = element.currentX; + element.initialY = element.currentY; + + document.removeEventListener("mousemove", drag); + document.removeEventListener("mouseup", dragEnd); + document.removeEventListener("onmouseleave", dragEnd); + document.removeEventListener("onmouseenter", dragEnd); + + element.isDragging = false; + } + + function drag(e) { + if (element.isDragging) { + element.currentX = e.clientX - element.initialX; + element.currentY = e.clientY - element.initialY; + + // Get the dimensions of the viewport + let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); + let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); + + // Get the dimensions of the object + let elementWidth = element.offsetWidth; + let elementHeight = element.offsetHeight; + // console.log('elementWidth:\n',elementWidth) + // console.log('elementHeight:\n',elementHeight) + + // Calculate the boundaries + let maxX = vw - elementWidth; + let maxY = vh - elementHeight; + let minX = 0; + let minY = 0; + + // Calculate real boundaries (parent position: fixed issues) + let topOffset = 0; + let leftOffset = 0; + let elementOffset = element; + while (elementOffset) { + topOffset += elementOffset.offsetTop; + leftOffset += elementOffset.offsetLeft; + elementOffset = elementOffset.offsetParent; + } + + // Adjust the position if it's going beyond the boundaries + let realX = element.currentX + leftOffset; + let realY = element.currentY + topOffset; + + if (realX > maxX) { + element.currentX = maxX - leftOffset; + } else if (realX < minX) { + element.currentX = minX - leftOffset; + } + + if (realY > maxY) { + element.currentY = maxY - topOffset; + } else if (realY < minY) { + element.currentY = minY - topOffset; + } + // Update the position and offset + element.xOffset = element.currentX; + element.yOffset = element.currentY; + + element.style.transform = `translate(${element.currentX}px, ${element.currentY}px)`; + } + } +} + +function clearCacheForCurrentSite() { + if ('caches' in window) { + try { + caches.keys().then(function (cacheNames) { + cacheNames.forEach(function (cacheName) { + caches.delete(cacheName); + }); + }); + log("Cache cleared for current site"); + } catch (e) { } + } else { + warnlog("Cache API not supported"); + } +} + +function removeStorage(cname) { + localStorage.removeItem(cname); +} + +function clearStorage() { + localStorage.clear(); + //clearCacheForCurrentSite(); // cache as well. + if (!session.cleanOutput) { + warnUser("The local storage and saved settings have been cleared", 1000); + } +} + +function setStorage(cname, cvalue, hours = 9999) { + // not actually a cookie + var now = new Date(); + var item = { + value: cvalue, + expiry: now.getTime() + hours * 60 * 60 * 1000 + }; + try { + localStorage.setItem(cname, JSON.stringify(item)); + } catch (e) { + errorlog(e); + } +} + +function getStorage(cname) { + try { + var itemStr = localStorage.getItem(cname); + } catch (e) { + errorlog(e); + return; + } + if (!itemStr) { + return ""; + } + var item = JSON.parse(itemStr); + var now = new Date(); + if (now.getTime() > item.expiry) { + localStorage.removeItem(cname); + return ""; + } + return item.value; +} + +function play(streamid = null, UUID = false) { + // play whatever is in the URL params; or filter by a streamID option + log("play stream: " + session.view + " " + streamid); + + if (session.viewDirectorOnly) { + if (!(UUID || streamid)) { + warnlog("No UUID and StreamID"); + return; + } else if (session.directorList.indexOf(UUID) == -1) { + warnlog("Not a director"); + return; + } + } + + if (session.view_set) { + var played = false; + for (var j in session.view_set) { + if (streamid === null) { + // play what is in the view list ; not a group room probably + session.watchStream(session.view_set[j]); + played = true; + } else if (streamid === session.view_set[j]) { + // plays if the group room list matches the explicit list + session.watchStream(session.view_set[j]); + played = true; + } + } + if (session.include) { + session.include.forEach(sid => { + if (session.view_set.includes(sid)) { + // already played + } else if (streamid === null) { + // play what is in the view list ; not a group room probably + session.watchStream(sid); + } else if (streamid === sid) { + // plays if the group room list matches the explicit list + session.watchStream(sid); + played = true; + } + }); + } + + if (!played && streamid) { + if (session.scene !== false) { + if (!session.permaid) { + if (!session.queue) { + // I don't want to deal with queues. + if (session.exclude === false || !session.exclude.includes(streamid)) { + if (UUID) { + if (session.directorList.indexOf(UUID) >= 0) { + warnlog("stream ID added to badStreamList: " + streamid); + session.badStreamList.push(streamid); + // if I uncomment this, the director can mute the solo link. + // session.watchStream(streamid); // changed June 4th 2024. We shouldn't be viewing the stream if on the bad list, no? + } + } + } + } + } + } + } + } else if (streamid && session.exclude !== false) { + if (session.exclude.includes(streamid)) { + // we don't play it at all. (if explicity listed as VIDEO, then OKay.) + } else { + session.watchStream(streamid); // I suppose we do play it. + } + } else if (streamid) { + if (session.optimize === 0) { + log("Running special optimize===logic logic for loading rtc connections"); + try { + // must be a scene and not an auto scene + if (session.scene && session.activatedStreams.size) { + if (session.activatedStreams.has(streamid)) { + session.watchStream(streamid); + return; + } + } + if (UUID && streamid) { + if (session.directorList.indexOf(UUID) >= 0) { + session.watchStream(streamid); + } + } + } catch (e) { + errorlog(e); + } + } else { + session.watchStream(streamid); + } + } else if (session.include.length) { + session.include.forEach(sid => { + session.watchStream(sid); + }); + } +} + +function nextQueue() { + if (!session.queue) { + return; + } + if (!session.director) { + return; + } + if (session.queueList.length == 0) { + getById("queuebutton").classList.add("red"); + setTimeout(function () { + getById("queuebutton").classList.remove("red"); + }, 50); + return; + } + var nextStream = session.queueList.shift(); + + getById("queuebutton").classList.add("red"); + setTimeout(function () { + getById("queuebutton").classList.remove("red"); + }, 200); + + updateQueue(); + + session.watchStream(nextStream); + log("next stream loading: " + nextStream); +} + +function updateQueue(adding = false) { + if (!session.queue) { + return; + } + if (!session.director) { + return; + } + if (session.queueList.length) { + if (session.queueList.length > 10) { + getById("queueNotification").innerHTML = "‼"; + } else { + getById("queueNotification").innerHTML = session.queueList.length; + } + getById("queueNotification").classList.add("queueNotification"); + } else { + getById("queueNotification").innerHTML = ""; + getById("queueNotification").classList.remove("queueNotification"); + } + + // Keep the toolbar button visually hot while guests are waiting + var queueButton = getById("queuebutton"); + if (queueButton) { + if (session.queueList.length) { + queueButton.classList.add("queueAttention"); + } else { + queueButton.classList.remove("queueAttention"); + } + } + var queueBadge = getById("queueNotification"); + if (queueBadge) { + if (session.queueList.length) { + queueBadge.classList.add("queueNotificationPulse"); + } else { + queueBadge.classList.remove("queueNotificationPulse"); + } + } + + if (adding) { + if (session.beepToNotify) { + // Favor the louder knock tone when approvals are enabled + playtone(false, session.knockToneEnabled ? "knocktone" : "testtone"); + showNotification("someone joined the queue", "queue length: " + session.queueList.length); + } + getById("queuebutton").classList.remove("shake"); + setTimeout(function () { + getById("queuebutton").classList.add("shake"); + }, 10); + } +} + +function hideStreamLowBandwidth(bandwidth, UUID) { + if (session.lowBitrateCutoff === false) { // allow 0,but I should probably also do this if there is a disconnect. + return; + } + + if (session.directorList.includes(UUID) || session.rpcs[UUID].director) { + if (session.showDirector || session.rpcs[UUID].showDirector) { + // all good + } else { + return; // we don't include the director since not treated as a guest + } + } + + if (bandwidth <= session.lowBitrateCutoff) { + //log("bandwidth <= session.lowBitrateCutoff"); + log("actual bandwidth: " + bandwidth + " < bandwidth cut threshold: " + session.lowBitrateCutoff); + // <= used so 0 can be used as a trigger + if (session.lowBitrateSceneChange) { + changeSceneLowBandwidth(true); + } else if (!session.rpcs[UUID].bandwidthMuted) { + session.rpcs[UUID].bandwidthMuted = true; + updateMixer(); + } + } else if (session.lowBitrateSceneChange) { + log("changeSceneLowBandwidth(false)"); + changeSceneLowBandwidth(false); + } else if (session.rpcs[UUID].bandwidthMuted) { + session.rpcs[UUID].bandwidthMuted = false; + if (session.rpcs[UUID].videoElement) { + session.rpcs[UUID].videoElement.muted = checkMuteState(UUID); + } + updateMixer(); + } +} + +var changeSceneEnabled = false; +var changeSceneLowBandwidthRevert = false; +function changeSceneLowBandwidth(state) { + if (!session.lowBitrateSceneChange) { + return; + } + if (!session.obsState) { + return; + } + try { + if (session.obsState.sourceActive && session.obsState.details && session.obsState.details.currentScene) { + + changeSceneLowBandwidthRevert = session.obsState.details.currentScene.name || false; + } else if ("sourceActive" in session.obsState && !session.obsState.sourceActive && session.obsState.details && session.obsState.details.currentScene) { + if (session.obsState.details.currentScene.name !== session.lowBitrateSceneChange) { + return; // not the FML scene, nor are we visible, so we're not going to switch back. Assume the user has overtaken the setup. + } + } + if (!window.obsstudio || !window.obsstudio["setCurrentScene"]) { + return; + } + if (state && changeSceneLowBandwidthRevert) { + if (changeSceneEnabled) { + // bitrate was higher , so we can now cut off. + //log("2 changeSceneEnabled: "+session.lowBitrateSceneChange+" changeSceneEnabled:"+changeSceneEnabled); + log("Changing to cut scene due to low bandwidth"); + window.obsstudio["setCurrentScene"](session.lowBitrateSceneChange); + } else { + log("Low bandwidth, but not changing scenes because threshold not hit at least once yet"); + } + } else if (changeSceneLowBandwidthRevert) { + changeSceneEnabled = true; + //log("1 setCurrentScene: "+changeSceneLowBandwidthRevert+" changeSceneEnabled:"+changeSceneEnabled); + log("Reverting to original scene due to good bandwidth"); + window.obsstudio["setCurrentScene"](changeSceneLowBandwidthRevert); + } + } catch (e) { + errorlog(e); + } +} + +function setupIncomingScreenTracking(v, UUID) { + // SCREEN element. + + if (session.directorList.indexOf(UUID) >= 0) { + v.muted = false; + } + + v.addEventListener( + "playing", + e => { + try { + var bigPlayButton = document.getElementById("bigPlayButton"); + if (bigPlayButton) { + bigPlayButton.parentNode.removeChild(bigPlayButton); + } + } catch (e) { } + + resetupAudioOut(e.target, true); + + try { + if (session.pip) { + if (v.readyState >= 3) { + if (!v.pip) { + v.pip = true; + toggleSystemPip(v, true); + } + } + } + } catch (e) { } + }, + { once: true } + ); + + v.onpause = event => { + // prevent things from pausing; human or other + + if (v.dataset.UUID && session.rpcs[v.dataset.UUID] && session.rpcs[v.dataset.UUID].manualBandwidth === 0) { + return true; + } + if (!(event.ctrlKey || event.metaKey)) { + warnlog("Video paused; force it to play again"); + //return; + //session.audioCtx.resume(); + //log("ctx resume"); + + event.currentTarget + .play() + .then(_ => { + log("playing 4"); + }) + .catch(error => { + warnlog("didnt play 1"); + }); + if (Firefox) { + unPauseVideo(v); + } + } + return true; + }; + + if (session.pip) { + v.onloadedmetadata = function () { + if (!v.paused) { + if (!v.pip) { + v.pip = true; + toggleSystemPip(v, true); + } + } + }; + } + + v.addEventListener("resize", e => { + // if the aspect ratio changes, then we might want to update the mixer. If audio only, then this doesn't matter. + var v = e.target; + var aspectRatio = parseFloat(v.videoWidth / v.videoHeight) || 0; + log("resize event: " + aspectRatio); + + if (!aspectRatio) { + v.resetAR = true; + return; + } // if Audio only, then we don't want to set or update any aspect ratio. + + if (v.resetAR) { + log("ASPECT RATIO UNMUTED"); + delete v.resetAR; + v.dataset.aspectRatio = aspectRatio; + pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); + setTimeout(function () { + updateMixer(); + }, 1); + } else if (v.dataset.aspectRatio) { + if (aspectRatio != parseFloat(v.dataset.aspectRatio)) { + log("ASPECT RATIO CHANGED"); + v.dataset.aspectRatio = aspectRatio; + pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); + setTimeout(function () { + updateMixer(); + }, 1); // We don't want to run this on the first resize? just subsequent ones. + } + } else { + log("NEW VIDEO ? ASPECT RATIO new"); + v.dataset.aspectRatio = aspectRatio; + pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); + setTimeout(function () { + updateMixer(); + }, 1); + } + }); + if (typeof session.volume == "number") { + v.volume = session.volume; + } else { + v.volume = 1.0; // play audio automatically + } + v.autoplay = true; + v.controls = session.showControls || false; + v.classList.add("tile"); + v.setAttribute("playsinline", ""); + v.controlTimer = null; + + v.dataset.menu = "context-menu-video"; + if (!session.cleanOutput) { + v.classList.add("task"); // this adds the right-click menu + } + + if (document.getElementById("mainmenu")) { + var m = getById("mainmenu"); + m.remove(); + document.querySelectorAll(".hidden2").forEach(ele2 => { + ele2.classList.remove("hidden2"); + }); + } + + if (session.director) { + if (session.showControls !== null) { + v.controls = session.showControls; + } else { + v.controls = true; + } + var container = getById("screenContainer_" + UUID); + v.container = container; + v.disablePictureInPicture = false; + v.setAttribute("controls", "controls"); + container.appendChild(v); + pokeIframeAPI("control-box-video-updated", v.id, UUID); + session.requestRateLimit(session.directorViewBitrate, UUID); /// limit resolution for director + v.title = "Hold CTRL or CMD (⌘) while clicking the video to open detailed stats"; + if (session.beepToNotify) { + playtone(); + } + } else if (session.scene !== false) { + v.controls = session.showControls || false; + + if (session.view) { + // specific video to be played + v.style.display = "block"; + } else if (session.scene === "0") { + // auto plays, right? + v.style.display = "block"; + } else { + // group scene I guess; needs to be added manually + v.style.display = "none"; + v.mutedStateScene = true; + } + + setTimeout(function () { + updateMixer(); + }, 1); + } else if (session.roomid !== false) { + if (session.cleanOutput) { + v.controls = session.showControls || false; + } else if (session.studioSoftware) { + v.controls = session.showControls || false; + } else if (session.showControls !== null) { + v.controls = session.showControls; + } else { + v.controls = true; + } + //if ((session.roomid==="") && (session.bitrate)){ + // let's keep the default bitrates, since this isn't a real room and bitrates are specified. + //} //else if (session.novideo !== false){ + // if (session.novideo.includes(session.rpcs[UUID].streamID)){ // no video will have muted the video already anyways. + // session.requestRateLimit(0,UUID, false);// optimizing audio here doesn't later get turned back on. let the automixer disable audio instead + // } + //} //else { + // session.requestRateLimit(0,UUID, false);//// optimizing audio here doesn't later get turned back on. let the automixer disable audio instead + //} + setTimeout(function () { + updateMixer(); + }, 1); + } else { + v.style.display = "block"; + setTimeout(function () { + updateMixer(); + }, 1); + } + + v.addEventListener("click", function (e) { + // show stats of video if double clicked + log("clicked"); + try { + var uid = e.currentTarget.dataset.UUID; + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + if (session.statsMenu !== false) { + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, uid); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + e.stopPropagation(); + return false; + } else if ("prePausedBandwidth" in session.rpcs[uid]) { + unPauseVideo(e.currentTarget); + } + } catch (e) { + errorlog(e); + } + }); + + if (session.statsMenu) { + if ("stats" in session.rpcs[UUID]) { + if (getById("menuStatsBox")) { + clearInterval(getById("menuStatsBox").interval); + getById("menuStatsBox").remove(); + } + + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, UUID); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID); + } + } + + v.touchTimeOut = null; + v.touchLastTap = 0; + v.touchCount = 0; + v.addEventListener("touchend", function (event) { + if (session.disableMouseEvents) { + return; + } + + log("touched"); + + //document.ontouchup = null; + //document.onmouseup = null; + document.onmousemove = null; + document.ontouchmove = null; + + var currentTime = new Date().getTime(); + var tapLength = currentTime - v.touchLastTap; + clearTimeout(v.touchTimeOut); + if (tapLength < 500 && tapLength > 0) { + /// + log("double touched"); + v.touchCount += 1; + event.preventDefault(); + if (v.touchCount < 5) { + v.touchLastTap = currentTime; + return false; + } + v.touchLastTap = 0; + v.touchCount = 0; + + log("double touched"); + if (session.statsMenu !== false) { + var uid = event.currentTarget.dataset.UUID; + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + + printViewStats(innerMenu, uid); + + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + event.stopPropagation(); + return false; + ////// + } else { + v.touchCount = 1; + v.touchTimeOut = setTimeout( + function (vv) { + clearTimeout(vv.touchTimeOut); + vv.touchLastTap = 0; + vv.touchCount = 0; + }, + 5000, + v + ); + v.touchLastTap = currentTime; + } + }); + + if (v.controls == false) { + v.addEventListener("click", function () { + if (v.paused) { + log("PLAYING MANUALLY?"); + v.play() + .then(_ => { + log("playing 5"); + }) + .catch(warnlog); + } + }); + if (session.nocursor == false) { + // we do not want to show the controls. This is because MacOS + OBS does not work; so electron app needs this. + if (!session.cleanOutput) { + if (session.studioSoftware) { + } else if (session.showControls === false) { + // explicitly disabled; default null. + } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + } else { + if (v.controlTimer) { + clearInterval(v.controlTimer); + } + v.controlTimer = setTimeout(showControlBar.bind(null, v), 1000); + //v.controlTimer = setTimeout(function (){v.controls=true;},3000); // 3 seconds before I enable the controls automatically. This way it doesn't auto appear during loading. 3s enough, right? + } + } + } + } + + //if (session.fadein){ + v.addEventListener("animationend", function (e) { + v.classList.remove("fadein"); // allows the video to fade in. + if (v.holder) { + v.holder.classList.remove("fadein"); + } + }); + // v.classList.add("fadein"); // allows the video to fade in. + // if (v.holder){ + // v.holder.classList.add("fadein"); + // } + //} + + applyMuteState(UUID); // TODO; needs to be specific to screen video + v.usermuted = false; + + v.addEventListener("volumechange", function (e) { + var muteState = checkMuteState(UUID); + if (this.muted && this.muted !== muteState) { + this.usermuted = 1; + } else if (!this.muted && this.muted !== muteState) { + this.usermuted = 2; + } else if (!this.muted) { + this.usermuted = false; + } + }); + + if (session.screenShareStartPaused) { + // we know this is a screen share already + pauseVideo(v, false); + } + + if (session.director) { + /* var wss = ""; + if (session.customWSS || session.wssSetViaUrl){ + if (session.customWSS && (session.customWSS!==true)){ + wss = "&pie="+session.customWSS; + } else if (session.customWSS==true){ + wss = "&wss=" + session.wss; + } else { + wss = "&wss2=" + session.wss; + } + } */ + /* + var codecGroupFlag=""; + if (session.codecGroupFlag){ + codecGroupFlag = session.codecGroupFlag; + } */ + + /* var passAdd2=""; + if (session.password){ + if (session.defaultPassword===false){ + passAdd2="&password="+session.password; + } + } */ + + if (session.customWSS && "isScene" in msg && msg.isScene !== false) { + // this is a scene, so lets not show it. + } else { + var soloLink = soloLinkGenerator(session.rpcs[UUID].streamID); + createControlBoxScreenshare(UUID, soloLink, session.rpcs[UUID].streamID); + } + } + + if (session.autorecord || session.autorecordremote) { + log("AUTO RECORD START"); + setTimeout( + function (UUID, v) { + var videoKbps = session.recordDefault; + if (session.recordLocal !== false) { + videoKbps = session.recordLocal; + } + + if (session.director) { + recordVideo(document.querySelector("[data-action-type='recorder-local'][data--u-u-i-d='" + UUID + "']"), null, videoKbps); + } else if (v.stopWriter || v.recording) { + } else if (v.startWriter) { + v.startWriter(); + } else { + recordLocalVideo(null, videoKbps, v); + } + }, + 2000, + UUID, + v + ); + } + if (session.rpcs[UUID]) { + clearTimeout(session.rpcs[UUID].getStatsTimeout); + session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, 100, UUID); + } +} + +function setupIncomingVideoTracking(v, UUID) { + // video element. + + if (session.directorList.indexOf(UUID) >= 0) { + v.muted = false; + } + + v.onpause = event => { + // prevent things from pausing; human or other + + if (v.dataset.UUID && session.rpcs[v.dataset.UUID] && session.rpcs[v.dataset.UUID].manualBandwidth === 0) { + return true; + } + + if (!CtrlPressed) { + warnlog("Video paused; force it to play again"); + //return; + //session.audioCtx.resume(); + //log("ctx resume"); + + event.currentTarget + .play() + .then(_ => { + log("playing 6"); + }) + .catch(error => { + warnlog("didnt play 1"); + }); + unPauseVideo(v); + } else if (Firefox && CtrlPressed) { + log("CLICK 351"); + if (session.statsMenu !== false) { + var uid = event.currentTarget.dataset.UUID; + event.preventDefault(); + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + + printViewStats(innerMenu, uid); + + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + event.stopPropagation(); + return false; + } + return true; + }; + + /* v.onerror = function(event){ + errorlog(event); + try{ + warnlog("Vidieo element threw an error; going to reconnect it"); + session.rpcs[UUID].videoElement.stop(); + session.rpcs[UUID].videoElement.srcObject = null; + session.rpcs[UUID].videoElement.srcObject = session.rpcs[UUID].streamSrc; // replaecd with updateIncomingVideoElement these days + session.rpcs[UUID].videoElement.play(); + setTimeout(function(){updateMixer();},1); + } catch(e){errorlog(e);} + } */ + + if (session.pip) { + v.onloadedmetadata = function () { + if (!v.paused) { + if (!v.pip) { + v.pip = true; + toggleSystemPip(v, true); + } + } + }; + } + + v.addEventListener("resize", e => { + var v = e.target; + var aspectRatio = parseFloat(v.videoWidth / v.videoHeight) || 0; + log("resize event: " + aspectRatio); + + if (!aspectRatio) { + v.resetAR = true; + return; + } // if Audio only, then we don't want to set or update any aspect ratio. + if (typeof v.manualRotate == "number") { + //v.rotated = v.manualRotate; // ((session.rotate || 0) + 90) % 360; + } else if (session.keepIncomingVideosInLandscape) { + if (aspectRatio < 1) { + // session.keepIncomingVideosInLandscape + v.rotated = session.keepIncomingVideosInLandscape; + } else { + v.rotated = 0; + } + } else if (session.keepIncomingVideosInPortrait) { + if (aspectRatio > 1) { + // session.keepIncomingVideosInLandscape + v.rotated = session.keepIncomingVideosInPortrait; + } else { + v.rotated = 0; + } + } + + if (v.resetAR) { + log("ASPECT RATIO UNMUTED"); + delete v.resetAR; + v.dataset.aspectRatio = aspectRatio; + pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); + setTimeout(function () { + updateMixer(); + }, 1); + } else if (v.dataset.aspectRatio) { + if (aspectRatio != parseFloat(v.dataset.aspectRatio)) { + log("ASPECT RATIO CHANGED"); + v.dataset.aspectRatio = aspectRatio; + pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); + setTimeout(function () { + updateMixer(); + }, 1); // We don't want to run this on the first resize? just subsequent ones. + } + } else { + log("NEW VIDEO ? ASPECT RATIO new"); + v.dataset.aspectRatio = aspectRatio; + pokeIframeAPI("aspect-ratio", v.dataset.aspectRatio, v.dataset.UUID, v.dataset.sid); + setTimeout(function () { + updateMixer(); + }, 1); + } + }); + + if (typeof session.volume == "number") { + v.volume = session.volume; + } else { + v.volume = 1.0; // play audio automatically + } + v.autoplay = true; + v.controls = session.showControls || false; + v.classList.add("tile"); + v.setAttribute("playsinline", ""); + v.controlTimer = null; + + v.dataset.menu = "context-menu-video"; + if (!session.cleanOutput) { + v.classList.add("task"); // this adds the right-click menu + } + + if (document.getElementById("mainmenu")) { + var m = getById("mainmenu"); + m.remove(); + document.querySelectorAll(".hidden2").forEach(ele2 => { + ele2.classList.remove("hidden2"); + }); + } + + if (session.director) { + if (session.showControls === false) { + v.controls = false; + } else { + v.controls = true; + } + var container = getById("videoContainer_" + UUID); + v.container = container; + v.disablePictureInPicture = false; + v.setAttribute("controls", "controls"); + container.appendChild(v); + pokeIframeAPI("control-box-video-updated", v.id, UUID); + container.classList.add("hasMedia"); + session.requestRateLimit(session.directorViewBitrate, UUID); /// limit resolution for director + v.title = "Hold CTRL or CMD (⌘) while clicking the video to open detailed stats"; + if (session.beepToNotify) { + playtone(); + } + } else if (session.scene !== false) { + v.controls = session.showControls || false; + + if (session.view) { + // specific video to be played + v.style.display = "block"; + } else if (session.scene === "0") { + // auto plays, right? + v.style.display = "block"; + } else if (session.scene !== false && session.autoadd && session.rpcs[UUID].streamID && session.autoadd.includes(session.rpcs[UUID].streamID)) { + /// session.autoadd + v.style.display = "block"; // auto added because manually added. + } else { + // group scene I guess; needs to be added manually + v.style.display = "none"; + session.rpcs[UUID].mutedStateScene = true; + } + } else if (session.roomid !== false) { + if (session.cleanOutput) { + v.controls = session.showControls || false; + } else if (session.studioSoftware) { + v.controls = session.showControls || false; + } else if (session.showControls !== null) { + v.controls = session.showControls; + } else { + v.controls = true; + } + } else { + v.style.display = "block"; + } + + v.addEventListener("click", function (e) { + // show stats of video if double clicked + log("clicked"); + try { + var uid = e.currentTarget.dataset.UUID; + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + if (session.statsMenu !== false) { + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, uid); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + e.stopPropagation(); + return false; + } else if ("prePausedBandwidth" in session.rpcs[uid]) { + unPauseVideo(e.currentTarget); + } + } catch (e) { + errorlog(e); + } + }); + + if (session.statsMenu) { + if ("stats" in session.rpcs[UUID]) { + if (getById("menuStatsBox")) { + clearInterval(getById("menuStatsBox").interval); + getById("menuStatsBox").remove(); + } + + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, UUID); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID); + } + } + + v.touchTimeOut = null; + v.touchLastTap = 0; + v.touchCount = 0; + v.addEventListener("touchend", function (event) { + if (session.disableMouseEvents) { + return; + } + + log("touched"); + + //document.ontouchup = null; + //document.onmouseup = null; + document.onmousemove = null; + document.ontouchmove = null; + + var currentTime = new Date().getTime(); + var tapLength = currentTime - v.touchLastTap; + clearTimeout(v.touchTimeOut); + if (tapLength < 500 && tapLength > 0) { + /// + log("double touched"); + v.touchCount += 1; + event.preventDefault(); + if (v.touchCount < 5) { + v.touchLastTap = currentTime; + return false; + } + v.touchLastTap = 0; + v.touchCount = 0; + + log("double touched"); + if (session.statsMenu !== false) { + var uid = event.currentTarget.dataset.UUID; + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, uid); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + event.stopPropagation(); + return false; + ////// + } else { + v.touchCount = 1; + v.touchTimeOut = setTimeout( + function (vv) { + clearTimeout(vv.touchTimeOut); + vv.touchLastTap = 0; + vv.touchCount = 0; + }, + 5000, + v + ); + v.touchLastTap = currentTime; + } + }); + + if (session.rpcs[UUID].stats.info && "remote" in session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote) { + v.addEventListener("wheel", remotePTZRequest); + // v.addEventListener("wheel", remoteFocusZoomRequest); // just remote focus -- obsolete. + } + + if (session.ptzSlider && (session.director || (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote))) { + + const ptzContainer = document.createElement('div'); + ptzContainer.className = 'video-ptz-controls'; + + // Zoom slider + const zoomSlider = document.createElement('div'); + zoomSlider.className = 'video-zoom-slider'; + + const zoomLabel = document.createElement('label'); + zoomLabel.innerText = "Zoom"; + + const zoomInput = document.createElement('input'); + zoomInput.title = "Camera zoom control"; + zoomInput.type = 'range'; + zoomInput.min = '0'; + zoomInput.max = '100'; + zoomInput.value = '0'; + + let zoomUpdating = false; + zoomInput.addEventListener('input', (e) => { + if (zoomUpdating) return; + const zoomValue = parseInt(e.target.value) / 100; // Normalize to 0-1 + + session.requestZoomChange(zoomValue, UUID, session.remote, true); + }); + + // Pan slider + const panSlider = document.createElement('div'); + panSlider.className = 'video-pan-slider'; + + const panLabel = document.createElement('label'); + panLabel.innerText = "Pan"; + + const panInput = document.createElement('input'); + panInput.title = "Camera pan control (left/right)"; + panInput.type = 'range'; + panInput.min = '-100'; + panInput.max = '100'; + panInput.value = '0'; + + let panUpdating = false; + panInput.addEventListener('input', (e) => { + if (panUpdating) return; + const panValue = parseInt(e.target.value) / 100; // Normalize to -1 to 1 + + session.requestPanChange(panValue, UUID, session.remote, true); + }); + + // Tilt slider + const tiltSlider = document.createElement('div'); + tiltSlider.className = 'video-tilt-slider'; + + const tiltLabel = document.createElement('label'); + tiltLabel.innerText = "Tilt"; + + const tiltInput = document.createElement('input'); + tiltInput.title = "Camera tilt control (up/down)"; + tiltInput.type = 'range'; + tiltInput.min = '-100'; + tiltInput.max = '100'; + tiltInput.value = '0'; + + let tiltUpdating = false; + tiltInput.addEventListener('input', (e) => { + if (tiltUpdating) return; + const tiltValue = parseInt(e.target.value) / 100; // Normalize to -1 to 1 + + session.requestTiltChange(tiltValue, UUID, session.remote, true); + }); + + // Append elements to containers + zoomSlider.appendChild(zoomLabel); + zoomSlider.appendChild(zoomInput); + + panSlider.appendChild(panLabel); + panSlider.appendChild(panInput); + + tiltSlider.appendChild(tiltLabel); + tiltSlider.appendChild(tiltInput); + + ptzContainer.appendChild(zoomSlider); + ptzContainer.appendChild(panSlider); + ptzContainer.appendChild(tiltSlider); + + if (!v.container) { + v.container = getById("videoContainer_" + UUID); + } + + v.container.appendChild(ptzContainer); + + // Store references for external updates + session.rpcs[UUID].zoomSlider = (value) => { + zoomUpdating = true; + zoomInput.value = Math.round(value * 100); // Convert 0-1 to 0-100 + zoomUpdating = false; + }; + + session.rpcs[UUID].panSlider = (value) => { + panUpdating = true; + panInput.value = Math.round(value * 100); // Convert -1 to 1 to -100 to 100 + panUpdating = false; + }; + + session.rpcs[UUID].tiltSlider = (value) => { + tiltUpdating = true; + tiltInput.value = Math.round(value * 100); // Convert -1 to 1 to -100 to 100 + tiltUpdating = false; + }; + } else if (session.zoomSlider && (session.director || (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.remote))) { + const slider = document.createElement('div'); + slider.className = 'video-zoom-slider0'; + + const input = document.createElement('input'); + input.title = "Hint: The remote camera's browser may needs to be visible for zoom to work in certain browsers"; + input.type = 'range'; + input.min = '0'; + input.max = '255'; + input.value = '0'; + + let updating = false; + input.addEventListener('input', (e) => { + if (updating) return; + const zoomValue = parseInt(e.target.value) / 255; + session.requestZoomChange(zoomValue, UUID, session.remote, true); + }); + + // Update slider if remote changes occur + const updateSlider = (value) => { + updating = true; + input.value = Math.round(value * 255); + updating = false; + input.title = input.value * 100 + ""; + }; + + slider.appendChild(input); + + if (!v.container) { + v.container = getById("videoContainer_" + UUID); + } + + v.container.appendChild(slider); + + // Store reference for external updates + session.rpcs[UUID].zoomSlider = updateSlider; + } + + if (v.controls == false) { + v.addEventListener("click", function () { + log("click 33"); + if (v.paused) { + log("PLAYING MANUALLY?"); + v.play() + .then(_ => { + log("playing 7"); + }) + .catch(warnlog); + } + }); + if (session.nocursor == false) { + // we do not want to show the controls. This is because MacOS + OBS does not work; so electron app needs this. + if (!session.cleanOutput) { + if (session.studioSoftware) { + } else if (session.showControls === false) { + // explicitly disabled; default null. + } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + } else { + if (v.controlTimer) { + clearInterval(v.controlTimer); + } + v.controlTimer = setTimeout(showControlBar.bind(null, v), 1000); + //v.controlTimer = setTimeout(function (){v.controls=true;},3000); // 3 seconds before I enable the controls automatically. This way it doesn't auto appear during loading. 3s enough, right? + } + } + } + } + + //if (session.fadein){ + v.addEventListener("animationend", function (e) { + v.classList.remove("fadein"); // allows the video to fade in. + if (v.holder) { + v.holder.classList.remove("fadein"); + } + }); + //v.classList.add("fadein"); // allows the video to fade in. + // if (v.holder){ + //// v.holder.classList.add("fadein"); + // } + //} + + applyMuteState(UUID); + v.usermuted = false; + + if (session.screenShareStartPaused && session.rpcs[UUID].screenShareState) { + pauseVideo(v, false); + } + + v.addEventListener("volumechange", function (e) { + var muteState = checkMuteState(UUID); + if (this.muted && this.muted !== muteState) { + this.usermuted = 1; + } else if (!this.muted && this.muted !== muteState) { + this.usermuted = 2; + } else if (!this.muted) { + this.usermuted = false; + } + }); + + if (session.autorecord || session.autorecordremote) { + log("AUTO RECORD START"); + setTimeout( + function (UUID, v) { + var videoKbps = session.recordDefault; + if (session.recordLocal !== false) { + videoKbps = session.recordLocal; + } + + if (session.director) { + recordVideo(document.querySelector("[data-action-type='recorder-local'][data--u-u-i-d='" + UUID + "']"), null, videoKbps); + } else if (v.stopWriter || v.recording) { + } else if (v.startWriter) { + v.startWriter(); + } else { + recordLocalVideo(null, videoKbps, v); + } + }, + 2000, + UUID, + v + ); + } + + if (session.rpcs[UUID]) { + clearTimeout(session.rpcs[UUID].getStatsTimeout); + session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, 100, UUID); + } +} + +session.requestPanChange = async function (pan, UUID, passwd = session.remote, absolute = false) { + // pan is now expected to be a value between -1 and 1 + log("request pan change: " + pan); + var msg = {}; + msg.pan = pan; // Normalized value -1 to 1 + msg.remote = passwd; + msg.abs = absolute; + msg = await session.encodeRemote(msg); + if (session.sendRequest(msg, UUID)) { + log("pan success"); + return true; + } else { + errorlog("failed to send pan change request"); + return false; + } +}; + +session.requestTiltChange = async function (tilt, UUID, passwd = session.remote, absolute = false) { + // tilt is now expected to be a value between -1 and 1 + log("request tilt change: " + tilt); + var msg = {}; + msg.tilt = tilt; // Normalized value -1 to 1 + msg.remote = passwd; + msg.abs = absolute; + msg = await session.encodeRemote(msg); + if (session.sendRequest(msg, UUID)) { + log("tilt success"); + return true; + } else { + errorlog("failed to send tilt change request"); + return false; + } +}; + +session.requestZoomChange = async function (zoom, UUID, passwd = session.remote, absolute = false) { + // zoom is now expected to be a value between 0 and 1 + log("request zoom change: " + zoom); + var msg = {}; + msg.zoom = zoom; // Normalized value 0 to 1 + msg.abs = absolute; + msg.remote = passwd; + msg = await session.encodeRemote(msg); + if (session.sendRequest(msg, UUID)) { + log("zoom success"); + return true; + } else { + errorlog("failed to send zoom change request"); + return false; + } +}; + +session.requestFocusChange = async function (focal, UUID, passwd = session.remote, absolute = false) { + log("request focus change: " + focal); + + var msg = {}; + msg.focus = focal; + msg.abs = absolute; + msg.remote = passwd; + msg = await session.encodeRemote(msg); + + if (session.sendRequest(msg, UUID)) { + log("focus success"); + } else { + errorlog("failed to send focus change request"); + } +}; + +session.requestAutofocusChange = async function (enabled, UUID, passwd = session.remote) { + log("request autofocus change: " + enabled); + + var msg = {}; + msg.autofocus = enabled; + msg.remote = passwd; + msg = await session.encodeRemote(msg); + + if (session.sendRequest(msg, UUID)) { + log("autofocus request success"); + } else { + errorlog("failed to send autofocus change request"); + } +}; + +function remotePTZRequest(event) { + event.preventDefault(); + + var scale = event.deltaY > 0 ? -0.05 : 0.05; // Use larger normalized steps + + if (!event.altKey) { + scale *= 2; // Double the scale when not holding Alt + } + + if (event.ctrlKey || event.metaKey) { + if (event.shiftKey) { + // tilt: -1 to 1 + session.requestTiltChange(scale, event.currentTarget.dataset.UUID); + } else { + // focus: -1 to 1 + session.requestFocusChange(scale, event.currentTarget.dataset.UUID); + } + } else if (event.shiftKey) { + // pan: -1 to 1 + session.requestPanChange(scale, event.currentTarget.dataset.UUID); + } else { + // zoom: 0 to 1 (relative) + session.requestZoomChange(scale, event.currentTarget.dataset.UUID); + } +} + +function remoteFocusZoomRequest(event) { // obsolete. + event.preventDefault(); + + var scale = event.deltaY > 0 ? -0.004 : 0.004; + log(event.currentTarget); + log(event.deltaY); + + if (!event.altKey) { + scale *= 10; + } + + if (event.ctrlKey || event.metaKey) { + // focus + session.requestFocusChange(scale, event.currentTarget.dataset.UUID); + } else { + // zoom + session.requestZoomChange(scale, event.currentTarget.dataset.UUID); + } +} + +function mediaAudioTrackUpdated(UUID, streamID) { + pokeIframeAPI("new-audio-track-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess. +} +function mediaVideoTrackUpdated(UUID, streamID) { + pokeIframeAPI("new-video-track-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess. +} +function mediaSourceUpdated(UUID, streamID) { + pokeIframeAPI("new-stream-added", true, UUID, streamID); // videoTrack is whether video. audio will be false I guess. + pokeAPI("streamAdded", streamID); +} + +function showControlBar(vel) { + try { + vel.controls = true; + } catch (e) { + errorlog(e); + } +} + +function createRichVideoElement(UUID) { + // this function is used to check and generate a rich video element if needed + if (!session.rpcs[UUID].videoElement) { + log("video element is being created and any media tracks added"); + session.rpcs[UUID].videoElement = createVideoElement(); + session.rpcs[UUID].videoElement.dataset.UUID = UUID; + session.rpcs[UUID].videoElement.id = "videosource_" + UUID; // could be set to UUID in the future + + if (session.rpcs[UUID].streamID) { + session.rpcs[UUID].videoElement.dataset.sid = session.rpcs[UUID].streamID; + } + + if (session.rpcs[UUID].rotate !== false) { + session.rpcs[UUID].videoElement.rotated = session.rpcs[UUID].rotate; + session.rpcs[UUID].videoElement.dataset.rotated = session.rpcs[UUID].rotate; + updateVideoTransform(session.rpcs[UUID].videoElement); + } + + session.rpcs[UUID].videoElement.addEventListener( + "playing", + e => { + try { + var bigPlayButton = document.getElementById("bigPlayButton"); + if (bigPlayButton) { + bigPlayButton.parentNode.removeChild(bigPlayButton); + } + } catch (e) { } + + resetupAudioOut(e.target, true); + + try { + if (session.pip) { + if (v.readyState >= 3) { + if (!v.pip) { + v.pip = true; + toggleSystemPip(v, true); + } + } + } + } catch (e) { } + }, + { once: true } + ); + + if (session.rpcs[UUID].mirrorState !== null || session.rpcs[UUID].flipState !== null) { + applyMirrorGuest( + !!session.rpcs[UUID].mirrorState, + session.rpcs[UUID].videoElement, + session.rpcs[UUID].flipState !== null ? !!session.rpcs[UUID].flipState : undefined + ); + } + + if (session.posterImage) { + session.rpcs[UUID].videoElement.poster = session.posterImage; + } + + setupIncomingVideoTracking(session.rpcs[UUID].videoElement, UUID); + pokeIframeAPI("video-element-created", "videosource_" + UUID, UUID); + } + return session.rpcs[UUID].videoElement; +} + +function updateVolume(update = false) { + if (session.audioGain !== false) { + if (update) { + if (session.roomid) { + var pswd = session.password || ""; + generateHash(session.streamID + session.roomid + pswd + session.salt, 6).then(function (hash) { + setStorage("micVolume_" + hash, session.audioGain, (hours = 6)); + }); + } + } + if (session.audioGain === 0) { + getById("header").classList.add("orange"); + getById("head7").classList.remove("hidden"); + } else { + getById("header").classList.remove("orange"); + getById("head7").classList.add("hidden"); + } + } else { + var pswd = session.password || ""; + generateHash(session.streamID + session.roomid + pswd + session.salt, 6).then(function (hash) { + var volume = getStorage("micVolume_" + hash); + if (volume !== "") { + if (parseInt(volume) === 0) { + getById("header").classList.add("orange"); + getById("head7").classList.remove("hidden"); + } else if (parseInt(volume)) { + getById("header").classList.remove("orange"); + getById("head7").classList.add("hidden"); + } else { + return; + } + session.audioGain = parseInt(volume); + var vol = parseFloat(session.audioGain / 100) || 0; + for (var waid in session.webAudios) { + // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (trackid === wa.id){.. + log("Adjusting Gain; only track 0 in all likely hood, unless more than track 0 support is added."); + session.webAudios[waid].gainNode.gain.setValueAtTime(vol, session.webAudios[waid].audioContext.currentTime); + } + } + }); + } +} + +function hideHomeCheck() { + if (session.hidehome) { + getById("logoname").classList.add("permahide"); + getById("container-1").classList.add("permahide"); + getById("container-4").classList.add("permahide"); + getById("dropButton").classList.add("permahide"); + getById("head1").classList.add("permahide"); + if (session.permaid === false && session.roomid == false && !session.webcamonly && !session.screenshare) { + getById("mainmenu").classList.add("permahide"); + } else { + getById("mainmenu").classList.remove("permahide"); + } + + getById("audioScreenCaptureDocs").classList.add("permahide"); + getById("audioScreenCaptureDocs2").classList.add("permahide"); + getById("translateButton").classList.add("permahide"); + // getById("legal").classList.add("permahide"); + getById("calendarButton").classList.add("permahide"); + getById("info").classList.add("permahide"); + getById("helpbutton").classList.add("permahide"); + } + + if (urlParams.has("headertitle")) { + let pageTitle = urlParams.get("headertitle") || ""; + pageTitle = decodeURIComponent(pageTitle) || ""; + document.title = pageTitle; + getById("metaTitle").content = pageTitle; + } + + if (urlParams.has("favicon")) { + let favicon = ""; + if (urlParams.get("favicon")) { + favicon = decodeURIComponent(urlParams.get("favicon")) || ""; + } + getById("favicon1").href = favicon; + getById("favicon2").href = favicon; + getById("favicon3").href = favicon; + } +} + +function stashRoomSession(broadcastFlag = null) { + try { + let settings = {}; + + settings.roomid = session.roomid; + settings.password = session.password; + settings.label = session.label; + settings.trb = session.totalRoomBitrate; + settings.widget = session.widget; + settings.codecGroupFlag = session.codecGroupFlag; + settings.showDirector = session.showDirector; + + if (broadcastFlag !== null) { + settings.broadcast = broadcastFlag; + } + + setStorage("directorOtherSettings", settings); + } catch (e) { + errorlog(e); + } +} + +// toggleQualityDirector(1200, this.dataset.UUID, this) + +function switchModes(state = null) { + if (state === null) { + session.switchMode = !session.switchMode; + } else { + session.switchMode = state; + } + if (session.switchMode) { + getById("directorlayout").classList.add("hidden"); + getById("gridlayout").classList.remove("hidden"); + updateMixer(); + } else { + getById("directorlayout").classList.remove("hidden"); + getById("gridlayout").classList.add("hidden"); + + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement) { + session.rpcs[UUID].videoElement.style = ""; + session.rpcs[UUID].videoElement.alreadyAdded = false; + var target = document.querySelector("#container_" + UUID + " .controlVideoBox"); + if (target) { + target.prepend(session.rpcs[UUID].videoElement); + if (session.signalMeter) { + if (session.rpcs[UUID].signalMeter) { + target.appendChild(session.rpcs[UUID].signalMeter); + } + } + if (session.batteryMeter) { + if (session.rpcs[UUID].batteryMeter) { + target.appendChild(session.rpcs[UUID].batteryMeter); + } + } + if (session.rpcs[UUID].voiceMeter) { + target.appendChild(session.rpcs[UUID].voiceMeter); + } + if (session.rpcs[UUID].remoteMuteElement) { + target.appendChild(session.rpcs[UUID].remoteMuteElement); + } + } + } + } + + if (session.videoElement) { + session.videoElement.style = ""; + session.videoElement.alreadyAdded = false; + + if (session.showDirector == true) { + var target = document.querySelector("#videoContainer_director"); + if (target && session.videoElement) { + target.prepend(session.videoElement); + } + } else if ((session.videoElement.srcObject && session.videoElement.srcObject.getTracks().length) || getById("press2talk").dataset.enabled == true) { + getById("miniPerformer").prepend(session.videoElement); + } + } + + if (session.screenShareElement) { + session.screenShareElement.style = ""; + session.screenShareElement.alreadyAdded = false; + + if (session.showDirector == true) { + var target = document.querySelector("#videoScreenContainer_director"); + if (target && session.screenShareElement && session.screenShareElement.srcObject && session.screenShareElement.srcObject.getTracks().length) { + target.prepend(session.screenShareElement); + } + } else if ((session.screenShareElement.srcObject && session.screenShareElement.srcObject.getTracks().length) || getById("press2talk").dataset.enabled == true) { + getById("miniPerformer").prepend(session.videoElement); + } + } + + applyQualityDirector(); + } +} + +var updateMixerTimer = null; +var updateMixerActive = false; +function updateMixer(e = false) { + var controlBar = document.getElementById("subControlButtons"); + if (controlBar && controlBar.dragElement && !controlBar.isDragging) { + let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); + let vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); + + // Calculate real boundaries (parent position: fixed issues) + let topOffset = 0; + let leftOffset = 0; + let elementOffset = controlBar; + while (elementOffset) { + topOffset += elementOffset.offsetTop; + leftOffset += elementOffset.offsetLeft; + elementOffset = elementOffset.offsetParent; + } + + let realX = controlBar.xOffset + leftOffset; + let realY = controlBar.yOffset + topOffset; + let maxX = vw - controlBar.offsetWidth; + let maxY = vh - controlBar.offsetHeight; + + if (realX > maxX) { + controlBar.xOffset = maxX - leftOffset; + } else if (realX < 0) { + controlBar.xOffset = 0 - leftOffset; + } + + if (realY > maxY) { + controlBar.yOffset = maxY - topOffset; + } else if (realY < 0) { + controlBar.yOffset = 0 - topOffset; + } + + controlBar.style.transform = `translate(${controlBar.xOffset}px, ${controlBar.yOffset}px)`; + } + + if (session.manual === true) { + return; + } else if (!session.switchMode && session.director) { + return; + } else if (session.windowed) { + return; + } + + clearInterval(updateMixerTimer); + if (updateMixerActive) { + if (session.mobile) { + updateMixerTimer = setTimeout(function () { + updateMixer(); + }, 200); + } else { + updateMixerTimer = setTimeout(function () { + updateMixer(); + }, 50); + } + return; + } + updateMixerActive = true; + log("updating mixer"); + + try { + updateMixerRun(e); + } catch (e) { } + + if (session.mobile) { + setTimeout(function () { + updateMixerActive = false; + }, 500); + } else { + setTimeout(function () { + updateMixerActive = false; + }, 100); + } +} + +function updateMixerRun(e = false) { + // this is the main auto-mixing code. It's a giant function that runs when there are changes to screensize, video track statuses, etc. + try { + if (session.switchMode) { + } else if (session.director) { + return; + } else if (session.manual === true) { + return; + } + + var header = getById("header"); + var playarea = getById("gridlayout"); + + if (session.pipWindow) { + var hi = 0; + var h = session.pipWindow.clientHeight || session.pipWindow.innerHeight || session.pipWindow.outerHeight; + var w = session.pipWindow.clientWidth || session.pipWindow.innerWidth || session.pipWindow.outerWidth; + } else if (document.body.dataset.rotated) { + var hi = header.offsetHeight; + var w = document.body.clientHeight; + + if (session.widget && session.iFramesAllowed) { + w *= (100 - session.widgetwidth) / 100; + try { + let widget = document.getElementById("widget"); + if (!widget) { + widget = document.createElement("iframe"); + widget.id = "widget"; + widget = loadIframe(parseURL4Iframe(session.widget), widget); + if (widget) { + document.body.appendChild(widget); + if (session.widgetleft) { + widget.classList.add("left"); + playarea.style.left = session.widgetwidth + "%"; + playarea.style.width = (100 - session.widgetwidth) + "%"; + } else { + playarea.style.left = "0"; + playarea.style.width = (100 - session.widgetwidth) + "%"; + } + } + } + if (widget) { + widget.style.height = "calc(100% - " + hi + "px)"; + widget.style.top = hi; + } + } catch (e) { + errorlog(e); + } + } else if (!session.widget && session.widgetleft) { + playarea.style.left = "0"; + } + + var h = document.body.clientWidth - hi; + if (session.dedicatedControlBarSpace || document.body.clientWidth <= 700) { + // # This needs to be reviewed. + if (session.dedicatedControlBarSpace !== false) { + let controlBar = document.getElementById("subControlButtons"); + if (controlBar && !session.overlayControls) { + if (!controlBar.yOffset || controlBar.yOffset > -10) { + h = document.body.clientWidth - hi - controlBar.offsetHeight; + } + } + } + } + } else { + var hi = header.offsetHeight; + var w = window.innerWidth; + + if (session.widget && session.iFramesAllowed) { + w *= (100 - session.widgetwidth) / 100; + try { + let widget = document.getElementById("widget"); + if (!widget) { + widget = document.createElement("iframe"); + widget.id = "widget"; + widget = loadIframe(parseURL4Iframe(session.widget), widget); + if (widget) { + document.body.appendChild(widget); + if (session.widgetleft) { + widget.classList.add("left"); + playarea.style.left = session.widgetwidth + "%"; + playarea.style.width = (100 - session.widgetwidth) + "%"; + playarea.style.position = "absolute"; + } else { + playarea.style.left = "0"; + playarea.style.width = (100 - session.widgetwidth) + "%"; + } + } + } + if (widget) { + widget.style.height = "calc(100% - " + hi + "px)"; + widget.style.top = hi; + } + } catch (e) { + errorlog(e); + } + } else if (!session.widget && session.widgetleft) { + playarea.style.left = "0"; + } + + var h = window.innerHeight - hi; + if (session.dedicatedControlBarSpace || window.innerHeight <= 700) { + // # This needs to be reviewed. + if (session.dedicatedControlBarSpace !== false) { + let controlBar = document.getElementById("subControlButtons"); + if (controlBar && !session.overlayControls) { + if (!controlBar.yOffset || controlBar.yOffset > -10) { + h = window.innerHeight - hi - controlBar.offsetHeight; + } + } + } + } + } + + if (session.locked) { + var w123 = w; + var h123 = h; + + if (w > h * session.locked) { + w = h * session.locked; + } else if (h > w / session.locked) { + h = w / session.locked; + } + + playarea.style.left = (w123 - w) / 2 + "px"; + playarea.style.top = (h123 - h) / 2 + "px"; + playarea.style.width = w + "px"; + playarea.style.height = h + "px"; + playarea.style.position = "absolute"; + playarea.style.display = "block"; + } + + var arW = 16.0; + var arH = 9.0; + + if (session.aspectRatio) { + if (session.aspectRatio == 1) { + arW = 9.0; + arH = 16.0; + } else if (session.aspectRatio == 2) { + arW = 12.0; // square root; cause why not. + arH = 12.0; + } else if (session.aspectRatio == 3) { + arW = 12.0; // square root; cause why not. + arH = 9.0; + } + } + + var groups = [...session.group]; + if (session.groupView.length) { + groups.push(...session.groupView); + } + var sssid = false; + var soloVideo = false; + + if (session.infocus === true) { + soloVideo = true; + } else if (session.infocus && session.infocus in session.rpcs) { + // if the infocus stream is connected + if (groups.length || session.allowNoGroup) { + try { + if (groups.some(item => session.rpcs[session.infocus].group.includes(item))) { + soloVideo = session.infocus; + } + } catch (e) { + errorlog(e); + } + } else { + soloVideo = session.infocus; + } + } else if (session.infocus2 === true) { + sssid = session.streamID; + } else if (session.infocus2 && session.infocus2 in session.rpcs) { + // if the infocus2 stream is connected + if (groups.length || session.allowNoGroup) { + try { + if (groups.some(item => session.rpcs[session.infocus2].group.includes(item))) { + sssid = session.rpcs[session.infocus2].streamID; + } + } catch (e) { + errorlog(e); + } + } else { + sssid = session.rpcs[session.infocus2].streamID; + } + } + + var ww = w / arW; + var hh = h / arH; + + var mediaPool = []; + var mediaPool_invisible = []; + + var miniPreview = session.minipreview; + + if (miniPreview && (!session.activeSpeaker && session.layout) && session.streamID && session.streamID in session.layout) { + miniPreview = false; + if (session.videoElement.container && session.videoElement.container.id == "minipreview") { + delete session.videoElement.container; + } + if (document.getElementById("minipreview")) { + document.getElementById("minipreview").remove(); + } + } + + if (session.iframeEle && session.iframeEle.style.display !== "none") { + // local feed + if (session.order !== false) { + session.iframeEle.order = session.order; + } else { + session.iframeEle.order = 0; + } + if (session.activeSpeaker && !session.activelySpeaking) { + mediaPool_invisible.push(session.iframeEle); + } else { + mediaPool.push(session.iframeEle); + } + } + + if (session.videoElement && (session.videoElement.src || session.videoElement.srcObject)) { + // I, myself, exist + if (session.videoElement.style.display !== "none") { + // local feed + if (miniPreview && soloVideo !== true) { + + } else { + if (session.order !== false) { + session.videoElement.order = session.order; + } else { + session.videoElement.order = 0; + } + if (session.activeSpeaker && !session.activelySpeaking) { + //mediaPool_invisible.push(session.videoElement); + //} else if (session.videoElement && session.videoElement.srcObject && (session.videoElement.srcObject.getTracks().length === 0)){ + // do not show a video element if its completely empty. + } else if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length === 0) { + // do not show a video element if its completely empty. + } else if (soloVideo && soloVideo !== true) { + // + } else if (session.videoMuted && session.style === 1) { + // i'm too tired to try to get this working. + } else { + mediaPool.push(session.videoElement); + } + } + } + } + + if (session.screenShareState && session.screenShareElement) { + // I, myself, exist + if (!session.screenShareElementHidden) { + if (session.order !== false) { + session.screenShareElement.order = session.order; + } else { + session.screenShareElement.order = 0; + } + + if (soloVideo !== false) { + //session.screenShareElement.style.display="none"; + } else if (session.activeSpeaker && !session.activelySpeaking) { + //session.screenShareElement.style.display="none"; + } else if (!session.noScreenShare) { + mediaPool.push(session.screenShareElement); + } else { + session.screenShareElement.style.display = "none"; + } + } + } + + var delayedRequestList = {}; + + function delayedRequestRate(bandwidth, UUID, optimizeAudio = false, lock = null) { + delayedRequestList[UUID] = [bandwidth, UUID, optimizeAudio, lock]; + } + + if (soloVideo && soloVideo in session.rpcs) { // this technically can be a scene or guest + // remote guest being full screened; infocus == UUID + mediaPool = []; // remove myself from fullscreen + + if (iOS || iPad) { + if (!miniPreview) { + miniPreview = 1; + } + } + + for (var j in session.rpcs) { + if (groups.length || session.allowNoGroup) { + try { + if (!groups.some(item => session.rpcs[j].group.includes(item))) { + continue; + } + } catch (e) { + errorlog(e); + } + } + + if (j != soloVideo) { + // this remote guest is NOT in focus + try { + if (session.rpcs[j].iframeEle) { + mediaPool_invisible.push(session.rpcs[j].iframeEle); + } + if (session.rpcs[j].videoElement && session.rpcs[j].videoElement.style.display !== "none") { + // Add it if not hidden + if (session.scene !== false) { + //delayedRequestRate(session.hiddenSceneViewBitrate, j); + } else { + if (document.pictureInPictureElement && document.pictureInPictureElement.id && document.pictureInPictureElement.id == session.rpcs[j].videoElement.id) { + var bitratePIP = parseInt(session.zoomedBitrate / 4); + //warnUser("GOOD"); + delayedRequestRate(bitratePIP, j); + } else { + delayedRequestRate(0, j); // disable the video of non-fullscreen videos + } + } + } else if (session.rpcs[j].videoElement) { + delayedRequestRate(0, j, true); // disable the video of non-fullscreen videos + } + } catch (e) { + errorlog(e); + } + } else { + // remote guest is in-focus video + //////// + try { + if (session.rpcs[j].iframeEle) { + mediaPool_invisible.push(session.rpcs[j].iframeEle); + } + if (session.rpcs[j].videoElement) { + mediaPool.push(session.rpcs[j].videoElement); // active speaker + session.rpcs[j].videoElement.style.visibility = "visible"; + if (session.rpcs[j].order !== false) { + session.rpcs[j].videoElement.order = session.rpcs[j].order; + } else { + session.rpcs[j].videoElement.order = 0; + } + if (session.scene !== false) { + + } else { + var totalRoomBitrate = session.totalRoomBitrate; + if (session.controlRoomBitrate !== false && session.controlRoomBitrate !== true) { + totalRoomBitrate = Math.min(session.controlRoomBitrate, totalRoomBitrate); + } + var targetBitrate = session.zoomedBitrate; + if (totalRoomBitrate > session.zoomedBitrate) { + targetBitrate = totalRoomBitrate; + } + delayedRequestRate(targetBitrate, j); // 1.2-Mbps is decent, no? in-focus, so higher bitrate + } + } + } catch (e) { + errorlog(e); + } + } + } + } else if (soloVideo && soloVideo === true) { // this cannot be a scene, as you can't have yourself in a scene. + // well, fullscreen myself. "true" represents me. UUID would be for others. + // already added myself to this as fullscreen + for (var j in session.rpcs) { + if (groups.length || session.allowNoGroup) { + try { + if (!groups.some(item => session.rpcs[j].group.includes(item))) { + continue; + } + } catch (e) { + errorlog(e); + } + } + try { + if (session.rpcs[j].videoElement && session.rpcs[j].videoElement.style.display !== "none") { + // Add it if not hidden + if (document.pictureInPictureElement && document.pictureInPictureElement.id && document.pictureInPictureElement.id == session.rpcs[j].videoElement.id) { + var bitratePIP = parseInt(session.zoomedBitrate / 4); + delayedRequestRate(bitratePIP, j); + //warnUser("GOOD"); + } else { + delayedRequestRate(0, j); // disable the video of non-fullscreen videos + } + // mediaPool_invisible.push(session.rpcs[j].videoElement); + } else if (session.rpcs[j].videoElement) { + delayedRequestRate(0, j, true); // other videos are disabled when previewing yourself, but audio retained + } + } catch (e) { + errorlog(e); + } + } + } else { + var roomQuality = 0; + var screenShareTotal = 0; + + for (var i in session.rpcs) { + if (session.rpcs[i] === null) { + continue; + } + if (groups.length || session.allowNoGroup) { + try { + if (!groups.some(item => session.rpcs[i].group.includes(item))) { + continue; + } + } catch (e) { + errorlog(e); + } + } + if (session.rpcs[i].videoElement) { + // remote feeds + if (session.rpcs[i].videoElement.style.display !== "none") { + if (session.rpcs[i].videoElement.srcObject && session.rpcs[i].videoElement.srcObject.getVideoTracks().length) { + // only count videos with actual video tracks; audio-only excluded + if (session.rpcs[i].videoMuted) { + // it's video muted + // mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on + } else if (session.rpcs[i].directorVideoMuted) { + // it's muted by the director, so likely disabled. + // mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on + } else if (session.rpcs[i].virtualHangup) { + } else if (session.rpcs[i].bandwidthMuted) { + } else if (session.rpcs[i].videoElement.style.opacity === "0") { + // mediaPool_invisible.push(session.rpcs[i].videoElement); // skipped later on + } else { + roomQuality += 1; + if (session.rpcs[i].screenShareState) { + screenShareTotal += 1; + } + } + } + } + } + } + + if (session.broadcast !== false) { + if ((!session.activeSpeaker && session.layout) && session.streamID in session.layout) { + // skip + } else { + if (roomQuality > 0) { + if (session.nopreview !== false) { + for (var i = 0; i < mediaPool.length; i++) { + if (mediaPool[i].nodeName && mediaPool[i].nodeName == "IFRAME") { + mediaPool[i].style.display = "none"; + } + } + mediaPool = []; // we don't want to show our self-preview if in broadcast mode and there is a director. + } + } + } + } + + if (roomQuality === 0) { + roomQuality = 1; + } + + var totalRoomBitrate = session.totalRoomBitrate; + + if (session.controlRoomBitrate !== false && session.controlRoomBitrate !== true) { + totalRoomBitrate = Math.min(session.controlRoomBitrate, totalRoomBitrate); + } + + var roomBitrate = totalRoomBitrate; + var sceneBitrate = false; + var screenShareBitrate = false; + + if (session.bitrate && !roomBitrate && !session.totalSceneBitrate) { + roomBitrate = session.bitrate; + } else if (session.screenShareBitrate !== false) { + screenShareBitrate = session.screenShareBitrate; + if (roomQuality - screenShareTotal > 0) { + roomBitrate = parseInt(totalRoomBitrate / (roomQuality - screenShareTotal)); + if (session.totalSceneBitrate) { + sceneBitrate = parseInt(session.totalSceneBitrate / (roomQuality - screenShareTotal)); + if (session.bitrate !== false) { + sceneBitrate = Math.min(session.bitrate, sceneBitrate); + } + } + } + } else if (screenShareTotal) { + try { + if (session.roomid !== false && session.scene === false) { + if (roomQuality - screenShareTotal <= 0) { + roomBitrate = totalRoomBitrate; + screenShareBitrate = totalRoomBitrate; + } else { + screenShareBitrate = totalRoomBitrate / (1.5 * screenShareTotal); + roomBitrate = parseInt((totalRoomBitrate - screenShareBitrate) / (roomQuality - screenShareTotal)); + } + } else if (session.totalSceneBitrate !== false) { + if (roomQuality - screenShareTotal <= 0) { + sceneBitrate = session.totalSceneBitrate; + if (session.bitrate !== false) { + sceneBitrate = Math.min(session.bitrate, sceneBitrate); + } + screenShareBitrate = sceneBitrate; + } else { + screenShareBitrate = parseInt(totalRoomBitrate / (1.5 * screenShareTotal)); + sceneBitrate = parseInt((totalRoomBitrate - screenShareBitrate) / (roomQuality - screenShareTotal)); + if (session.bitrate !== false) { + sceneBitrate = Math.min(session.bitrate, sceneBitrate); + screenShareBitrate = Math.min(session.bitrate, screenShareBitrate); + } + } + } else { + screenShareBitrate = false; + } + } catch (e) { + errorlog(e); + } + } else { + roomBitrate = parseInt(totalRoomBitrate / roomQuality); + if (session.totalSceneBitrate) { + sceneBitrate = parseInt(session.totalSceneBitrate / roomQuality); + if (session.bitrate !== false) { + sceneBitrate = Math.min(session.bitrate, sceneBitrate); + } + } + } + + if (session.minimumRoomBitrate) { + if (session.totalRoomBitrate && roomBitrate < session.minimumRoomBitrate) { + roomBitrate = session.minimumRoomBitrate; + if (roomBitrate > session.totalRoomBitrate) { + roomBitrate = session.totalRoomBitrate; + } + } + if (session.totalSceneBitrate && sceneBitrate < session.minimumRoomBitrate) { + sceneBitrate = session.minimumRoomBitrate; + if (sceneBitrate > session.totalSceneBitrate) { + sceneBitrate = session.totalSceneBitrate; + } + } + } + + var i = null; + var countOrder = 0; + try { + var RPCSkeys = Object.keys(session.rpcs); // default sorting type: time added; //RPCSkeys.sort(); + } catch (e) { + return; + } + + for (var keyIndex = 0; keyIndex < RPCSkeys.length; keyIndex++) { + i = RPCSkeys[keyIndex]; + if (session.rpcs[i] === null) { + continue; + } + session.rpcs[i].mutedStateMixer = false; + if (groups.length || session.allowNoGroup) { + // The MAIN and LAST group filter. + try { + if (!groups.some(item => session.rpcs[i].group.includes(item))) { + if (session.scene !== false) { + if (session.groupAudio) { + delayedRequestRate(session.hiddenSceneViewBitrate, i, false); + } else { + delayedRequestRate(session.hiddenSceneViewBitrate, i, true); // hidden. I dont want it to be super low, for video quality reasons. + session.rpcs[i].mutedStateMixer = true; + } + if (!session.hiddenSceneViewBitrate) { + session.rpcs[i].videoElement.nogb = 2; + } + } else { + if (session.groupAudio) { + delayedRequestRate(0, i, false); + } else { + delayedRequestRate(0, i, true); // w/e This is not in OBS, so we just set it as low as possible. Shoudln't exist really unless loading? + session.rpcs[i].mutedStateMixer = true; + } + } + applyMuteState(i); + continue; + } + } catch (e) { } + } + applyMuteState(i); + var doNotPush = false; + + var isScreenShareFeed = false; + try { + if ("realUUID" in session.rpcs[i]) { + isScreenShareFeed = true; + } else if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.dataset && session.rpcs[i].videoElement.dataset.sid) { + isScreenShareFeed = session.rpcs[i].videoElement.dataset.sid.endsWith(":s"); + } + } catch (e) { } + + if (session.noScreenShare && isScreenShareFeed) { + doNotPush = true; + if (session.rpcs[i].videoElement) { + session.rpcs[i].videoElement.style.display = "none"; + } + } + + if (session.rpcs[i].iframeEle) { + if (session.rpcs[i].iframeEle.style.display == "none") { + // pass + } else if (session.rpcs[i].iframeEle.style.opacity === "0") { + // pass + } else { + session.rpcs[i].iframeEle.style.visibility = "visible"; + if (session.rpcs[i].order !== false) { + session.rpcs[i].iframeEle.order = session.rpcs[i].order; + } else { + session.rpcs[i].iframeEle.order = 0; + } + try { + if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { + mediaPool_invisible.push(session.rpcs[i].iframeEle); // TODO: this needs validation; will the iframe be maintained if activer speaker is going? do we even want this? + /* } else if (session.rpcs[i].iframeEle.dataset.meshcast){ //////// MESH CAST ONLY LOGIC + if (session.rpcs[i].iframeEle.contentDocument && session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){ + if (session.rpcs[i].iframeVideo){ + mediaPool.push(session.rpcs[i].iframeVideo); + } else if (session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){ + session.rpcs[i].iframeVideo = session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video")[0]; + session.rpcs[i].iframeVideo.id="meshcast_"+i; + //errorlog("THIS IS GOOD"); + mediaPool.push(session.rpcs[i].iframeVideo); + } else { + //errorlog("No video yet"); + } + } else { // this is a problem is not on the same domain. + if (!document.getElementById("iframe_"+i)){ + if (document.getElementById("hiddenElements")){ + document.getElementById("hiddenElements").append(session.rpcs[i].iframeEle); + } else { + document.body.append(session.rpcs[i].iframeEle); + } + if (session.rpcs[i].iframeVideo){ + mediaPool.push(session.rpcs[i].iframeVideo); + } else if (session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video").length){ + session.rpcs[i].iframeVideo = session.rpcs[i].iframeEle.contentDocument.querySelectorAll("video")[0]; + session.rpcs[i].iframeVideo.id="meshcast_"+i; + mediaPool.push(session.rpcs[i].iframeVideo); + } else { + //errorlog("No video yet"); + } + } else { + if (session.rpcs[i].iframeVideo){ + mediaPool.push(session.rpcs[i].iframeVideo); + } else { + //errorlog("Does not support contentDocument or something"); + } + } + } */ + } else { + ///////// MESH CAST LOGIC ENDS HERE + //errorlog("not meshcast"); + mediaPool.push(session.rpcs[i].iframeEle); + } + } catch (e) { + errorlog(e); + } + } + } + + if (session.rpcs[i].imageElement) { + //if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.srcObject.getAudioTracks().length) { + // is there audio? + // mediaPool_invisible.push(session.rpcs[i].videoElement); // include audio as hidden track; + //} + + if (session.rpcs[i].videoMuted || session.rpcs[i].directorVideoMuted || session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted) { + continue; + } + + if (session.rpcs[i].videoElement && session.rpcs[i].videoElement.style.display == "none") { + // currently this is considered the state of scenes. pertty dumb on my part. + continue; + } + + if (session.rpcs[i].order !== false) { + session.rpcs[i].imageElement.order = session.rpcs[i].order; + } else { + session.rpcs[i].imageElement.order = 0; + } + if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { + // mediaPool_invisible.push(session.rpcs[i].imageElement); + } else { + mediaPool.push(session.rpcs[i].imageElement); + } + + doNotPush = true; + } + + if (session.rpcs[i].videoElement) { + // remote feeds + //session.rpcs[i].targetBandwidth = -1; + if (session.rpcs[i].videoElement.style.opacity === "0") { + continue; + } + try { + session.rpcs[i].videoElement.style.visibility = "visible"; + } catch (e) { + errorlog(e); + } + + if (session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted || session.rpcs[i].directorVideoMuted) { + continue; + } + + if (session.style && session.style >= 2) { + if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) { + if (session.rpcs[i].videoElement.style.display == "none") { + // currently this is considered the state of scenes. pertty dumb on my part. + continue; + } + if (createStyleCanvas(i)) { + applyStyleEffect(i); + } + if (session.rpcs[i].order !== false) { + session.rpcs[i].canvas.order = session.rpcs[i].order; + } else { + session.rpcs[i].canvas.order = 0; + } + if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { + // mediaPool_invisible.push(session.rpcs[i].canvas); + } else { + mediaPool.push(session.rpcs[i].canvas); + } + doNotPush = true; + //continue; + } + } else if (session.style == 1) { + if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) { + //if (session.style==1){ // avatars and waveforms might be better done elsewhere? as a canvas effect even? + doNotPush = true; + //} + } + } else if (session.rpcs[i].videoElement.srcObject && (session.rpcs[i].videoElement.srcObject.getVideoTracks().length == 0 || session.rpcs[i].videoMuted)) { + if (session.rpcs[i].screenShareState) { + doNotPush = true; + } + } + //} else if (!session.directorList.indexOf(i)>=0){ // director is never audio-only. Video if need, yes, but not visualized-audio. + // if (session.rpcs[i].videoElement.srcObject && ((session.rpcs[i].videoElement.srcObject.getVideoTracks().length==0) || (session.rpcs[i].videoMuted)) && !session.rpcs[i].directorVideoMuted){ + // continue; + // } + //} + + session.rpcs[i].opacityMuted = "1"; + if (session.rpcs[i].opacityDisconnect == "1") { + if (session.rpcs[i].videoElement) { + session.rpcs[i].videoElement.style.opacity = "1"; + } + } + if (session.rpcs[i].videoMuted) { + if (session.rpcs[i].videoElement.srcObject.getAudioTracks().length == 0) { + // if no audio track, no point in removing the video track, since it will just stall out then. + continue; // easiest is to just not show anything if no video and no audio track. + } + if (session.rpcs[i].videoElement.srcObject) { + session.rpcs[i].videoElement.srcObject.getVideoTracks().forEach(track => { + log("remove track3"); + session.rpcs[i].videoElement.srcObject.removeTrack(track); + session.rpcs[i].videoElement.load(); + }); + } + //continue; // currently disabling this, since we want to show it. + } else if (session.rpcs[i].virtualHangup || session.rpcs[i].bandwidthMuted || session.rpcs[i].directorVideoMuted) { + continue; + } + + if (session.scene !== false) { + if (session.sceneType === 3) { + // order + countOrder += 1; + if (session.order === false) { + if (countOrder == 1) { + session.rpcs[i].videoElement.style.display = "block"; + } else { + session.rpcs[i].videoElement.style.display = "none"; + } + } else if (session.order === countOrder) { + session.rpcs[i].videoElement.style.display = "block"; + } else { + session.rpcs[i].videoElement.style.display = "none"; + } + } + } + + if (session.rpcs[i].videoElement.style.display == "none") { + // Video is disabled; run at lowest + if (session.scene !== false) { + delayedRequestRate(session.hiddenSceneViewBitrate, i, true); // hidden. I dont want it to be super low, for video quality reasons. + if (!session.hiddenSceneViewBitrate) { + session.rpcs[i].videoElement.nogb = 2; + } + } else { + delayedRequestRate(0, i, true); // w/e This is not in OBS, so we just set it as low as possible. Shoudln't exist really unless loading? + } + } else if (session.scene !== false) { + // max + // + if (sceneBitrate !== false) { + if (screenShareBitrate !== false && session.rpcs[i].screenShareState) { + delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality. + } else { + delayedRequestRate(sceneBitrate, i); // well, screw that. Setting it to room quality. + } + } else { + if (screenShareBitrate !== false && session.rpcs[i].screenShareState) { + delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality. + } else { + delayedRequestRate(-1, i); // unlock. + } + } + if (session.rpcs[i].order !== false) { + session.rpcs[i].videoElement.order = session.rpcs[i].order; + } else { + session.rpcs[i].videoElement.order = 0; + } + if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { + if (!(session.rpcs[i].videoElement in mediaPool_invisible)) { + // mediaPool_invisible.push(session.rpcs[i].videoElement); + } else { + errorlog("THIS SHOULD NOT HAPPEN; 650"); + } + } else if (!doNotPush) { + mediaPool.push(session.rpcs[i].videoElement); + } + } else if (session.roomid !== false) { + // guests should see video at low bitrate, ie: 100kbps (not 35kbps like if disabled) + if (session.rpcs[i].order !== false) { + session.rpcs[i].videoElement.order = session.rpcs[i].order; + } else { + session.rpcs[i].videoElement.order = 0; + } + if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { + if (!(session.rpcs[i].videoElement in mediaPool_invisible)) { + // mediaPool_invisible.push(session.rpcs[i].videoElement); + } else { + errorlog("THIS SHOULD NOT HAPPEN; 665"); + } + } else if (!doNotPush) { + mediaPool.push(session.rpcs[i].videoElement); + } + if (session.roomid === "" && session.bitrate) { + // we will let the URL specified bitrate hold, since this isn't a real room. + delayedRequestRate(-1, i); + } else { + if (screenShareBitrate !== false && session.rpcs[i].screenShareState) { + delayedRequestRate(screenShareBitrate, i); // well, screw that. Setting it to room quality. + } else { + delayedRequestRate(roomBitrate, i); // well, screw that. Setting it to room quality. + } + } + } else { + // view=xx,yy or whatever. This should be highest quality. + if (session.rpcs[i].order !== false) { + session.rpcs[i].videoElement.order = session.rpcs[i].order; + } else { + session.rpcs[i].videoElement.order = 0; + } + if (session.activeSpeaker && !session.rpcs[i].defaultSpeaker) { + if (!(session.rpcs[i].videoElement in mediaPool_invisible)) { + // mediaPool_invisible.push(session.rpcs[i].videoElement); + } else { + errorlog("THIS SHOULD NOT HAPPEN; 684"); + } + } else if (!doNotPush) { + mediaPool.push(session.rpcs[i].videoElement); + } + if (sceneBitrate) { + delayedRequestRate(sceneBitrate, i); + } else if (session.screenShareBitrate !== false && session.rpcs[i].screenShareState) { + // session.screenShareBitrate is non-room + delayedRequestRate(session.screenShareBitrate, i); // well, screw that. Setting it to room quality. + } else { + delayedRequestRate(-1, i); + } + } + if (session.rpcs[i].videoElement.nogb == 2) { + session.rpcs[i].videoElement.nogb = 1; + session.rpcs[i].videoElement.classList.add("nogb"); + } else if (session.rpcs[i].videoElement.nogb == 1) { + session.rpcs[i].videoElement.nogb = 0; + session.rpcs[i].videoElement.classList.remove("nogb"); + } + } + } + } + + if (session.broadcastIFrame && session.broadcastIFrame.src) { + // keep alive iframes whennot visible. i think + if (!mediaPool.length) { + mediaPool.push(session.broadcastIFrame); + } + } + + if (document.fullscreenElement) { + try { + if (document.fullscreenElement.tagName === "VIDEO") { + // if its HTML, than we assume its the full canvas + for (var i = 0; i < mediaPool.length; i++) { + // if its your local camera, it shouldn't be a problem, so we can focus on remote cameras only + if (mediaPool[i].id !== document.fullscreenElement.id) { + // if its selected camera, we want to exclude it + if (mediaPool[i].dataset && mediaPool[i].dataset.UUID && mediaPool[i].tagName && mediaPool[i].tagName == "VIDEO") { + delayedRequestRate(session.hiddenSceneViewBitrate, mediaPool[i].dataset.UUID, null); // null implies don't change the current audio setting + mediaPool_invisible.push(mediaPool[i]); // move visible elements to the invisible list, since something is full screen + mediaPool.splice(i, 1); + } + } + } + } + } catch (e) { + errorlog(e); + } + } + + var sscount = 0; + + var skip = false; + + for (var m = 0; m < mediaPool.length; m++) { + mediaPool[m].alreadyAdded = false; + } + + if (!session.layout || session.activeSpeaker) { + if (session.orderby) { + if (session.orderby == "id") { + mediaPool.sort(compare_vids_sid); + } else if (session.orderby == "label") { + mediaPool.sort(compare_vids_label); + } else { + mediaPool.sort(compare_vids_sid); + } + } + mediaPool.sort(compare_vids); + } else if (session.exclusiveLayoutAudio) { + [...mediaPool].forEach(ele => { + if (ele.dataset.sid) { + if (session.layout[ele.dataset.sid]) { + return; + } else if (session.layout[""]) { + let matched = false; + session.layout[""].forEach(i => { + if (i.defaultStreamID && i.defaultStreamID == ele.dataset.sid) { + matched = true; + } + }); + if (matched) { + return; + } + } + const index = mediaPool.indexOf(ele); + if (index > -1) { + if (ele.dataset.UUID && session.scene !== false) { + delayedRequestRate(session.hiddenSceneViewBitrate, ele.dataset.UUID, false); // it's added already, so we know it needs sound. But lets d + } + mediaPool.splice(index, 1); // 2nd parameter means remove one item only + } + } + }); + if (RPCSkeys) { + for (var keyIndex = 0; keyIndex < RPCSkeys.length; keyIndex++) { + i = RPCSkeys[keyIndex]; + if (session.rpcs[i] === null) { + continue; + } + if (session.rpcs[i].streamID) { + let matched = false; + mediaPool.forEach(ele => { + if (ele.dataset.sid == session.rpcs[i].streamID) { + matched = true; + } + }); + if (!matched) { + if (session.rpcs[i].mutedStateMixer === false) { + session.rpcs[i].mutedStateMixer = true; + applyMuteState(i); + } + } + } + } + } + } + + if (session.fakeFeeds && session.fakeFeeds.length && mediaPool.length < session.fakeFeeds.length) { + for (let i = 0; i < session.fakeFeeds.length; i++) { + if (mediaPool.length < session.fakeFeeds.length) { + mediaPool.push(session.fakeFeeds[i]); + } else { + try { + session.fakeFeeds[i].remove(); + } catch (e) { + errorlog(e); + } + } + } + } + + if (session.slotsList && session.slotsList.length > 0) { + // Filter mediaPool to only include videos in the slotsList + const filteredMediaPool = []; + for (let i = 0; i < mediaPool.length; i++) { + if (session.slotsList.includes(i + 1)) { // +1 for 1-indexed slotsList + filteredMediaPool.push(mediaPool[i]); + } + } + mediaPool = filteredMediaPool; + + } + + var mpl = session.slots || mediaPool.length; + + if (!sssid) { + if (mpl > 1) { + var BB = 0; + var rw = 1; + var rh = 1; + var NW; + var NH; + var current; + for (NW = 1; NW <= mpl; NW++) { + NH = Math.ceil(mpl / NW); + var www = ww / NW; + var hhh = hh / NH; + if (www > hhh) { + current = Math.round(hhh * hhh * (mpl / (NW * NH))); + } else { + current = Math.round(www * www * (mpl / (NW * NH))); + } + + if (current >= BB) { + BB = current; + rw = NW; + rh = NH; + } + + if (mediaPool[NW - 1]) { + //if (mediaPool[NW-1].tagName == "VIDEO"){ + if (mediaPool[NW - 1].dataset.UUID) { + if (mediaPool[NW - 1].dataset.UUID in session.rpcs) { + const rpc = session.rpcs[mediaPool[NW - 1].dataset.UUID]; + const currentUUID = mediaPool[NW - 1].dataset.UUID; + + // Skip parent UUID if a _screen counterpart exists (avoid double-counting) + if (!currentUUID.endsWith("_screen") && session.rpcs[currentUUID + "_screen"]) { + // This is a parent with a _screen entry; let the _screen entry be counted instead + continue; + } + + // Check for screen share indicators: flag, UUID suffix, or stream ID suffix + const isScreen = rpc.screenShareState || + currentUUID.endsWith("_screen") || + (mediaPool[NW - 1].dataset.sid && mediaPool[NW - 1].dataset.sid.endsWith(":s")); + + if (isScreen && !rpc.smallScreen) { + let hasLiveScreenVideo = false; + try { + // Check streamSrc first (most reliable for local/direct) + if (rpc.streamSrc && rpc.streamSrc.getVideoTracks) { + hasLiveScreenVideo = rpc.streamSrc.getVideoTracks().some(trk => trk.readyState === "live"); + } + // Fallback: check the video element's srcObject if streamSrc is missing/empty + if (!hasLiveScreenVideo && mediaPool[NW - 1].srcObject && mediaPool[NW - 1].srcObject.getVideoTracks) { + hasLiveScreenVideo = mediaPool[NW - 1].srcObject.getVideoTracks().some(trk => trk.readyState === "live"); + } + } catch (e) { } + + console.log("updateMixer check:", { + uuid: mediaPool[NW - 1].dataset.UUID, + isScreen, + hasLiveScreenVideo, + screenShareState: rpc.screenShareState + }); + + if (hasLiveScreenVideo) { + sscount += 1; + sssid = mediaPool[NW - 1].dataset.sid; + } + } + } + } else if ("id" in mediaPool[NW - 1] && mediaPool[NW - 1].id == "screensharesource" && session.notifyScreenShare) { + sscount += 1; + sssid = mediaPool[NW - 1].dataset.sid; + } + } + } + } else { + var rw = 1; + var rh = 1; + } + + if (sscount > 1) { + sssid = false; // lets not maximize if more than one screen share. + } + } + } catch (e) { + errorlog(e); + sssid = false; + } + + // Add screen share status classes to the gridlayout element + if (sscount > 0) { + playarea.classList.add("has-screenshare"); + playarea.classList.remove("no-screenshare"); + } else { + playarea.classList.add("no-screenshare"); + playarea.classList.remove("has-screenshare"); + } + + var customLayout = false; + + var allowScreenshareAutoLayout = !session.layout || session.activeSpeaker || session.alignRight; + var effectiveScreenshareStyle = session.screenshareStyle; + if (!effectiveScreenshareStyle && session.alignRight) { + effectiveScreenshareStyle = 2; + } + + if (!session.notifyScreenShare && session.scene !== false) { + // this is a scene, so lets assume &smallshare will disable larger screen shares since there is no one to screen share. + } else if (sssid && effectiveScreenshareStyle && allowScreenshareAutoLayout) { + customLayout = {}; + let spotlightLayout; + if (mediaPool.length >= 12) { + spotlightLayout = { x: 10, y: 10, w: 90, h: 90, c: false }; + } else if (mediaPool.length >= 10) { + spotlightLayout = { x: 0, y: 10, w: 100, h: 90, c: false }; + } else if (mediaPool.length >= 8) { + spotlightLayout = { x: 20, y: 20, w: 80, h: 80, c: false }; + } else if (mediaPool.length == 7) { + spotlightLayout = { x: 16.66667, y: 0, w: 83.33333, h: 100, c: false }; + } else if (mediaPool.length == 5 || mediaPool.length == 6) { + spotlightLayout = { x: 20, y: 0, w: 80, h: 100, c: false }; + } else { + spotlightLayout = { x: 20, y: 0, w: 80, h: 100, c: false }; + } + + if (effectiveScreenshareStyle === 2) { + if (spotlightLayout.x < 20) { + spotlightLayout.x = 20; + } + if (spotlightLayout.w > 80) { + spotlightLayout.w = 80; + } + } + + customLayout[sssid] = spotlightLayout; + + if (effectiveScreenshareStyle === 2 && mediaPool.length > 6) { + var columnSlot = 0; + var totalOthers = mediaPool.length - 1; + var slotHeight = totalOthers > 0 ? 100 / totalOthers : 100; + for (var i = 0; i < mediaPool.length; i++) { + if (mediaPool[i].dataset.sid === sssid) { + continue; + } + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: slotHeight * columnSlot, w: 20, h: slotHeight, c: true }; + columnSlot += 1; + } + } else { + var posCount = 0; + for (var i = 0; i < mediaPool.length; i++) { + if (mediaPool[i].dataset.sid === sssid) { + continue; + } + if (mediaPool.length == 2) { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: 0, w: 20, h: 100, c: session.cover }; + } else if (mediaPool.length == 3) { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 41 + 9, w: 20, h: 41, c: session.cover }; + } else if (mediaPool.length == 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 33.3333, w: 20, h: 33.3333, c: session.cover }; + } else if (mediaPool.length == 5) { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: session.cover }; + } else if (mediaPool.length == 6) { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 20, w: 20, h: 20, c: session.cover }; + } else if (mediaPool.length == 7) { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 16.66667, w: 16.66667, h: 16.66667, c: session.cover }; + } else if (mediaPool.length == 8) { + if (posCount == 0 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 70, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 1 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 50, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 2 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 30, y: 0, w: 20, h: 20, c: true }; + } else { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: true }; + } + } else if (mediaPool.length == 9) { + if (posCount == 0 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 1 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 60, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 2 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 40, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 3 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 20, y: 0, w: 20, h: 20, c: true }; + } else { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 25, w: 20, h: 25, c: true }; + } + } else if (mediaPool.length >= 10) { + if (posCount < 10) { + customLayout[mediaPool[i].dataset.sid] = { x: 90 - 10 * posCount, y: 0, w: 10, h: 10, c: true }; + } else { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: (posCount - 9) * 10, w: 10, h: 10, c: true }; + } + } else { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: posCount * 20, w: 20, h: 20, c: true }; + } + posCount += 1; + } + } + } else if (sssid && allowScreenshareAutoLayout) { + customLayout = {}; + + if (mediaPool.length >= 12) { + customLayout[sssid] = { x: 0, y: 10, w: 90, h: 90, c: false }; + } else if (mediaPool.length >= 10) { + customLayout[sssid] = { x: 0, y: 10, w: 100, h: 90, c: false }; + } else if (mediaPool.length >= 8) { + customLayout[sssid] = { x: 0, y: 20, w: 80, h: 80, c: false }; + } else if (mediaPool.length == 7) { + customLayout[sssid] = { x: 0, y: 0, w: 83.33333, h: 100, c: false }; + } else if (mediaPool.length == 5) { + customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false }; + } else if (mediaPool.length == 6) { + customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false }; + } else if (mediaPool.length == 1) { + customLayout[sssid] = { x: 0, y: 0, w: 100, h: 100, c: false }; + } else { + customLayout[sssid] = { x: 0, y: 0, w: 80, h: 100, c: false }; + } + var posCount = 0; + for (var i = 0; i < mediaPool.length; i++) { + if (mediaPool[i].dataset.sid === sssid) { + continue; + } + if (mediaPool.length == 2) { + // + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: 0, w: 20, h: 100, c: session.cover }; + } else if (mediaPool.length == 3) { + // + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 41 + 9, w: 20, h: 41, c: session.cover }; + } else if (mediaPool.length == 4) { + // + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 33.3333, w: 20, h: 33.3333, c: session.cover }; + } else if (mediaPool.length == 5) { + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: session.cover }; + } else if (mediaPool.length == 6) { + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 20, w: 20, h: 20, c: session.cover }; + } else if (mediaPool.length == 7) { + customLayout[mediaPool[i].dataset.sid] = { x: 83.33333, y: posCount * 16.66667, w: 16.66667, h: 16.66667, c: session.cover }; + } else if (mediaPool.length == 8) { + if (posCount == 0 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 10, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 1 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 30, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 2 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 50, y: 0, w: 20, h: 20, c: true }; + } else { + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: true }; + } + } else if (mediaPool.length == 9) { + if (posCount == 0 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 0, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 1 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 20, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 2 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 40, y: 0, w: 20, h: 20, c: true }; + } else if (posCount == 3 + 4) { + customLayout[mediaPool[i].dataset.sid] = { x: 60, y: 0, w: 20, h: 20, c: true }; + } else { + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 25, w: 20, h: 25, c: true }; + } + } else if (mediaPool.length >= 10) { + if (posCount < 10) { + customLayout[mediaPool[i].dataset.sid] = { x: 10 * posCount, y: 0, w: 10, h: 10, c: true }; + } else { + customLayout[mediaPool[i].dataset.sid] = { x: 90, y: (posCount - 9) * 10, w: 10, h: 10, c: true }; + } + } else { + customLayout[mediaPool[i].dataset.sid] = { x: 80, y: posCount * 20, w: 20, h: 20, c: true }; // + } + posCount += 1; + } + + } else if (session.rows && mediaPool.length) { + try { + customLayout = {}; + let n = mediaPool.length; + + if (session.slots && n < session.slots) { + n = session.slots; // If we have fewer videos than slots, still use the full slots count + } + + let rows = 1; + if (session.rows.length >= n) { + rows = parseInt(session.rows[n - 1]) || 1; + } else { + rows = parseInt(session.rows[session.rows.length - 1]) || 1; + } + + if (rows < 0) { + rows = Math.abs(rows); + let cols = Math.ceil(n / rows) || 1; + for (var i = 0; i < n; i++) { + let col = i % cols; + let row = parseInt(i / cols) % rows; + + if (row === Math.floor((n - 1) / cols) && n % cols !== 0) { // Last row logic + let itemsInLastRow = n % cols || cols; + let offset = (cols - itemsInLastRow) * (100 / cols) / 2; + customLayout[mediaPool[i].dataset.sid] = { + y: (100 / rows) * row, + h: 100 / rows, + x: offset + (100 / cols) * col, + w: 100 / cols, + c: session.cover + }; + } else { + customLayout[mediaPool[i].dataset.sid] = { + y: (100 / rows) * row, + h: 100 / rows, + x: (100 / cols) * col, + w: 100 / cols, + c: session.cover + }; + } + } + } else { + + let cols; + + if (session.slots) { + cols = session.slots / rows; + } else { + cols = Math.ceil(n / rows) || 1; + } + + for (var i = 0; i < n; i++) { + let col = i % cols; + let row = parseInt(i / cols) % rows; + //console.log(row,col, rows,cols,i,n) + if (cols >= 2 && rows >= 2 && n < (row + 1) * cols) { + let delta = (row + 1) * cols - n; + customLayout[mediaPool[i].dataset.sid] = { y: (100 / rows) * row, h: 100 / rows, x: (100 / cols) * (col + delta), w: 100 / cols, c: session.cover }; + } else { + customLayout[mediaPool[i].dataset.sid] = { y: (100 / rows) * row, h: 100 / rows, x: (100 / cols) * col, w: 100 / cols, c: session.cover }; + } + } + } + } catch (e) { + errorlog(e); + } + } + + try { + if (!skip) { + var childNodes = playarea.childNodes; + + for (var n = 0; n < childNodes.length; n++) { + if (childNodes[n].querySelector("video")) { + var vidtemp = childNodes[n].querySelector("video"); + var matched = false; + for (var m = 0; m < mediaPool.length; m++) { + if (vidtemp.id === mediaPool[m].id) { + vidtemp.alreadyAdded = true; + mediaPool[m] = vidtemp; + matched = true; + childNodes[n].matched = true; + break; + } + } + if (!matched && vidtemp.isInvisible) { + vidtemp.isInvisible = false; + if (session.pauseInvisible && vidtemp.dataset.UUID) { + applyMuteState(vidtemp.dataset.UUID); + } + } + } else if (childNodes[n].querySelector("iframe")) { + var iftemp = childNodes[n].querySelector("iframe"); + if (iftemp.framesource) { + if ((!session.activeSpeaker && session.layout) && session.layout[""]) { + for (var i = 0; i < session.layout[""].length; i++) { + if (session.layout[""][i].iframeSrc && session.layout[""][i].iframeSrc == iftemp.framesource) { + childNodes[n].matched = true; + if (iftemp.isConnected) { + iftemp.alreadyAdded = true; + } + } + } + } + continue; + } + for (var m = 0; m < mediaPool.length; m++) { + if (mediaPool[m].nodeName === "IFRAME" && mediaPool[m].src && iftemp.src === mediaPool[m].src) { + iftemp.alreadyAdded = true; + iftemp.id = mediaPool[m].id; + if (session.directorList.indexOf(iftemp.dataset.UUID) == -1) { + iftemp.dataset.UUID = mediaPool[m].dataset.UUID; + iftemp.dataset.sid = mediaPool[m].dataset.sid; + } + mediaPool[m] = iftemp; + childNodes[n].matched = true; + break; + } + } + for (var m = 0; m < mediaPool_invisible.length; m++) { + if (mediaPool_invisible[m].nodeName === "IFRAME" && mediaPool_invisible[m].src && iftemp.src === mediaPool_invisible[m].src) { + iftemp.alreadyAdded = true; + iftemp.id = mediaPool_invisible[m].id; + if (session.directorList.indexOf(iftemp.dataset.UUID) == -1) { + iftemp.dataset.UUID = mediaPool_invisible[m].dataset.UUID; + iftemp.dataset.sid = mediaPool_invisible[m].dataset.sid; + } + mediaPool_invisible[m] = iftemp; + childNodes[n].matched = true; + break; + } + } + } + } + + for (var n = 0; n < childNodes.length; n++) { + if (!childNodes[n].matched) { + playarea.removeChild(childNodes[n]); + n--; + } else { + childNodes[n].matched = null; + } + } + } + } catch (e) { + errorlog(e); + } + + if (session.videoElement && (session.videoElement.src || session.videoElement.srcObject)) { + // fileshare or stream + if ("playlist" in session.videoElement) { + playarea.appendChild(session.videoElement); // fileshare. + } else if (session.videoElement.style.display !== "none") { + if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length) { + if (miniPreview) { + var container = null; + if (mpl === 0 && miniPreview === 2) { + if (soloVideo !== true) { + // since miniPreview==2, we want to full screen our preview. Deleting the old mini preview container will ensure it loads right. + if (!session.layout || session.activeSpeaker) { + if (session.videoElement.container && session.videoElement.container.id == "minipreview") { + delete session.videoElement.container; + } + if (document.getElementById("minipreview")) { + document.getElementById("minipreview").remove(); + } + } + // + mediaPool.push(session.videoElement); + mpl = 1; + } + } else if (miniPreview === 3) { + if (soloVideo !== true) { + container = document.createElement("div"); + session.videoElement.container = container; + container.style.top = "-500px"; + container.style.left = "-500px"; + container.style.width = "1px"; + container.style.height = "1px"; + //container.style.display = "flex"; + container.style.zIndex = "0"; + container.style.margin = "0"; + container.style.position = "absolute"; + container.style.cursor = "pointer"; + container.style.border = "0"; + container.appendChild(session.videoElement); + playarea.appendChild(container); + } + } else if (soloVideo !== true) { + if (document.getElementById("minipreview")) { + container = document.getElementById("minipreview"); + } else { + container = document.createElement("div"); + var togglePreview = document.createElement("div"); + togglePreview.className = "togglePreview"; + try { + container.style.top = "calc(" + hi + "px + 2vh)"; + container.style.maxHeight = parseInt(playarea.offsetHeight) + "px"; + togglePreview.style.top = "calc(" + hi + "px + 2vh)"; + togglePreview.style.maxHeight = parseInt(playarea.offsetHeight) + "px"; + } catch (e) { + errorlog(e); + container.style.top = hi + "px"; + togglePreview.style.top = hi + "px"; + } + // + if (miniPerformerY !== null) { + container.style.top = miniPerformerY + "%"; + } + if (miniPerformerX !== null) { + if (session.widget && !session.leftMiniPreview) { + if (miniPerformerX > (99 - session.widgetwidth)) { + miniPerformerX = 99 - session.widgetwidth; + } + } + container.style.left = miniPerformerX + "%"; + } else if (session.leftMiniPreview !== false) { + container.style.left = session.leftMiniPreview + "%"; + togglePreview.style.left = session.leftMiniPreview + "%"; + } else if (session.widget) { + container.style.right = session.widgetwidth + "%"; + togglePreview.style.right = session.widgetwidth + "%"; + } else { + container.style.right = "2vw"; + togglePreview.style.right = "2vw"; + } + container.appendChild(session.videoElement); + session.videoElement.container = container; + playarea.appendChild(container); + togglePreview.innerHTML = ''; + if (!session.previewToggleState) { + container.classList.toggle("hidden"); + togglePreview.classList.toggle("blinded"); + } + if (!(iOS || iPad)) { + playarea.appendChild(togglePreview); + } + togglePreview.onclick = function (event) { + event.preventDefault(); + event.stopPropagation(); + getById("minipreview").classList.toggle("hidden"); + this.classList.toggle("blinded"); + session.previewToggleState = !session.previewToggleState; + return false; + }; + makeMiniDraggableElement(container); + container.id = "minipreview"; + } + container.style.width = "18%"; + //container.style.display = "flex"; + container.style.zIndex = "3"; + container.style.margin = "0"; + container.style.position = "absolute"; + container.style.cursor = "pointer"; + container.style.border = "2px #BBB solid"; + container.style.height = "block"; + applyMirror(session.mirrorExclude); + } else if (soloVideo === true) { + if (document.getElementById("minipreview")) { + container = document.getElementById("minipreview"); + container.style.height = "100%"; + //container.style.transform = "block"; + //container.style.transformOrigin = "unset"; + } + } + if (session.ruleOfThirds) { + if (container && container.id == "minipreview" && !container.svg) { + var svg = document.createElement("img"); + svg.src = session.ruleOfThirds; + svg.style.width = "100%"; + svg.style.height = "100%"; + svg.style.position = "absolute"; + svg.style.left = "0"; + svg.style.top = "0"; + container.svg = svg; + container.appendChild(svg); + } + } + container = null; // clear reference + } + } else if (session.streamSrc && !session.videoElement.srcObject) { + warnlog("THIS SHOULD NOT HAPPEN; 2067"); + } + } + } + + try { + if (session.slots) { + var slotArray = []; + mediaPool.forEach(vid => { + if (vid.slotBlank) { + vid.slotBlank = false; + vid.slot = 0; + } + + if ("slot" in vid && vid.slot) { + if (!slotArray.includes(parseInt(vid.slot))) { + slotArray.push(parseInt(vid.slot)); + } else { + vid.slot = 0; + //mediaPool_invisible.push(vid); + //var index = mediaPool.indexOf(vid); + //if (index > -1) { + // mediaPool.splice(index, 1); + //} + } + } + }); + var slotCounter = 1; + mediaPool.reverse(); + var j = mediaPool.length; + while (j--) { + if (!("slot" in mediaPool[j]) || mediaPool[j].slot == "0" || !mediaPool[j].slot) { + while (slotArray.includes(slotCounter)) { + slotCounter += 1; + } + slotArray.push(slotCounter); + mediaPool[j].slot = slotCounter; + mediaPool[j].slotBlank = true; + } + if (!("slot" in mediaPool[j]) || !parseInt(mediaPool[j].slot) || mediaPool[j].slot == "0" || !mediaPool[j].slot || session.slots < parseInt(mediaPool[j].slot)) { + //if ((!("slot" in mediaPool[j]) || !parseInt(mediaPool[j].slot) || mediaPool[j].slot == "0" || !mediaPool[j].slot || session.slots < parseInt(mediaPool[j].slot)) && !customLayout) { + mediaPool_invisible.push(mediaPool[j]); + mediaPool.splice(j, 1); + } + } + mediaPool.reverse(); + } + + if (session.pauseInvisible) { + mediaPool_invisible.forEach(vid => { // This is an experimental version, with improvements? Maybe its better? + if (vid) { + try { + vid.style.width = ""; + vid.style.height = ""; + if (!vid.isInvisible) { + vid.isInvisible = true; + if (session.pauseInvisible && vid.dataset.UUID) { + session.requestRateLimit(session.hiddenSceneViewBitrate, vid.dataset.UUID, true); + vid.muted = true; + } + } + if (vid.dataset.doNotMove) { + return; + } + vid.style.top = "0px"; + vid.style.left = "0px"; + if (vid.isConnected) { + console.warn("Video is invisible, yet connected to the DOM?"); + } + } catch (e) { + errorlog(e); + } + } + }); + } else { // this is the old version; a fail safe. + mediaPool_invisible.forEach(vid => { + if (vid) { + try { + vid.style.width = "0px"; + vid.style.height = "0px"; + vid.style.top = "0px"; + vid.style.left = "0px"; + vid.isInvisible = true; + if (vid.alreadyAdded && vid.alreadyAdded == true) { + vid.alreadyAdded = false; + return; + } else if (vid.dataset.doNotMove) { + return; + } + if (!(vid.nodeName == "IFRAME" && vid.isConnected)) { + playarea.appendChild(vid); + } + } catch (e) { + errorlog(e); + } + } + }); + } + + } catch (e) { + errorlog(e); + } + + var layout = false; + if (customLayout || (!session.activeSpeaker && session.layout)) { + layout = session.layout || customLayout; + layout = { ...layout }; + if (layout[""]) { + for (var i = 0; i < layout[""].length; i++) { + if (layout[""][i].defaultStreamID && !layout[layout[""][i].defaultStreamID]) { + var found = false; + for (var ell in mediaPool) { + if (mediaPool[ell].dataset.sid && mediaPool[ell].dataset.sid === layout[""][i].defaultStreamID) { + layout[layout[""][i].defaultStreamID] = layout[""][i]; + found = true; + } + } + if (found) { + continue; + } + } + layout["#" + i] = layout[""][i]; + if (layout["#" + i].iframeSrc) { + if (session.iframeSrcs[layout["#" + i].iframeSrc]) { + var ele = session.iframeSrcs[layout["#" + i].iframeSrc]; + ele.id = "#" + i; + } else { + var ele = loadIframe(parseURL4Iframe(layout["#" + i].iframeSrc), "#" + i); + ele.framesource = layout["#" + i].iframeSrc; + session.iframeSrcs[layout["#" + i].iframeSrc] = ele; + } + //ele.alreadyAdded = true; + //ele.matched = true; + } else if (layout["#" + i].backgroundMedia || layout["#" + i].text || layout["#" + i].foregroundMedia) { + var ele = document.createElement("div"); + ele.dataset.sid = "#" + i; + } else { + continue; + } + ele.dataset.sid = "#" + i; + mediaPool.push(ele); + } + } + + try { + mediaPool = sortByZ(mediaPool, layout); + } catch (e) { + // layout = false; + errorlog(e); + } + } + var i = 0; + var offset = 0; + + if (session.waitImage && !(mediaPool_invisible.length || mediaPool.length)) { + if (!session.waitImageTimeoutObject) { + session.waitImageTimeoutObject = setTimeout(function () { + session.waitImageTimeoutObject = true; + if (!document.getElementById("retryimage")) { + playarea.innerHTML += ''; + getById("retryimage").src = decodeURIComponent(session.waitImage); + getById("retryimage").onerror = function () { + this.style.display = "none"; + }; + + if (session.cover) { + getById("retryimage").style.objectFit = "cover"; + } + } + getById("retryimage").style.display = "block"; + }, session.waitImageTimeout); + } + } else if (session.waitImage) { + try { + clearTimeout(session.waitImageTimeoutObject); + session.waitImageTimeoutObject = false; + getById("retryimage").style.display = "none"; + } catch (e) { } + } + mediaPool.forEach(vid => { + try { + if (!vid || !("id" in vid)) { + errorlog(vid); + return; + } + + if (vid.needsLoading) { + try { + vid.load(); + } catch (e) { + errorlog(e); + } + } + + if (session.slots) { + if ("slot" in vid && parseInt(vid.slot)) { + i = parseInt(vid.slot) - 1; + if (i < 0) { + return; + } + } else { + return; + } + } + + var offsetx = 0; + if (i !== 0) { + if (Math.ceil((i + 0.01) / rw) == rh) { + if (mpl % rw) { + offsetx = Math.max(((rw - (mpl % rw)) * (w / rw)) / 2, 0); + } + } + } + + var cover = session.cover; + var borderOffset = session.border || 0; + var videoMargin = session.videoMargin || 0; + var borderRadius = session.borderRadius || 0; + var borderColor = session.borderColor || "#000"; + var cropTop = 0, cropRight = 0, cropBottom = 0, cropLeft = 0; + var fadein = session.fadein || false; + var backgroundMedia = session.defaultMedia || false; + var foregroundMedia = session.defaultOverlayMedia || false; + var animated = session.animatedMoves || 0; + var textOverlay = false; + if (!borderOffset) { + borderColor = "#0000"; + } + + if (layout) { + if (!(vid.dataset.sid && vid.dataset.sid in layout)) { + + if (vid.container) { + vid.container.style.display = "none"; + } + + vid.isInvisible = true; + if (session.pauseInvisible && vid.dataset.UUID) { + session.requestRateLimit(session.hiddenSceneViewBitrate, vid.dataset.UUID, true); + vid.muted = true; + return; + } + + if (vid.dataset.UUID) { + delayedRequestRate(session.hiddenSceneViewBitrate, vid.dataset.UUID, false); // it's added already, so we know it needs sound. But lets d + } + return; + } + if ("borderThickness" in layout[vid.dataset.sid]) { + borderOffset = layout[vid.dataset.sid].borderThickness || 0; + } + if ("animated" in layout[vid.dataset.sid]) { + animated = layout[vid.dataset.sid].animated || 0; + if (animated === true) { + animated = session.animatedMoves || 50; + } + } + if ("margin" in layout[vid.dataset.sid]) { + videoMargin = layout[vid.dataset.sid].margin || 0; + } + if ("rounded" in layout[vid.dataset.sid]) { + borderRadius = layout[vid.dataset.sid].rounded || 0; + } + if (layout[vid.dataset.sid].borderColor) { + borderColor = layout[vid.dataset.sid].borderColor; + } + // Crop properties + if ("cropTop" in layout[vid.dataset.sid] || "cropRight" in layout[vid.dataset.sid] || + "cropBottom" in layout[vid.dataset.sid] || "cropLeft" in layout[vid.dataset.sid]) { + cropTop = layout[vid.dataset.sid].cropTop || 0; + cropRight = layout[vid.dataset.sid].cropRight || 0; + cropBottom = layout[vid.dataset.sid].cropBottom || 0; + cropLeft = layout[vid.dataset.sid].cropLeft || 0; + } + if (layout[vid.dataset.sid].fadeIn) { + fadein = layout[vid.dataset.sid].fadeIn; + } + if ("backgroundMedia" in layout[vid.dataset.sid]) { + backgroundMedia = layout[vid.dataset.sid].backgroundMedia || false; + } + if ("foregroundMedia" in layout[vid.dataset.sid]) { + foregroundMedia = layout[vid.dataset.sid].foregroundMedia || false; + } + + if (layout[vid.dataset.sid].text) { + + if (!vid.container || !vid.container.textOverlay) { + textOverlay = document.createElement("div"); + textOverlay.className = "textOverlay"; + //vid.container.appendChild(vid.container.textOverlay); + } else { + textOverlay = vid.container.textOverlay; + } + + textOverlay.innerText = layout[vid.dataset.sid].text; + textOverlay.style.color = layout[vid.dataset.sid].textColor || "#ffffff"; + textOverlay.style.fontSize = layout[vid.dataset.sid].fontSize || "24px"; + textOverlay.style.fontFamily = layout[vid.dataset.sid].fontFamily || "Arial, sans-serif"; + textOverlay.style.position = "absolute"; + textOverlay.style.width = "100%"; + textOverlay.style.textAlign = "center"; + textOverlay.style.zIndex = "10"; + + // Position the text + const textPosition = layout[vid.dataset.sid].textPosition || "50%"; + textOverlay.style.top = textPosition; + textOverlay.style.transform = "translateY(-50%)"; + + // Add background if specified + if (layout[vid.dataset.sid].textBackground) { + textOverlay.style.backgroundColor = layout[vid.dataset.sid].textBackground; + textOverlay.style.padding = "10px"; + } else { + textOverlay.style.backgroundColor = "transparent"; + textOverlay.style.textShadow = "1px 1px 2px rgba(0,0,0,0.8)"; + } + } else if (vid.container && vid.container.textOverlay) { + vid.container.textOverlay.remove(); + delete vid.container.textOverlay; + } + + if (vid.container) { + if (!(vid.nodeName == "IFRAME" && vid.isConnected)) { + // moving an iframe will break it. + if (!vid.alreadyAdded || vid.nodeName == "IFRAME") { + playarea.appendChild(vid.container); + } + } + } + } + + var skipAnimation = false; + if (vid.isInvisible) { + vid.isInvisible = false; + if (session.pauseInvisible && vid.dataset.UUID) { + applyMuteState(vid.dataset.UUID); + } + skipAnimation = true; + if (fadein) { + vid.classList.add("fadein"); + if (vid.holder) { + vid.holder.classList.add("fadein"); + } + } + + } + + offsety = Math.max((h - Math.ceil(mpl / rw) * Math.ceil(h / rh)) / 2, 0); + + if (vid.container) { + var container = vid.container; + if (container.move) { + clearInterval(container.move); + container.move = null; + } + } else { + var container = document.createElement("div"); + vid.container = container; + } + container.style.position = "absolute"; + container.style.display = "block"; + container.classList.add("container_holder_video"); + + // Add screen share class to individual containers + var isScreenShare = false; + var vidUUID = vid.dataset.UUID; + var vidSid = vid.dataset.sid; + if (vidUUID && session.rpcs[vidUUID] && !vidSid && session.rpcs[vidUUID].streamID) { + vidSid = session.rpcs[vidUUID].streamID; + } + + if (vid.id === "screensharesource" || (vidUUID && vidUUID.endsWith("_screen")) || (vidSid && vidSid.endsWith(":s"))) { + isScreenShare = true; + } else if (vidUUID && session.rpcs[vidUUID] && session.rpcs[vidUUID].screenShareState) { + if (!session.rpcs[vidUUID + "_screen"]) { + isScreenShare = true; + } + } + + if (isScreenShare) { + container.classList.add("is-screenshare"); + container.classList.remove("is-not-screenshare"); + } else { + container.classList.add("is-not-screenshare"); + container.classList.remove("is-screenshare"); + } + + // ANIMATED - CONTAINER ; width/height/z-index/cover/////////////// + if (layout) { + try { + var left = (w / 100) * layout[vid.dataset.sid].x || layout[vid.dataset.sid].xp || 0; + var top = (h / 100) * layout[vid.dataset.sid].y || layout[vid.dataset.sid].yp || 0; + top += hi; + var width = (w / 100) * layout[vid.dataset.sid].w || layout[vid.dataset.sid].wp || 0; + var height = (h / 100) * layout[vid.dataset.sid].h || layout[vid.dataset.sid].hp || 0; + + if (layout[vid.dataset.sid].cover || layout[vid.dataset.sid].c) { + // this should be true/false + //vid.style.objectFit = "cover"; + cover = layout[vid.dataset.sid].cover || layout[vid.dataset.sid].c; + } else { + //vid.style.objectFit = "contain"; // this should fall back to sessio.cover if no layout supplied + cover = false; + } + //container.style.zindex = 0; + container.style.zIndex = layout[vid.dataset.sid].zIndex || layout[vid.dataset.sid].z || 0; + } catch (e) { + errorlog(e); + } + } else { + var left = Math.max(offsetx + Math.floor((((i % rw) + 0) * w) / rw), 0); + var top = Math.max(offsety + Math.floor(((Math.floor(i / rw) + 0) * h) / rh + hi), 0); + var width = Math.ceil(w / rw); + var height = Math.ceil(h / rh); + //container.style.zIndex = 0; + } + + var computed = getComputedStyle(vid); + + if (animated && !skipAnimation) { + container.style.transition = "width " + animated + "ms ease-in-out 0s, height " + animated + "ms ease-in-out 0s, background-color " + animated + "ms ease-in-out 0s, transform " + animated + "ms ease-in-out 0s, top " + animated + "ms ease-in-out 0s, left " + animated + "ms ease-in-out 0s"; + } else { + container.style.transition = ""; + } + + if (layout) { + ////////////////// NOT ANIMATED - CONTAINER ; width/height/z-index/cover/////////////// + container.style.left = left + "px"; + container.style.top = top + "px"; + container.style.width = width + "px"; + container.style.height = height + "px"; + container.twidth = width; + container.theight = height; + } else { + container.style.left = offsetx + Math.floor((((i % rw) + 0) * w) / rw) + "px"; + container.style.top = offsety + Math.floor(((Math.floor(i / rw) + 0) * h) / rh + hi) + "px"; + container.twidth = Math.ceil(w / rw); + container.theight = Math.ceil(h / rh); + container.style.width = container.twidth + "px"; + container.style.height = container.theight + "px"; + } + + var maxWidth = 0; + if (parseInt(computed.width) > parseInt(container.style.width)) { + maxWidth = computed.width; + } else { + maxWidth = container.style.width; + } + + var maxHeight = 0; + if (parseInt(computed.height) > parseInt(container.style.height)) { + maxHeight = computed.height; + } else { + maxHeight = container.style.height; + } + if (cover === true) { + vid.style.maxWidth = maxWidth; + vid.style.maxHeight = maxHeight; + vid.style.objectFit = "cover"; + } else if (cover == 2) { + // For session.cover == 2, determine whether to use cover or contain + // based on aspect ratio comparison + vid.style.maxWidth = maxWidth; + vid.style.maxHeight = maxHeight; + + const vw = vid.naturalWidth || vid.videoWidth || 0; + const vh = vid.naturalHeight || vid.videoHeight || 0; + + if (vw && vh) { + // Calculate aspect ratios + const videoAspect = vw / vh; + + // Use container dimensions for comparison + const containerWidth = parseFloat(maxWidth); + const containerHeight = parseFloat(maxHeight); + const containerAspect = containerWidth / containerHeight; + + // If video is wider than container proportionally (width being squished), + // use cover. Otherwise use contain. + if (videoAspect > containerAspect) { + vid.style.objectFit = "cover"; + } else { + vid.style.objectFit = "contain"; + } + } else { + // Default to contain if we can't determine dimensions + vid.style.objectFit = "contain"; + } + } else { + vid.style.objectFit = "contain"; + vid.style.maxWidth = maxWidth; + vid.style.maxHeight = maxHeight; + } + + //try { + if (vid.alreadyAdded && vid.alreadyAdded == true) { + if (!container.holder) { + var holder = document.createElement("div"); + container.holder = holder; + holder.className = "holder"; + holder.dataset.holder = true; + container.appendChild(holder); + holder.appendChild(vid); + } else { + var holder = container.holder; + } + } else if (vid.dataset.doNotMove) { + vid.style.position = "absolute"; + vid.style.left = left + "px"; + vid.style.top = top + "px"; + vid.style.width = width + "px"; + vid.style.height = height + "px"; + vid.style.display = "flex"; + i += 1; + return; + } else { + if (!container.holder) { + var holder = document.createElement("div"); + container.holder = holder; + holder.className = "holder"; + holder.dataset.holder = true; + holder.appendChild(vid); + container.appendChild(holder); + } else { + var holder = container.holder; + holder.prepend(vid); + } + playarea.appendChild(container); + vid.style.maxWidth = "100%"; + vid.style.maxHeight = "100%"; + } + + if (layout) { + var wrw = (w / 100) * layout[vid.dataset.sid].w || 0; + var hrh = (h / 100) * layout[vid.dataset.sid].h || 0; + } else { + var wrw = w / rw; + var hrh = h / rh; + } + + if (backgroundMedia) { + container.style.backgroundImage = "url(" + backgroundMedia + ")"; + if (cover) { + container.style.backgroundSize = "cover"; + } else { + container.style.backgroundSize = "contain"; + } + container.style.backgroundPosition = "center"; + container.style.backgroundRepeat = "no-repeat"; + } else if (container.style.backgroundImage) { + container.style.backgroundImage = "unset"; + } + + if (foregroundMedia) { + if (!container.foregroundMedia) { + container.foregroundMedia = document.createElement("img"); + container.foregroundMedia.className = "foregroundMedia"; + container.appendChild(container.foregroundMedia); + } + container.foregroundMedia.src = foregroundMedia; + } else if (container.foregroundMedia) { + try { + container.foregroundMedia.remove(); + delete container.foregroundMedia; + } catch (e) { + errorlog(e); + } + } + + if (textOverlay && !container.textOverlay) { + container.appendChild(textOverlay); + } + + if ("rotated" in vid && vid.rotated !== false) { + if (vid.dataset) { + vid.dataset.rotated = vid.rotated ? vid.rotated : "0"; + } + updateVideoTransform(vid); + } else if (vid.dataset && vid.dataset.rotated) { + vid.dataset.rotated = "0"; + updateVideoTransform(vid); + } + + vid.style.width = "100%"; + vid.style.height = "100%"; + holder.style.position = "absolute"; + + if (vid.classList.contains("paused")) { + if (holder.paused) { + holder.paused.className = "playButton"; + } else { + var paused = document.createElement("span"); + paused.id = "paused_" + vid.dataset.UUID; + paused.className = "playButton"; + paused.dataset.UUID = vid.dataset.UUID; + paused.onclick = function () { + unPauseVideo(vid); + }; + holder.paused = paused; + holder.appendChild(paused); + } + } else if (holder.paused) { + holder.paused.className = "hidden"; + } + + var vw = vid.naturalWidth || vid.videoWidth; // naturalWidth is for images I guess + var vh = vid.naturalHeight || vid.videoHeight; + + // log(vw + " : "+vh); + + if (cover && !session.structure) { + ////// + if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) { + holder.style.left = borderOffset + "px"; + holder.style.top = borderOffset + "px"; + holder.style.height = "calc(100% - " + videoMargin * 2 + "px)"; + holder.style.width = "calc(100% - " + videoMargin * 2 + "px)"; + + vid.style.width = height - (borderOffset + videoMargin) * 2 + "px"; + vid.style.height = width - (borderOffset + videoMargin) * 2 + "px"; + vid.style.left = 0; + vid.style.top = 0; + } else { + holder.style.left = videoMargin + "px"; + holder.style.top = videoMargin + "px"; + holder.style.height = "calc(100% - " + videoMargin * 2 + "px)"; + holder.style.width = "calc(100% - " + videoMargin * 2 + "px)"; + + vid.style.width = "100%"; + vid.style.height = "100%"; + vid.style.left = 0; + vid.style.top = 0; + } + + ////////// COVER VERSION + if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) { + // do not dynamically scale the screen share feed. + } else if (session.dynamicScale) { + if (vid.dataset.UUID) { + let targetWidth = wrw; + let targetHeight = hrh; + targetWidth -= (borderOffset + videoMargin) * 2; + targetHeight -= (borderOffset + videoMargin) * 2; + if (targetWidth < 0) { + targetWidth = 0; + } + if (targetHeight < 0) { + targetHeight = 0; + } + if (session.devicePixelRatio) { + session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only + } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) { + session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover); + } else { + session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover); + } + } + } + + vid.style.borderColor = borderColor; + vid.style.borderWidth = borderOffset + "px"; + vid.style.borderRadius = borderRadius + "px"; + // Apply crop via clip-path + if (cropTop || cropRight || cropBottom || cropLeft) { + vid.style.clipPath = `inset(${cropTop}px ${cropRight}px ${cropBottom}px ${cropLeft}px)`; + } else { + vid.style.clipPath = ""; + } + holder.style.borderColor = borderColor; + holder.style.borderWidth = "0px"; + holder.style.borderRadius = borderRadius + "px"; + } else if ((vw && vh) || (vid.width && vid.height) || vid.dataset.aspectRatio) { + if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) { + if (vw && vh) { + var vvw = parseInt(vh); + var vvh = parseInt(vw); + } else if (vid.width && vid.height) { + var vvw = parseInt(vid.height); + var vvh = parseInt(vid.width); + } else { + // video disabled; fall back to aspect Ratio + var vvw = 1; + var vvh = vid.dataset.aspectRatio; + } + + vid.style.objectFit = "cover"; //contain; + vid.style.overflow = "unset"; //contain; + //vid.style.maxWidth = "unset"; + //vid.style.maxHeight = "unset"; + } else { + if (vw && vh) { + var vvw = parseInt(vw); + var vvh = parseInt(vh); + } else if (vid.width && vid.height) { + var vvw = parseInt(vid.width); + var vvh = parseInt(vid.height); + } else { + var vvw = vid.dataset.aspectRatio; + var vvh = 1; + } + } + + var asw = (wrw - videoMargin * 2 - borderOffset * 2) / vvw; // (window.innerWidth/ N) / vid.videoHeight; + var ash = (hrh - videoMargin * 2 - borderOffset * 2) / vvh; + + if (session.structure) { + // wrw x hrh + var arx = (wrw - videoMargin * 2 - borderOffset * 2) / (hrh - videoMargin * 2 - borderOffset * 2); + var tarx = arW / arH; + //var arW = 16.0; + //var arH = 9.0; + if (arx > tarx) { + // width is too long + var hsw = hrh * tarx - videoMargin * 2 * tarx - borderOffset * 2; + var hsl = (wrw - hsw) / 2; + var hst = videoMargin; + var hsh = hrh - videoMargin * 2; + } else { + var hsh = (wrw - videoMargin * 2 + borderOffset * 2) / tarx; + var hst = (hrh - hsh) / 2; + var hsl = videoMargin; + var hsw = wrw - videoMargin * 2; + } + } else if (asw > ash) { + var hsh = hrh - videoMargin * 2; + var hst = videoMargin; + var hsw = (hsh - borderOffset * 2) * (vvw / vvh) + borderOffset * 2; + var hsl = (wrw - hsw) / 2; + } else { + var hsw = wrw - videoMargin * 2; + var hsl = videoMargin; + var hsh = (hsw - borderOffset * 2) / (vvw / vvh) + borderOffset * 2; + var hst = (hrh - hsh) / 2; + } + + holder.style.left = Math.floor(hsl) + "px"; // this needs to be replaced with padding. This means testing with rotation = 90 + holder.style.top = Math.floor(hst) + "px"; + holder.style.width = Math.ceil(hsw) + "px"; + holder.style.height = Math.ceil(hsh) + "px"; + //holder.style.padding = videoMargin + "px"; + + holder.style.borderColor = borderColor; + holder.style.borderWidth = borderOffset + "px"; + holder.style.borderRadius = borderRadius + "px"; + vid.style.borderWidth = "0px"; + // Apply crop via clip-path + if (cropTop || cropRight || cropBottom || cropLeft) { + vid.style.clipPath = `inset(${cropTop}px ${cropRight}px ${cropBottom}px ${cropLeft}px)`; + } else { + vid.style.clipPath = ""; + } + + if ("rotated" in vid && (vid.rotated == 90 || vid.rotated == 270)) { + vid.style.width = Math.ceil(wrw - borderOffset * 2) + "px"; + vid.style.height = Math.ceil(hsw - borderOffset * 2) + "px"; + vid.style.left = 0; + + vid.style.maxWidth = "100vh"; + vid.style.maxHeight = "100vw"; + + if (ChromiumVersion && ChromiumVersion < 77) { + if (!animated && parseInt(container.style.width) > parseInt(holder.style.height)) { + vid.style.position = "relative"; + vid.style.objectFit = "contain"; //contain; + } else if (animated && container.twidth && parseInt(container.twidth) > parseInt(holder.style.height)) { + vid.style.position = "relative"; + vid.style.objectFit = "contain"; //contain; + } + } else { + vid.style.position = "relative"; + } + } else if (session.blurBackground !== false && vid.nodeName == "VIDEO" && !container.blurred && vid.srcObject && ((asw > 1 && ash > 1) || asw >= 1 || ash >= 1)) { + vid.srcObject.getVideoTracks().forEach(trk => { + if (!container.blurred) { + container.blurred = document.createElement("video"); + container.blurred.controls = false; + container.blurred.style = "z-index:-10000;position:absolute;left:0;width:100%;height:100%;top:0;object-fit:fill;-webkit-filter: blur(" + session.blurBackground + "px)"; + container.blurred.srcObject = createMediaStream(); + container.blurred.srcObject.addTrack(trk); + container.blurred.play(); + holder.appendChild(container.blurred); + } else { + if (container.blurred.paused) { + container.blurred.play(); + } + } + }); + } else if (session.blurBackground !== false && vid.nodeName == "VIDEO" && vid.srcObject && ((asw > 1 && ash > 1) || asw >= 1 || ash >= 1)) { + if (container.blurred.paused) { + container.blurred.play(); + } + //container.blurred.style = "z-index:-10000;position:absolute;left:0;width:100%;height:100%;top:0;object-fit:fill;-webkit-filter: blur("+session.blurBackground+"px)"; + } else if (container.blurred) { + try { + container.blurred.remove(); + } catch (e) { } + try { + delete container.blurred; + } catch (e) { } + } + + ////////// NON-COVER VERSION (based on holder) + if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) { + // do not dynamically scale the screen share feed. + } else if (session.dynamicScale) { + if (vid.dataset.UUID) { + let targetWidth = wrw; + let targetHeight = hrh; + targetWidth -= (borderOffset + videoMargin) * 2; + targetHeight -= (borderOffset + videoMargin) * 2; + if (targetWidth < 0) { + targetWidth = 0; + } + if (targetHeight < 0) { + targetHeight = 0; + } + if (session.devicePixelRatio) { + session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only + } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) { + session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover); + } else { + session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover); + } + } + } + } else { + holder.style.left = borderOffset + videoMargin + "px"; + holder.style.top = borderOffset + videoMargin + "px"; + holder.style.height = "calc(100% - " + (borderOffset + videoMargin * 2) + "px)"; + holder.style.width = "calc(100% - " + (borderOffset + videoMargin * 2) + "px)"; + + ////////// UNKNOWN VERSION + if (session.sharperScreen && sssid && vid.dataset.sid && vid.dataset.sid === sssid) { + // do not dynamically scale the screen share feed. + } else if (session.dynamicScale) { + if (vid.dataset.UUID) { + let targetWidth = wrw; + let targetHeight = hrh; + targetWidth -= (borderOffset + videoMargin) * 2; + targetHeight -= (borderOffset + videoMargin) * 2; + if (targetWidth < 0) { + targetWidth = 0; + } + if (targetHeight < 0) { + targetHeight = 0; + } + if (session.devicePixelRatio) { + session.requestResolution(vid.dataset.UUID, targetWidth * session.devicePixelRatio, targetHeight * session.devicePixelRatio, true, false, cover); // snap=true; if resolution close to 100%, send 100%. screenshare only + } else if (window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1) { + session.requestResolution(vid.dataset.UUID, targetWidth * window.devicePixelRatio, targetHeight * window.devicePixelRatio, true, false, cover); + } else { + session.requestResolution(vid.dataset.UUID, targetWidth, targetHeight, true, false, cover); + } + } + } + /////////////// + holder.style.borderColor = borderColor; + holder.style.borderWidth = borderOffset + "px"; + holder.style.borderRadius = borderRadius + "px"; + vid.style.borderWidth = "0px"; + // Apply crop via clip-path + if (cropTop || cropRight || cropBottom || cropLeft) { + vid.style.clipPath = `inset(${cropTop}px ${cropRight}px ${cropBottom}px ${cropLeft}px)`; + } else { + vid.style.clipPath = ""; + } + } + + if (session.colorVideosBackground) { + vid.style.backgroundColor = session.colorVideosBackground; + } else { + vid.style.backgroundColor = "unset"; + } + + if ((vid.dataset.UUID && session.rpcs && session.rpcs[vid.dataset.UUID] && + ( + ("label" in session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].label !== false && session.showlabels === true) || + (session.showmeta === true && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].meta) + )) + || + ((session.showlabels === true || session.showmeta === true) && + (((vid.id === "videosource") && session.label) || vid.labelText || session.meta) + )) { + + if (animated && container.twidth && container.theight) { + var vidwidth = container.twidth; + var vidheight = container.theight; + } else { + var vidwidth = vid.offsetWidth; + var vidheight = vid.offsetHeight; + } + + var fontsize = (vidwidth + vidheight) * 0.03; + if (vidwidth / 16 >= vidheight / 9) { + var voar = vidwidth / 16 / (vidheight / 9); + } else { + var voar = vidheight / 9 / (vidwidth / 16); + } + voar = Math.pow(voar, 0.5); + fontsize = fontsize / voar; + + if (holder.label) { + var label = holder.label; + } else { + var label = document.createElement("span"); + holder.label = label; + if (session.labelstyle) { + label.className = "video-label " + session.labelstyle; + } else { + label.className = "video-label"; + } + holder.appendChild(label); + } + + if (fontsize) { + if (session.labelsize) { + fontsize = (fontsize * session.labelsize) / 100; + } + label.style.fontSize = parseInt(fontsize) + "px"; + } + + if (( + vid.dataset.UUID && + session.rpcs && + session.rpcs[vid.dataset.UUID] && + ( + // Label check + ( + "label" in session.rpcs[vid.dataset.UUID] && + session.rpcs[vid.dataset.UUID].label !== false && + session.showlabels === true + ) + || + // Meta check with explicit undefined checks + ( + session.showmeta === true && + session.rpcs[vid.dataset.UUID] && + session.rpcs[vid.dataset.UUID].meta + ) + ) + ) + || + ( + (session.showlabels === true || session.showmeta === true) && + ( + ((vid.id === "videosource") && session.label) || + vid.labelText || + session.meta + ) + ) + ) { + + let labelContent = []; + let imageElements = []; + + if (session.showlabels === true) { + if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].label) { + session.rpcs[vid.dataset.UUID].label.split("\\n").forEach(label => { + labelContent.push({ type: 'text', value: label }); + }); + } else if ((vid.id === "videosource") && session.label || vid.labelText) { + (vid.labelText || session.label).split("\\n").forEach(label => { + labelContent.push({ type: 'text', value: label }); + }); + } + } + + if (session.showmeta === true) { + let metaData = []; + if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID] && session.rpcs[vid.dataset.UUID].meta) { + metaData = Object.entries(session.rpcs[vid.dataset.UUID].meta); + } else if (session.meta) { + metaData = Object.entries(session.meta); + } + metaData.forEach(([key, value]) => { + if (value && typeof value === 'object') { + if (value.type && value.type == 'file') { + // console.log(value); + if (value.filetype) { + imageElements.push(value); + } + } else { + labelContent.push({ + type: 'text', + value: value.value || JSON.stringify(value) + }); + } + } else if (value) { + labelContent.push({ type: 'text', value: value }); + } + }); + } + + // Create or get label container + if (!holder.labelContainer) { + holder.labelContainer = document.createElement("div"); + holder.labelContainer.className = "video-label-container"; + holder.appendChild(holder.labelContainer); + } + + // Handle text content + if (labelContent.length > 0) { + if (!holder.label) { + holder.label = document.createElement("div"); + if (session.labelstyle) { + holder.label.className = "video-label " + session.labelstyle; + } else { + holder.label.className = "video-label"; + } + holder.labelContainer.appendChild(holder.label); + } + // Apply font size adjustments as before + if (fontsize) { + if (session.labelsize) { + fontsize = (fontsize * session.labelsize) / 100; + } + holder.label.style.fontSize = parseInt(fontsize) + "px"; + } + holder.label.innerHTML = labelContent + .map(item => `${escapeHtml(String(item.value))}`) + .join(""); + } + + // Handle image content + if (imageElements.length > 0) { + if (!holder.imageContainer) { + holder.imageContainer = document.createElement("div"); + holder.imageContainer.className = "video-image-container"; + holder.imageContainer.style.position = "absolute"; + holder.imageContainer.style.top = "10px"; + holder.imageContainer.style.right = "10px"; + holder.imageContainer.style.display = "flex"; + holder.imageContainer.style.alignItems = "flex-start"; + holder.imageContainer.style.justifyContent = "flex-end"; + holder.appendChild(holder.imageContainer); + } + holder.imageContainer.style.maxWidth = "min(20vw, calc(" + holder.style.width + " * 0.25))"; + holder.imageContainer.style.maxHeight = "min(20vh, calc(" + holder.style.height + " * 0.25))"; + holder.imageContainer.style.minWidth = "max(7vw, calc(" + holder.style.width + " * 0.22), 30px)"; + holder.imageContainer.style.minHeight = "max(7vw, calc(" + holder.style.height + " * 0.22), 30px)"; + + // Clear existing images + holder.imageContainer.innerHTML = ''; + // Add new images + imageElements.forEach(meta => { + if (meta.filetype.startsWith("image/")) { + const imgElement = document.createElement("img"); + imgElement.src = meta.value; + imgElement.className = `meta-image ${meta.templateName || ''}`; + imgElement.style.width = "auto"; // Fill container width + imgElement.style.height = "auto"; // Fill container height + imgElement.style.objectFit = "contain"; // Maintain aspect ratio + imgElement.style.position = "absolute"; + imgElement.style.maxWidth = "100%"; + imgElement.style.maxHeight = "100%"; + holder.imageContainer.appendChild(imgElement); + } + }); + } + } + } else if (holder.label) { + holder.label.remove(); + delete holder.label; + } + + if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID]) { + if (session.rpcs[vid.dataset.UUID].voiceMeter) { + holder.appendChild(session.rpcs[vid.dataset.UUID].voiceMeter); + } + if (session.rpcs[vid.dataset.UUID].remoteMuteElement) { + holder.appendChild(session.rpcs[vid.dataset.UUID].remoteMuteElement); + } + + if (session.signalMeter) { + if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].signalMeter) { + session.rpcs[vid.dataset.UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true); + session.rpcs[vid.dataset.UUID].signalMeter.classList.remove("hidden"); + session.rpcs[vid.dataset.UUID].signalMeter.id = "signalMeter_" + vid.dataset.UUID; + session.rpcs[vid.dataset.UUID].signalMeter.dataset.level = 0; + session.rpcs[vid.dataset.UUID].signalMeter.title = getTranslation("signal-meter"); + holder.appendChild(session.rpcs[vid.dataset.UUID].signalMeter); + holder.signalMeter = session.rpcs[vid.dataset.UUID].signalMeter; + } else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].signalMeter) { + if (!holder.signalMeter) { + holder.appendChild(session.rpcs[vid.dataset.UUID].signalMeter); + holder.signalMeter = session.rpcs[vid.dataset.UUID].signalMeter; + } + } + } + + if (session.batteryMeter) { + if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].batteryMeter) { + session.rpcs[vid.dataset.UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true); + session.rpcs[vid.dataset.UUID].batteryMeter.classList.remove("hidden"); + session.rpcs[vid.dataset.UUID].batteryMeter.id = "batteryMeter_" + vid.dataset.UUID; + session.rpcs[vid.dataset.UUID].batteryMeter.dataset.level = 0; + session.rpcs[vid.dataset.UUID].batteryMeter.title = getTranslation("battery-meter"); + holder.appendChild(session.rpcs[vid.dataset.UUID].batteryMeter); + holder.batteryMeter = session.rpcs[vid.dataset.UUID].batteryMeter; + } else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].batteryMeter) { + if (!holder.batteryMeter) { + holder.appendChild(session.rpcs[vid.dataset.UUID].batteryMeter); + holder.batteryMeter = session.rpcs[vid.dataset.UUID].batteryMeter; + } + } + } + + if (session.volumeControl && session.rpcs[vid.dataset.UUID].videoElement && vid.tagName != "VIDEO") { + if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].volumeControl) { + session.rpcs[vid.dataset.UUID].volumeControl = getById("volumeControlTemplate").cloneNode(true); + session.rpcs[vid.dataset.UUID].volumeControl.classList.remove("hidden"); + session.rpcs[vid.dataset.UUID].volumeControl.id = "volumeControl_" + vid.dataset.UUID; + session.rpcs[vid.dataset.UUID].volumeControl.value = parseInt(session.rpcs[vid.dataset.UUID].videoElement.volume * 100); + session.rpcs[vid.dataset.UUID].volumeControl.dataset.UUID = vid.dataset.UUID; + session.rpcs[vid.dataset.UUID].volumeControl.title = getTranslation("volume-control"); + session.rpcs[vid.dataset.UUID].volumeControl.oninput = function () { + if (this.dataset.UUID && session.rpcs[this.dataset.UUID] && session.rpcs[this.dataset.UUID].videoElement) { + session.rpcs[this.dataset.UUID].videoElement.volume = parseFloat(this.value / 100); + } + }; + holder.appendChild(session.rpcs[vid.dataset.UUID].volumeControl); + holder.volumeControl = session.rpcs[vid.dataset.UUID].volumeControl; + } else if (vid.dataset.UUID && session.rpcs[vid.dataset.UUID].volumeControl) { + if (!holder.volumeControl && session.rpcs[vid.dataset.UUID].videoElement) { + holder.appendChild(session.rpcs[vid.dataset.UUID].volumeControl); + holder.volumeControl = session.rpcs[vid.dataset.UUID].volumeControl; + session.rpcs[vid.dataset.UUID].volumeControl.value = parseInt(session.rpcs[vid.dataset.UUID].videoElement.volume * 100); + } + } + } + + if (session.showConnections) { + if (!session.rpcs[vid.dataset.UUID].connectionDetails) { + createConnectionDetailsEle(vid.dataset.UUID); + } + holder.appendChild(session.rpcs[vid.dataset.UUID].connectionDetails); + } + } + + if (session.ruleOfThirds) { + if (vid.id == "videosource") { + if (!holder.svg) { + var svg = document.createElement("img"); + svg.src = session.ruleOfThirds; + svg.style.width = "100%"; + svg.style.height = "100%"; + svg.style.position = "absolute"; + svg.style.left = "0"; + svg.style.top = "0"; + svg.style.pointerEvents = "none"; + holder.svg = svg; + holder.appendChild(svg); + } + } + } + + try { + if (!(session.cleanOutput && session.cleanish == false)) { + if (session.firstPlayTriggered === false) { + // don't play unless needed; might cause clicking or who knows what else. + if (vid.tagName.toLowerCase() == "video") { + // we don't want to try playing an Iframe or Canvas. + if (vid.paused) { + warnlog("VIDEO IS NOT PLAYING"); + var playPromise = vid.play(); + if (playPromise !== undefined) { + playPromise + .then(_ => { + // playing + //session.firstPlayTriggered=true; // global tracking. "user gesture obtained", so no longer needed if playing. + }) + .catch(err => { + var bigPlayButton = document.getElementById("bigPlayButton"); + if (bigPlayButton) { + bigPlayButton.innerHTML = ''; + bigPlayButton.style.display = "block"; + } + }); + } else { + session.firstPlayTriggered = true; // well, I don't know if it's playing, and so whatever. fail gracefully. + } + } + } + } + } + } catch (e) { + errorlog(e); + var bigPlayButton = document.getElementById("bigPlayButton"); + if (bigPlayButton) { + bigPlayButton.parentNode.removeChild(bigPlayButton); + } + } + + if (vid.nodeName == "IFRAME") { + // I need to add this back in at some point. + i += 1; + return; + } + + if (!session.cleanOutput && !session.nocursor && !session.nofullwindowbutton) { + if (session.roomid !== false && session.scene === false) { + if (!(vid.id === "videosource" && miniPreview) && !session.infocusForceMode) { + if (!holder.button) { + var button = document.createElement("div"); + holder.button = button; + holder.appendChild(button); + } else { + var button = holder.button; + } + button.id = "button_" + vid.id; + button.dataset.button = true; + if (soloVideo) { + button.innerHTML = ""; + button.title = "Show all active videos togethers"; + button.style.visibility = "visible"; + } else if (mpl > 1 || session.fullscreenButton) { + // with session.fullscreenButton we hide the actuall full screen button, so this replaces it + button.innerHTML = ""; + button.title = "Enlarge video and increase its clarity"; + button.style.visibility = "visible"; + } else { + button.style.visibility = "hidden"; + } + button.classList.add("fullwindowButton"); + if (vid.id == "videosource") { + button.onclick = function (event) { + if (session.infocus === true) { + session.infocus = false; + } else { + session.infocus = true; + } + setTimeout(() => updateMixer(), 10); + if (session.fullscreenButton) { + if (session.infocus) { + fullscreenPageToggle(true); + } else { + fullscreenPageToggle(false); + } + } + }; + } else { + button.dataset.UUID = vid.dataset.UUID; + button.onclick = function (event) { + var target = event.currentTarget; + if (session.infocus === target.dataset.UUID) { + //target.childNodes[0].className = 'las la-arrows-alt'; + session.infocus = false; + } else { + //target.childNodes[0].className = 'las la-compress'; + session.infocus = target.dataset.UUID; + //log("session:"+target.dataset.UUID); + } + if (session.fullscreenButton) { + if (session.infocus) { + fullscreenPageToggle(true); + } else { + fullscreenPageToggle(false); + } + } + setTimeout(() => updateMixer(), 10); + }; + } + vid.onclick = function (event) { + if (session.disableMouseEvents) { + return; + } + button.style.display = "block"; + container.style.backgroundColor = "#4444"; + button.style.opacity = "100%"; + }; + button.onmouseenter = function (event) { + if (session.disableMouseEvents) { + return; + } + button.style.display = "block"; + container.style.backgroundColor = "#4444"; + setTimeout( + function (button) { + button.style.opacity = "100%"; + }, + 1, + button + ); + }; + button.onmousemove = function (event) { + if (session.disableMouseEvents) { + return; + } + button.style.display = "block"; + container.style.backgroundColor = "#4444"; + button.style.opacity = "100%"; + }; + container.onmousemove = function (event) { + if (session.disableMouseEvents) { + return; + } + button.style.display = "block"; + container.style.backgroundColor = "#4444"; + button.style.opacity = "100%"; + }; + container.onmouseenter = function (event) { + if (session.disableMouseEvents) { + return; + } + button.style.display = "block"; + container.style.backgroundColor = "#4444"; + setTimeout( + function (button) { + button.style.opacity = "100%"; + }, + 1, + button + ); + }; + container.onmouseleave = function (event) { + if (session.disableMouseEvents) { + return; + } + button.style.display = "none"; + container.style.backgroundColor = null; + button.style.opacity = "10%"; + }; + } else if (vid.id === "videosource" && miniPreview && soloVideo == true && !session.infocusForceMode) { + if (!holder.button) { + var button = document.createElement("div"); + holder.button = button; + holder.appendChild(button); + } else { + var button = holder.button; + } + button.id = "button_videosource"; + button.dataset.button = true; + if (soloVideo) { + button.innerHTML = ""; + button.title = "Show all active videos togethers"; + button.style.display = "unset"; + } else { + button.style.visibility = "hidden"; + button.style.display = "none"; + } + button.classList.add("fullwindowButton"); + button.onclick = function (event) { + event.stopPropagation(); + event.preventDefault(); + if (session.infocus === true) { + session.infocus = false; + setTimeout(() => updateMixer(), 10); + } + if (session.fullscreenButton) { + if (session.infocus) { + fullscreenPageToggle(true); + } else { + fullscreenPageToggle(false); + } + } + }; + } else if (session.infocusForceMode && holder.button) { + try { + holder.button.remove(); + } catch (e) { + errorlog(e); + } + } + } + } + i += 1; + } catch (err) { + errorlog(err); + } + }); + for (var uid in delayedRequestList) { + session.requestRateLimit(...delayedRequestList[uid]); + } + updateUserList(); +} + +var translationBacklog = []; + +function miniTranslate(ele, ident = false, direct = false) { + if (!translation) { + translation = {}; + } + + if (ident) { + if (translation.innerHTML && ident in translation.innerHTML) { + if (ele.querySelector("[data-translate]")) { + ele.querySelector("[data-translate]").innerHTML = translation.innerHTML[ident]; + ele.querySelector("[data-translate]").dataset.translate = ident; + } else { + ele.innerHTML = translation.innerHTML[ident]; + ele.dataset.translate = ident; + } + return; + } else if (direct) { + if (ele.querySelector("[data-translate]")) { + ele.querySelector("[data-translate]").innerHTML = direct; + ele.querySelector("[data-translate]").dataset.translate = ident; + } else { + ele.dataset.translate = ident; + ele.innerHTML = direct; + } + return; + } else { + if (!(ident in miscTranslations)) { + var value = ident.replaceAll("-", " "); // lets use the key as the translation + } else { + var value = miscTranslations[ident]; // lets use a miscellaneous translation as backup? + } + + if (ele.querySelector("[data-translate]")) { + ele.querySelector("[data-translate]").innerHTML = value; + ele.querySelector("[data-translate]").dataset.translate = ident; + } else { + ele.innerHTML = value; + ele.dataset.translate = ident; + } + return; + } + } + + var allItems = ele.querySelectorAll("[data-translate]"); + allItems.forEach(function (ele2) { + if (translation.innerHTML && ele2.dataset.translate in translation.innerHTML) { + ele2.innerHTML = translation.innerHTML[ele2.dataset.translate]; + } else if (translation.miscellaneous && ele2.dataset.translate in translation.miscellaneous) { + ele2.innerHTML = translation.miscellaneous[ele2.dataset.translate]; + } + }); + if (ele.dataset) { + if (translation.innerHTML && ele.dataset.translate in translation.innerHTML) { + ele.innerHTML = translation.innerHTML[ele.dataset.translate]; + } else if (translation.miscellaneous && ele.dataset.translate in translation.miscellaneous) { + ele.innerHTML = translation.miscellaneous[ele.dataset.translate]; + } + } + if (translation.titles) { + var allTitles = ele.querySelectorAll("[title]"); + allTitles.forEach(function (ele2) { + if (ele.dataset.key) { + var key = ele2.dataset.key; + } else { + var key = ele2.title.replace(/[\W]+/g, "-").toLowerCase(); + ele2.dataset.key = key; + } + + if (key in translation.titles) { + ele2.title = translation.titles[key]; + } else if (ele2.dataset.translate && ele2.dataset.translate in translation.titles) { + ele2.title = translation.titles[ele2.dataset.translate]; + } + }); + if (ele.title) { + if (ele.dataset.key) { + var key = ele.dataset.key; + } else { + var key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); + ele.dataset.key = key; + } + + if (key in translation.titles) { + ele.title = translation.titles[key]; + } else if (ele.dataset.translate && ele.dataset.translate in translation.titles) { + ele.title = translation.titles[ele.dataset.translate]; + } + } + } + if (translation.placeholders) { + var allPlaceholders = ele.querySelectorAll("[placeholder]"); + allPlaceholders.forEach(function (ele2) { + var key = ele2.placeholder.replace(/[\W]+/g, "-").toLowerCase(); + if (key in translation.placeholders) { + ele2.placeholder = translation.placeholders[key]; + } + }); + + if (ele.placeholder) { + var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); + if (key in translation.placeholders) { + ele.placeholder = translation.placeholders[key]; + } + } + } +} + +async function changeLg(lang, rtl = false, save = false) { + log("changeLg: " + lang); + var retry = false; + if (lang == "auto") { + const navlang = navigator.language || navigator.userLanguage || ""; + const userLang = navlang.toLowerCase(); // Convert to lower case + lang = userLang.replace("_", "-"); // Replace dash with underscore if present + retry = true; + } + + await fetchWithTimeout("./translations/" + lang + ".json", 2000) + .then(async response => { + try { + if (response.status !== 200) { + getById("mainmenu").style.opacity = 1; + if (retry) { + console.warn("Couldn't find the exact language file for '" + lang + "'; trying a more generic option instead"); + lang = lang.split("-")[0]; + if (lang && lang !== "auto") { + await changeLg(lang, rtl); // Retry with a more generic language code. + } + } + return; + } + await response + .json() + .then(async function (data) { + translation = data; // translation.innerHTML[ele.dataset.translate] + document.documentElement.dir = rtl ? "rtl" : "ltr"; + document.documentElement.setAttribute('lang', lang); + document.body.classList.remove('rtl'); + document.body.classList.remove('ltr'); + document.body.classList.add(rtl ? 'rtl' : 'ltr'); + document.documentElement.style.setProperty('--rtl-or-ltr', rtl ? 'right' : 'left'); + if (save) { + try { + localStorage.setItem("vdo_ninja_language", lang); + } catch (e) { + console.warn("Could not save language to localStorage", e); + } + } + if (translation.miscellaneous) { + Object.keys(translation.miscellaneous).forEach(key => { + miscTranslations[key] = translation.miscellaneous[key]; + }); + } + translation.miscellaneous = miscTranslations; + var allItems = document.querySelectorAll("[data-translate]"); + allItems.forEach(function (ele) { + if (ele.dataset.translate in translation.innerHTML) { + ele.innerHTML = translation.innerHTML[ele.dataset.translate]; + } else if (translation.miscellaneous && ele.dataset.translate in translation.miscellaneous) { + ele.innerHTML = translation.miscellaneous[ele.dataset.translate]; // use the misc translation if no main one is found + } + }); + var allTitles = document.querySelectorAll("[title]"); + allTitles.forEach(function (ele) { + if (ele.dataset.key) { + var key = ele.dataset.key; + } else { + var key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); + ele.dataset.key = key; + } + if (key in translation.titles) { + ele.title = translation.titles[key]; + } else if (ele.dataset.translate && translation.titles && ele.dataset.translate in translation.titles) { + ele.title = translation.titles[ele.dataset.translate]; + } + }); + var allPlaceholders = document.querySelectorAll("[placeholder]"); + allPlaceholders.forEach(function (ele) { + var key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); + if (key in translation.placeholders) { + ele.placeholder = translation.placeholders[key]; + } + }); + if (translationBacklog.length) { + for (var i = 0; i < translationBacklog.length; i++) { + try { + miniTranslate(translationBacklog[i][0], translationBacklog[i][1]); + } catch (e) { } + } + translationBacklog = []; + } + getById("mainmenu").style.opacity = 1; + }) + .catch(async err2 => { + if (retry) { + console.warn("Couldn't find the exact language file for '" + lang + "'; trying a more generic option instead"); + lang = lang.split("-")[0]; + if (lang && lang !== "auto") { + await changeLg(lang, rtl); // won't retry I'd hope. + } + } else { + errorlog(err2); + } + }); + } catch (e) { + getById("mainmenu").style.opacity = 1; + } + }) + .catch(async err => { + if (retry) { + console.warn("Couldn't find exact language; trying a more generic option instead"); + lang = lang.split("-")[0]; + if (lang && lang !== "auto") { + await changeLg(lang, rtl); // won't retry I'd hope. + } + } else { + errorlog(err); + } + }); +} + +var controlBarTimeout = false; +function showControl(e) { + if (controlBarTimeout) { + clearTimeout(controlBarTimeout); + } + if (session.mobile) { + getById("controlButtons").classList.remove("partialFadeout"); + } else { + getById("controlButtons").classList.remove("fadeout"); + } + controlBarTimeout = setTimeout(function () { + if (session.mobile) { + getById("controlButtons").classList.add("partialFadeout"); + } else { + getById("controlButtons").classList.add("fadeout"); + } + }, 5000); +} + +var loadedQRCode = false; +function loadQR(callback = false, value = false) { + if (loadedQRCode === false) { + loadedQRCode = true; + var script = document.createElement("script"); + if (callback) { + script.onload = function () { + callback(value); + }; + } + script.src = "./thirdparty/qrcode.min.js"; // dynamically load this only if its needed. Keeps loading time down. + document.head.appendChild(script); + } else if (callback) { + callback(value); + } +} + +function showInviteQR() { + var inviteURL = getById("inviteLinkURL").href; + warnUser("Loading QR Code..."); + loadQR(function(url) { + getById("alertModalMessage").innerHTML = ""; + var qrcode = new QRCode(getById("alertModalMessage"), { + width: 300, + height: 300, + colorDark: "#000000", + colorLight: "#FFFFFF", + useSVG: false + }); + qrcode.makeCode(url); + getById("alertModalMessage").title = ""; + setTimeout(function() { + getById("alertModalMessage").title = ""; + if (getById("alertModalMessage").getElementsByTagName("img").length) { + getById("alertModalMessage").getElementsByTagName("img")[0].style.cursor = "none"; + } + }, 100); + }, inviteURL); +} + +if (typeof session.pendingFramegrabAudioSettings === "undefined") { + session.pendingFramegrabAudioSettings = null; +} +if (typeof session.framegrabAudioPending === "undefined") { + session.framegrabAudioPending = false; +} +if (typeof session.framegrabAudioRetryTimer === "undefined") { + session.framegrabAudioRetryTimer = null; +} +if (typeof session.framegrabAudioRetryAttempts === "undefined") { + session.framegrabAudioRetryAttempts = 0; +} +if (typeof session.pendingMicRefreshTimeout === "undefined") { + session.pendingMicRefreshTimeout = null; +} + +session.updateFramegrabAudioUI = function (enable) { + try { + const controls = getById("controlButtons"); + const micButton = getById("mutebutton"); + const speakerButton = getById("mutespeakerbutton"); + if (enable) { + if (controls) { + controls.classList.remove("hidden"); + } + if (micButton) { + micButton.classList.remove("hidden"); + micButton.style.removeProperty("display"); + } + if (speakerButton) { + speakerButton.classList.remove("hidden"); + speakerButton.style.removeProperty("display"); + } + } else if (speakerButton) { + speakerButton.classList.add("hidden"); + } + } catch (err) { + errorlog(err); + } +}; + +session.clearFramegrabAudioTracks = function (stopTracks = true) { + const removeTracks = stream => { + if (!stream) { + return; + } + try { + stream.getAudioTracks().forEach(track => { + try { + stream.removeTrack(track); + } catch (err) { } + if (stopTracks) { + try { + track.stop(); + } catch (stopErr) { } + } + }); + } catch (err) { } + }; + removeTracks(session.streamSrc); + removeTracks(session.streamSrcClone); + if (session.videoElement && session.videoElement.srcObject) { + removeTracks(session.videoElement.srcObject); + } + session.framegrabAudioInitialized = false; + session.framegrabAudioAutoSelectionApplied = false; + session.framegrabAudioEnabling = false; + session.framegrabAudioPending = false; + session.framegrabAudioRetryAttempts = 0; + if (session.framegrabAudioRetryTimer) { + clearTimeout(session.framegrabAudioRetryTimer); + session.framegrabAudioRetryTimer = null; + } +}; + +session.prepareFramegrabAudioPreference = function (settings = {}) { + if (!settings) { + return; + } + if (Object.prototype.hasOwnProperty.call(settings, "deviceId")) { + const deviceId = settings.deviceId; + if (deviceId === null || deviceId === false || deviceId === "") { + session.audioDevice = 0; + } else if (deviceId === 1 || deviceId === "1" || deviceId === "default") { + session.audioDevice = 1; + } else if (deviceId === "communications") { + session.audioDevice = "communications"; + } else if (Array.isArray(deviceId)) { + session.audioDevice = deviceId.filter(Boolean); + } else { + session.audioDevice = String(deviceId); + } + } else if (!session.audioDevice || session.audioDevice === 0) { + session.audioDevice = 1; + } +}; + +session.autoSelectFramegrabAudioDevice = function () { + if (!session.framegrabAudio) { + return false; + } + const audioMenu = getById("audioSource3"); + if (!audioMenu) { + return false; + } + const inputs = Array.from(audioMenu.querySelectorAll("input[type='checkbox']")); + if (!inputs.length) { + return false; + } + const hasActiveMic = inputs.some(input => input.id !== "multiselect1" && input.checked); + if (hasActiveMic) { + session.framegrabAudioAutoSelectionApplied = true; + return true; + } + const noAudioOption = inputs.find(input => input.id === "multiselect1"); + const findByValue = value => inputs.find(input => input.value === value); + let candidate = null; + const preferList = []; + if (Array.isArray(session.audioDevice) && session.audioDevice.length) { + preferList.push(...session.audioDevice); + } else if (session.audioDevice === 0) { + return false; + } else if (session.audioDevice === 1 || session.audioDevice === "1") { + preferList.push("default", "communications"); + } else if (session.audioDevice) { + preferList.push(session.audioDevice); + } + for (let i = 0; i < preferList.length; i++) { + const value = preferList[i]; + if (typeof value !== "string") { + continue; + } + const directMatch = findByValue(value); + if (directMatch) { + candidate = directMatch; + break; + } + const desired = normalizeDeviceLabel(value); + const labelMatch = inputs.find(input => { + const label = input.dataset && input.dataset.label ? input.dataset.label : (input.getAttribute ? input.getAttribute("data-label") : ""); + return label && normalizeDeviceLabel(label) === desired; + }); + if (labelMatch) { + candidate = labelMatch; + break; + } + } + if (!candidate) { + candidate = findByValue("default") || findByValue("communications"); + } + if (!candidate) { + candidate = inputs.find(input => input.id !== "multiselect1"); + } + if (!candidate) { + return false; + } + try { + candidate.checked = true; + candidate.dispatchEvent(new Event("change", { bubbles: true })); + } catch (err) { } + if (typeof SelectedAudioInputDevices !== "undefined") { + if (!Array.isArray(SelectedAudioInputDevices)) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices = SelectedAudioInputDevices.filter(value => value && value !== "ZZZ"); + if (!SelectedAudioInputDevices.includes(candidate.value)) { + SelectedAudioInputDevices.push(candidate.value); + } + } + if (noAudioOption) { + noAudioOption.checked = false; + } + session.framegrabAudioAutoSelectionApplied = true; + return true; +}; + +session.applyFramegrabAudioSettings = async function (settings = {}) { + const enable = !(settings && settings.enable === false); + const clearRetry = () => { + if (session.framegrabAudioRetryTimer) { + clearTimeout(session.framegrabAudioRetryTimer); + session.framegrabAudioRetryTimer = null; + } + session.framegrabAudioRetryAttempts = 0; + }; + const scheduleRetry = () => { + if (!session.framegrabAudio || !session.framegrabAudioRequested) { + return; + } + session.pendingFramegrabAudioSettings = settings || { enable: true }; + session.framegrabAudioPending = true; + const MAX_RETRIES = 6; + const RETRY_DELAY_MS = 600; + if (session.framegrabAudioRetryAttempts >= MAX_RETRIES) { + log('[FRAMEGRAB AUDIO] Retry limit reached; disabling audio'); + session.framegrabAudio = false; + session.framegrabAudioRequested = false; + session.pendingFramegrabAudioSettings = null; + session.updateFramegrabAudioUI(false); + session.clearFramegrabAudioTracks(); + session.framegrabAudioPending = false; + if (!session.cleanOutput) { + warnUser('Unable to attach an audio input for the framegrab. Please confirm microphone access and try again.', 6000); + } + return; + } + session.framegrabAudioRetryAttempts += 1; + if (session.framegrabAudioRetryTimer) { + return; + } + session.framegrabAudioRetryTimer = setTimeout(() => { + session.framegrabAudioRetryTimer = null; + if (!session.framegrabAudio || !session.framegrabAudioRequested) { + return; + } + session.applyFramegrabAudioSettings(session.pendingFramegrabAudioSettings || { enable: true }).catch(errorlog); + }, RETRY_DELAY_MS); + }; + if (!enable) { + session.pendingFramegrabAudioSettings = null; + session.framegrabAudio = false; + session.framegrabAudioRequested = false; + session.framegrabAudioPending = false; + session.updateFramegrabAudioUI(false); + clearRetry(); + session.clearFramegrabAudioTracks(); + return; + } + session.framegrabAudio = true; + session.framegrabAudioRequested = true; + session.framegrabAudioPending = false; + session.pendingFramegrabAudioSettings = settings || { enable: true }; + session.updateFramegrabAudioUI(true); + session.framegrabAudioAutoSelectionApplied = false; + session.prepareFramegrabAudioPreference(settings || {}); + const overrideConstraints = (() => { + if (!settings || typeof settings !== "object") { + return false; + } + if (!Object.prototype.hasOwnProperty.call(settings, "deviceId")) { + return false; + } + let desiredId = settings.deviceId; + if (Array.isArray(desiredId)) { + desiredId = desiredId.find(Boolean) || null; + } + if ( + desiredId === null || + desiredId === false || + desiredId === "" || + desiredId === 0 || + desiredId === "0" || + desiredId === 1 || + desiredId === "1" || + desiredId === "default" || + desiredId === "communications" + ) { + return false; + } + desiredId = String(desiredId); + return { audio: { deviceId: desiredId } }; + })(); + if (!session.streamSrc) { + session.framegrabAudioPending = true; + return; + } + let deviceInfos = null; + try { + deviceInfos = await enumerateDevices(); + gotDevices(deviceInfos); + } catch (err) { + errorlog(err); + } + const audioInputsAvailable = Array.isArray(deviceInfos) && deviceInfos.some(info => info && info.kind === 'audioinput'); + let autoSelectOk = false; + try { + autoSelectOk = session.autoSelectFramegrabAudioDevice() === true; + } catch (err) { + errorlog(err); + } + if (!autoSelectOk) { + if (!audioInputsAvailable) { + log('[FRAMEGRAB AUDIO] No audio inputs detected; retrying'); + } + scheduleRetry(); + return; + } + try { + await grabAudio("#audioSource3", null, overrideConstraints); + session.framegrabAudioInitialized = true; + const trackCount = session.streamSrc && typeof session.streamSrc.getAudioTracks === "function" + ? session.streamSrc.getAudioTracks().length + : 0; + if (!trackCount) { + session.framegrabAudioPending = true; + scheduleRetry(); + return; + } + session.framegrabAudioPending = false; + if (trackCount) { + try { + session.seedStream(); + } catch (err) { + errorlog(err); + } + } + clearRetry(); + session.pendingFramegrabAudioSettings = null; + } catch (err) { + errorlog(err); + scheduleRetry(); + } +}; + +session.startFramegrabAudio = async function (overrideSettings = null) { + if (!session.framegrab) { + return false; + } + const pendingSettings = overrideSettings || session.pendingFramegrabAudioSettings || { enable: true }; + if (pendingSettings && pendingSettings.enable === false) { + return session.applyFramegrabAudioSettings(pendingSettings); + } + session.framegrabAudioRequested = true; + session.framegrabAudio = true; + const settings = Object.assign({ enable: true }, pendingSettings || {}); + session.pendingFramegrabAudioSettings = settings; + session.updateFramegrabAudioUI(true); + if (!session.streamSrc) { + session.framegrabAudioPending = true; + return true; + } + if (session.framegrabAudioEnabling) { + return session.streamSrc.getAudioTracks && session.streamSrc.getAudioTracks().length > 0; + } + session.framegrabAudioEnabling = true; + activatedPreview = false; + let success = false; + try { + await session.applyFramegrabAudioSettings(settings); + const hasAudio = session.streamSrc && session.streamSrc.getAudioTracks && session.streamSrc.getAudioTracks().length > 0; + if (hasAudio) { + session.muted = false; + const muteToggle = getById("mutetoggle"); + if (muteToggle) { + muteToggle.className = "las la-microphone toggleSize"; + } + const muteButton = getById("mutebutton"); + if (muteButton) { + muteButton.classList.remove("red", "pulsate"); + muteButton.ariaPressed = "false"; + } + if (!session.cleanOutput) { + try { + getById("header").classList.remove("red"); + } catch (err) { } + } + success = true; + } + } catch (err) { + errorlog(err); + warnUser("Unable to access the microphone. Please check browser permissions.", 6000); + session.framegrabAudio = false; + session.framegrabAudioRequested = false; + session.updateFramegrabAudioUI(false); + } finally { + session.framegrabAudioEnabling = false; + activatedPreview = false; + } + const result = success || session.framegrabAudioPending; + if (!result) { + session.framegrabAudio = false; + session.framegrabAudioRequested = false; + session.updateFramegrabAudioUI(false); + } + return result; +}; + +var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; +var eventer = window[eventMethod]; +var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message"; +eventer(messageEvent, function (e) { + // this listens for child IFRAMES. + try { + if (e.origin == "https://www.youtube.com") { + processYoutubeEvent(e); + } else if (e.data && typeof e.data == "object" && "action" in e.data) { + if (e.data.action == "screen-share-state" && !e.data.value) { + //pokeIframeAPI("screen-share-state", false); + if (session.screenShareElement && session.screenShareElement.contentWindow) { + if (e.source == session.screenShareElement.contentWindow) { + // reject messages send from other iframes + warnlog(e); + postMessageIframe(session.screenShareElement, { close: true }); + session.screenShareElement.parentNode.removeChild(session.screenShareElement); + session.screenShareElement = false; + updateMixer(); + } + } + } else if (e.data.action === "framegrab-audio-settings") { + if (!session.framegrab || typeof session.startFramegrabAudio !== "function" || typeof session.applyFramegrabAudioSettings !== "function") { + return; + } + const enable = !(typeof e.data.enable !== "undefined" && e.data.enable === false); + const payload = { enable }; + if (enable && typeof e.data.deviceId !== "undefined" && e.data.deviceId !== null) { + payload.deviceId = e.data.deviceId; + } + try { + const handler = enable ? session.startFramegrabAudio : session.applyFramegrabAudioSettings; + const maybePromise = handler(payload); + if (maybePromise && typeof maybePromise.then === "function") { + maybePromise.catch(errorlog); + } + } catch (err) { + errorlog(err); + } + } else if (e.data.action == "video-loaded") { + // TODO: if (e.source == session...iframeEle.contentWindow) { + warnlog(e); + toggleSpeakerMute(true); + updateMixer(); // harmless to let run. (not so harmless if updateMixer reloads meshcast actually) TODO; Do I need this? + } + } + } catch (e) { + errorlog(e); + } +}); + +function requestRotateGuest(ele) { + var UUID = ele.dataset.UUID; + var data = {}; + //data.mirrorGuestTarget = UUID; + //session.sendPeers(data, false, UUID); + + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + + data.rotate = true; + session.sendRequest(data, UUID); + + setTimeout( + function (el) { + el.value = 0; + el.classList.remove("pressed"); + el.ariaPressed = "false"; + }, + 500, + ele + ); +} + +function requestMirrorGuest(ele) { + var UUID = ele.dataset.UUID; + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + applyMirrorGuest(false, session.rpcs[UUID].videoElement, session.rpcs[UUID].flipState); + var data = {}; + data.mirrorGuestState = false; + + data.mirrorGuestTarget = UUID; + session.sendPeers(data, false, UUID); + + data.mirrorGuestTarget = true; + session.sendPeers(data, UUID); + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + applyMirrorGuest(true, session.rpcs[UUID].videoElement, session.rpcs[UUID].flipState); + var data = {}; + data.mirrorGuestState = true; + + data.mirrorGuestTarget = UUID; + session.sendPeers(data, false, UUID); + + data.mirrorGuestTarget = true; + session.sendPeers(data, UUID); + } +} + +function requestKeyframeScene(ele) { + var UUID = ele.dataset.UUID; + if (ele.value == 1) { + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + session.requestKeyframe(UUID, true); + setTimeout( + function (el) { + el.value = 0; + el.classList.remove("pressed"); + el.ariaPressed = "false"; + }, + 1000, + ele + ); + } +} + +function pokeIframeAPI(action, value = null, UUID = null, SID = null, CID = null) { + if (!isIFrame) { + return; + } + try { + var data = {}; + + data.action = action; + + if (value !== null) { + data.value = value; + } + if (UUID !== null) { + data.UUID = UUID; + } + if (!SID) { + if (UUID && UUID in session.rpcs) { + if (session.rpcs[UUID].streamID) { + SID = session.rpcs[UUID].streamID; + } + } + } + if (!SID) { + if (UUID && UUID in session.pcs) { + if (session.pcs[UUID].streamID) { + SID = session.pcs[UUID].streamID; + } + } + } + + if (SID) { + data.streamID = SID; + } + + if (CID) { + data.cib = CID; + } + + if (isIFrame) { + parent.postMessage(data, session.iframetarget); + } + } catch (e) { + errorlog(e); + } +} + +async function jumptoroom2() { + var arr = window.location.href.split("?"); + + var roomname = getById("videoname1").value; + roomname = sanitizeRoomName(roomname); + if (roomname.length) { + var pass = getById("passwordRoom").value; + pass = sanitizePassword(pass); + + var passStr = ""; + if (pass && pass.length) { + passStr = "&password=" + pass; + } + + if (arr.length > 1 && arr[1] !== "") { + window.location += "&room=" + roomname + passStr + "&host"; + } else { + window.location += "?room=" + roomname + passStr + "&host"; + } + } else { + getById("videoname1").focus(); + getById("videoname1").classList.remove("shake"); + setTimeout(function () { + getById("videoname1").classList.add("shake"); + }, 10); + } +} + +async function jumptoroom(event = null) { + if (event) { + if (event.which !== 13) { + return; + } + } + + var arr = window.location.href.split("?"); + var roomname = getById("joinroomID").value; + roomname = sanitizeRoomName(roomname); + if (roomname.length) { + var passStr = ""; + window.focus(); + var pass = await promptAlt(getTranslation("enter-password-if-desired"), false, true); //sanitizePassword(session.password); + if (pass && pass.length) { + session.password = sanitizePassword(pass); + passStr = "&password=" + session.password; + } else { + session.password = false; + } + + sessionStorage.setItem("jvi", "1"); // joined via input - for showing invite link header + if (arr.length > 1 && arr[1] !== "") { + window.location += "&room=" + roomname + passStr; + } else { + window.location += "?room=" + roomname + passStr; + } + } else { + getById("joinroomID").focus(); + getById("joinroomID").classList.remove("shake"); + setTimeout(function () { + getById("joinroomID").classList.add("shake"); + }, 10); + } +} + +async function jumptoURL(event = null) { + // this is for the native app + + var url = getById("joinbyURL").value; + if (url.length) { + if (url.startsWith("?")) { + url = "./" + url; + } + + if (url.startsWith("&")) { + url = "./?" + url; + } + + if (!url.startsWith("http") && !url.startsWith(".")) { + url = "./?" + url; + } + + setStorage("jumptoURL", url, 1008); // should be really only used by the native app; 6 months + + window.location = url; + } else { + getById("joinbyURL").focus(); + getById("joinbyURL").classList.remove("shake"); + setTimeout(function () { + getById("joinbyURL").classList.add("shake"); + }, 10); + } +} + +function sleep(ms = 0) { + return new Promise(r => setTimeout(r, ms)); // LOLz! +} + +function sleepCancellable(ms = 0) { + let resolve; + const promise = new Promise(r => { + resolve = r; + setTimeout(resolve, ms); + }); + return { promise, resolve }; +} + +async function changeAvatarImage(ev, ele, set = false) { + log("changeAvatarImage() triggered"); + + if (session.avatar && session.avatar.timer) { + clearInterval(session.avatar.timer); + session.avatar.timer = null; + } + + if (!session.streamSrc) { + checkBasicStreamsExist(); + } + + if (ele.files && ele.files.length) { + session.avatar = document.querySelector("img"); + session.avatar.ready = false; + session.avatar.onload = () => { + URL.revokeObjectURL(session.avatar.src); // no longer needed, free memory + session.avatar.ready = true; + getById("noAvatarSelected3").classList.remove("selected"); + getById("noAvatarSelected").classList.remove("selected"); + getById("defaultAvatar1").classList.add("selected"); + getById("defaultAvatar2").classList.add("selected"); + + var tracks = session.streamSrc.getVideoTracks(); + if (!tracks.length || session.videoMuted) { + updateRenderOutpipe(); + } + }; + + session.avatar.src = URL.createObjectURL(ele.files[0]); // set src to blob url + return; + } else if (ele.tagName.toLowerCase() == "img") { + session.avatar = ele; + session.avatar.ready = true; + getById("noAvatarSelected3").classList.remove("selected"); + getById("noAvatarSelected").classList.remove("selected"); + getById("defaultAvatar1").classList.add("selected"); + getById("defaultAvatar2").classList.add("selected"); + var tracks = session.streamSrc.getVideoTracks(); + if (!tracks.length || session.videoMuted) { + updateRenderOutpipe(); + } + } else { + session.avatar = false; + var tracks = session.streamSrc.getVideoTracks(); + if (!tracks.length || session.videoMuted) { + var msg = {}; + msg.videoMuted = true; + session.sendMessage(msg); + if (document.getElementById("videosource")) { + document.getElementById("videosource").load(); + } else if (document.getElementById("previewWebcam")) { + document.getElementById("previewWebcam").load(); + } + updateRenderOutpipe(); + } + + getById("noAvatarSelected3").classList.add("selected"); + getById("noAvatarSelected").classList.add("selected"); + getById("defaultAvatar1").classList.remove("selected"); + getById("defaultAvatar2").classList.remove("selected"); + return; + } +} + +session.autoSyncCallback = function (UUID = null) { + // session.autoSyncObject has been updated. You can overwrite this with your own function + log(session.autoSyncObject); + pokeIframeAPI("auto-sync-updated", session.autoSyncObject, UUID); +}; + +session.autoSync = function (alternative = null) { + // Update session.autoSyncObject and then run session.autoSync() + var msg = {}; + if (alternative === null) { + msg.autoSync = session.autoSyncObject; + } else { + session.autoSyncObject = alternative; + msg.autoSync = session.autoSyncObject; + } + session.sendPeers(msg); +}; + +//function updateRemotePTZControls(videoOptions, UUID){ +// console.log(videoOptions); +// console.log(UUID); +//} + +function isolateIncomingChannel(channel, UUID) { + if (!session.rpcs[UUID]) return; + + if (channel === 0 || channel === false) { + delete session.rpcs[UUID].isolatedChannel; + } else { + session.rpcs[UUID].isolatedChannel = channel || session.rpcs[UUID].isolatedChannel; + } + updateIncomingAudioElement(UUID); +} + +function isolateChannel(source, channel) { + if (!channel || channel === 0) { + return source; // No isolation, return the original source + } + + const splitter = session.audioCtx.createChannelSplitter(6); // Assuming max 6 channels + const merger = session.audioCtx.createChannelMerger(1); // Mono output + + source.connect(splitter); + splitter.connect(merger, channel - 1, 0); // Connect the specified channel to the mono output + + return merger; +} + +function directIsolateChannel(UUID, channel = null) { // isolateChannel() + try { + if (UUID) { + + var targets = document.querySelectorAll("[data--u-u-i-d='" + UUID + "'][data-action-type='isolate-channel']"); + var add = false; + + if (channel) { + add = true; + } + targets.forEach(ele => { + if (channel && parseInt(ele.dataset.channel) && (parseInt(ele.dataset.channel) == channel)) { + + if (ele.classList.contains("pressed")) { + add = false; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + + } else { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } + } else { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } + }); + + var msg = {}; + + if (add) { + msg.isolateChannel = channel + } else { + msg.isolateChannel = false; + } + return session.sendMessage(msg, UUID); + + } + } catch (e) { + errorlog(e); + } + return false; +} + +function uploadImageSnapshot(PostURL) { + if (!session.videoElement) { + return; + } + const video = session.videoElement; + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoHeight); // for drawing the video element on the canvas + const playImage = new Image(); + canvas.getContext("2d").drawImage(playImage, 0, 0, playImage.width, playImage.height); + canvas.toBlob( + function (blob) { + var request = new XMLHttpRequest(); + + if (PostURL.includes("?")) { + request.open("POST", PostURL + "&name=" + session.streamID); + } else { + request.open("POST", PostURL + "?name=" + session.streamID); + } + request.setRequestHeader("Content-Type", "image/jpeg"); + request.send(blob); + log("Posted image"); + }, + "image/jpeg", + 0.9 + ); +} + +var slideshowImage = false; +var slideshowCanvas = false; +var slideshowActive = false; + +function slideshowHack() { + // just shows an animated IMAGE of the first video. audio plays, but no "video" + if (!session.manual) { + session.manual = session.manual === null ? true : session.manual; + } + if (slideshowActive) { + return; + } + slideshowActive = true; + try { + if (!slideshowImage) { + slideshowImage = document.createElement("img"); + slideshowImage.style.width = "100%"; + slideshowImage.style.height = "100%"; + slideshowImage.style.objectFit = "contain"; + slideshowImage.style.border = "0"; + slideshowImage.style.padding = "0"; + slideshowImage.style.margin = "0"; + getById("gridlayout").innerHTML = ""; + getById("gridlayout").appendChild(slideshowImage); + } + var keys = Object.keys(session.rpcs); + if (!keys.length) { + slideshowActive = false; + return; + } + var video = false; + var key = false; + + for (var i = 0; i < keys.length; i++) { + if (session.rpcs[keys[i]].videoElement) { + key = keys[i]; + video = session.rpcs[keys[i]].videoElement; + break; + } + } + if (!video) { + slideshowCanvas = false; + slideshowActive = false; + return; + } + + if (!slideshowCanvas) { + slideshowCanvas = document.createElement("canvas"); + slideshowCanvas.getContext("2d").imageSmoothingEnabled = false; + session.requestResolution(key, session.viewwidth || 480, session.viewheight || 480); + } + slideshowCanvas.width = video.videoWidth; + slideshowCanvas.height = video.videoHeight; + slideshowCanvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoHeight); // for drawing the video element on the canvas + var image = slideshowCanvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); // here is the most important part because if you dont replace you will get a DOM 18 exception. + slideshowImage.src = image; + } catch (e) { + errorlog(e); + } + slideshowActive = false; +} + +function setAvatarImage(tracks) { + if (session.avatar && session.avatar.ready) { + if (session.avatar && session.avatar.timer) { + clearInterval(session.avatar.timer); + session.avatar.timer = null; + } + setupCanvas(); + + var width = 512; + var height = 288; + + var maxW = 1280; + var maxH = 720; + if (session.quality == 0) { + maxW = 1920; + maxH = 1080; + } else if (session.quality == 2) { + maxW = 640; + maxH = 360; + } + + if (session.width) { + maxW = session.width; + } + if (session.height) { + maxH = session.height; + } + + if (session.avatar.naturalHeight && session.avatar.naturalHeight > maxH) { + width = parseInt((maxH / session.avatar.naturalHeight) * session.avatar.naturalWidth); + height = maxH; + + if (width > maxW) { + width = maxW; + height = parseInt((maxW / width) * height); + } + } else if (session.avatar.naturalWidth && session.avatar.naturalWidth > maxW) { + width = maxW; + height = parseInt((maxW / session.avatar.naturalWidth) * session.avatar.naturalHeight); + } else { + width = session.avatar.naturalWidth; + height = session.avatar.naturalHeight; + } + + session.canvasSource.width = width; + session.canvasSource.height = height; + + session.canvas.height = 2 * parseInt(height / 2); + session.canvas.width = 2 * parseInt(width / 2); + + session.canvasCtx.drawImage(session.avatar, 0, 0, session.canvas.width, session.canvas.height); + + session.avatar.timer = setInterval(function () { + log("drawing"); + session.canvasCtx.drawImage(session.avatar, 0, 0, session.canvas.width, session.canvas.height); + }, 200); // too slow and it takes way too long for the video to udpate when a new guest joins + + applyMirror(true); + + session.avatar.tracks = session.canvas.captureStream().getVideoTracks(); + + return session.avatar.tracks; + } + applyMirror(session.mirrorExclude); + return tracks; +} + +var drawOnScreenObject = null; +function drawOnScreen() { + var canvas = document.getElementById("drawOnScreen"); + if (!canvas) { + canvas = document.createElement("canvas"); + document.getElementById("gridlayout").appendChild(canvas); + document.getElementById("gridlayout").style.position = "relative"; + } else { + return; + } + + var ctx = canvas.getContext("2d"); + canvas.width = parseInt(document.getElementById("gridlayout").clientWidth / 2); + canvas.height = parseInt(document.getElementById("gridlayout").clientHeight / 2); + canvas.style.width = "100%"; + canvas.style.height = "100%"; + canvas.style.display = "block"; + canvas.style.position = "absolute"; + canvas.style.bottom = "0"; + canvas.style.left = "0"; + + var flag = false, + prevX = 0, + currX = 0, + prevY = 0, + currY = 0, + dot_flag = false; + + var x = "black", + y = 2; + + var object = {}; + + function findxy(res, e) { + if (res == "down") { + prevX = currX; + prevY = currY; + currX = e.clientX - canvas.offsetLeft; + currY = e.clientY - canvas.offsetTop; + + flag = true; + dot_flag = true; + if (dot_flag) { + ctx.beginPath(); + ctx.fillStyle = x; + ctx.fillRect(currX, currY, 2, 2); + ctx.closePath(); + dot_flag = false; + } + } + if (res == "up" || res == "out") { + flag = false; + } + if (res == "move") { + if (flag) { + prevX = currX; + prevY = currY; + currX = e.clientX - canvas.offsetLeft; + currY = e.clientY - canvas.offsetTop; + draw(); + } + } + } + + function draw() { + ctx.beginPath(); + + var mx = canvas.width / parseInt(document.getElementById("gridlayout").clientWidth); + var my = canvas.height / parseInt(document.getElementById("gridlayout").clientHeight); + var mo = parseInt(document.getElementById("header").clientHeight); + + ctx.moveTo(prevX * mx, prevY * my - mo * my); + ctx.lineTo(currX * mx, currY * my - mo * my); + ctx.strokeStyle = x; + ctx.lineWidth = y; + ctx.stroke(); + ctx.closePath(); + } + + function onMouseMove(e) { + findxy("move", e); + } + + function onMouseDown(e) { + findxy("down", e); + } + + function onMouseUp(e) { + findxy("up", e); + } + + function onMouseOut(e) { + findxy("out", e); + } + + object.stop = function stop() { + canvas.removeEventListener("mousemove", onMouseMove, false); + canvas.removeEventListener("mousedown", onMouseDown, false); + canvas.removeEventListener("mouseup", onMouseUp, false); + canvas.removeEventListener("mouseout", onMouseOut, false); + canvas.remove(); + + document.getElementById("startDrawScreen").classList.remove("hidden"); + + document.querySelectorAll(".drawActive").forEach(ele => { + ele.classList.add("hidden"); + }); + + drawOnScreenObject = null; + }; + + object.init = function init() { + canvas.addEventListener("mousemove", onMouseMove, false); + canvas.addEventListener("mousedown", onMouseDown, false); + canvas.addEventListener("mouseup", onMouseUp, false); + canvas.addEventListener("mouseout", onMouseOut, false); + + document.getElementById("startDrawScreen").classList.add("hidden"); + + document.querySelectorAll(".drawActive").forEach(ele => { + ele.classList.remove("hidden"); + }); + }; + + object.color = function color(obj) { + switch (obj.dataset.color) { + case "green": + x = "green"; + break; + case "blue": + x = "blue"; + break; + case "red": + x = "red"; + break; + case "yellow": + x = "yellow"; + break; + case "orange": + x = "orange"; + break; + case "black": + x = "black"; + break; + case "white": + x = "white"; + break; + } + if (x == "white") y = 14; + else y = 2; + }; + + object.erase = function erase() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + }; + + object.save = function save() { + var dataURL = canvas.toDataURL(); + }; + + object.init(); + drawOnScreenObject = object; + return object; +} + +// SENDER DRAWERS LOGIC PORTION START +function fitCurve(points) { + if (points.length <= 1) return points; + if (points.length === 2) return [{ t: 'l', p: [points[0], points[1]] }]; + if (points.length === 3) return [{ t: 'q', p: points }]; + + let result = []; + for (let i = 0; i < points.length - 1; i += 3) { + let p0 = points[i]; + let p1 = points[i + 1] || p0; + let p2 = points[i + 2] || p1; + let p3 = points[i + 3] || p2; + + result.push({ + t: 'b', + p: [p0, p1, p2, p3] + }); + } + + return result; +} + +function isSharpTurn(points) { + if (points.length < 3) return false; + let angle1 = Math.atan2(points[1].y - points[0].y, points[1].x - points[0].x); + let angle2 = Math.atan2(points[2].y - points[1].y, points[2].x - points[1].x); + let angleDiff = Math.abs(angle2 - angle1); + return angleDiff > Math.PI / 4; +} + +function drawOnThis(video) { + try { + if (!video || !video.container) { + warnlog("no video holder; not compatible"); + return; + } + var container = video.container || video.parentNode; + var holder = container.holder || null; + + var canvas = document.createElement('canvas'); + if (!holder) { + holder = document.createElement("div"); + container.holder = holder; + holder.className = "holder"; + holder.dataset.holder = true; + + container.style = "display: flex;\ + align-items: center;\ + justify-content: center;"; + + container.appendChild(holder); + holder.appendChild(video); + video.style.setProperty('top', '0', 'important'); + video.style.setProperty('left', '0', 'important'); + + session.windowed = false; + applyMirror(); + + canvas.style.position = "fixed"; + holder.style = "position: relative;\ + width: 800px;\ + height: 450px; \ + display: flex;\ + align-items: center;\ + justify-content: center;" + } else { + canvas.className = "drawingCanvas"; + } + + canvas.style.pointerEvents = "none"; + holder.appendChild(canvas); + video.canvas = canvas; + + const ctx = canvas.getContext('2d'); + ctx.lineWidth = 3; + + const buttonContainer = document.createElement('div'); + const enableDrawingBtn = document.createElement('button'); + const clearDrawingBtn = document.createElement('button'); + const undoDrawingBtn = document.createElement('button'); // Undo button + + enableDrawingBtn.textContent = "Enable Drawing"; + clearDrawingBtn.textContent = "Clear"; + undoDrawingBtn.textContent = "Undo"; // Undo button text + buttonContainer.className = "buttonContainer"; + + buttonContainer.appendChild(enableDrawingBtn); + buttonContainer.appendChild(clearDrawingBtn); + buttonContainer.appendChild(undoDrawingBtn); // Add undo button to container + holder.appendChild(buttonContainer); + + let isDrawing = false; + let drawingEnabled = false; + let drawingData = []; + let lastPoint = null; + let lastSentTime = 0; + const sendInterval = 1000; // 1 second + let lastPoints = []; + + function startDrawing(e) { + if (!drawingEnabled) return; + isDrawing = true; + draw(e); + } + + function draw(e) { + if (!isDrawing || !drawingEnabled) return; + + const rect = canvas.getBoundingClientRect(); + let x = (e.clientX - rect.left) / rect.width; + let y = (e.clientY - rect.top) / rect.height; + + // Check if the mouse is within bounds + if (x < 0 || x > 1 || y < 0 || y > 1) { + stopDrawing(); + return; + } + + ctx.lineCap = 'round'; + ctx.strokeStyle = 'red'; + + const canvasX = x * canvas.width; + const canvasY = y * canvas.height; + + if (lastPoint) { + ctx.beginPath(); + ctx.moveTo(lastPoint.x * canvas.width, lastPoint.y * canvas.height); + ctx.lineTo(canvasX, canvasY); + ctx.stroke(); + } + + x = Math.round(x * 4000) / 4000; + y = Math.round(y * 4000) / 4000; + + lastPoint = { x, y }; + lastPoints.push({ x, y }); + + // Send data more frequently + if (lastPoints.length >= 5 || Date.now() - lastSentTime >= 50) { + sendDrawingData(); + } + } + + function redrawCanvas() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.beginPath(); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.strokeStyle = "red"; + + let isNewPath = true; + + for (let i = 0; i < drawingData.length; i++) { + let segment = drawingData[i]; + + if (!segment) { + // End of a path + ctx.stroke(); + ctx.beginPath(); + isNewPath = true; + continue; + } + + if (segment.t) { + // This is a complex segment (line or bezier) + switch (segment.t) { + case 'b': + let [p0, p1, p2, p3] = segment.p; + if (isNewPath) { + ctx.moveTo(p0.x * canvas.width, p0.y * canvas.height); + isNewPath = false; + } + ctx.bezierCurveTo( + p1.x * canvas.width, p1.y * canvas.height, + p2.x * canvas.width, p2.y * canvas.height, + p3.x * canvas.width, p3.y * canvas.height + ); + break; + case 'q': + let [q0, q1, q2] = segment.p; + if (isNewPath) { + ctx.moveTo(q0.x * canvas.width, q0.y * canvas.height); + isNewPath = false; + } + ctx.quadraticCurveTo( + q1.x * canvas.width, q1.y * canvas.height, + q2.x * canvas.width, q2.y * canvas.height + ); + break; + case 'l': + let [l0, l1] = segment.p; + if (isNewPath) { + ctx.moveTo(l0.x * canvas.width, l0.y * canvas.height); + isNewPath = false; + } + ctx.lineTo(l1.x * canvas.width, l1.y * canvas.height); + break; + default: + warnlog(segment); + } + } else if (segment.x !== undefined && segment.y !== undefined) { + // This is a simple point + const canvasX = segment.x * canvas.width; + const canvasY = segment.y * canvas.height; + if (isNewPath) { + ctx.moveTo(canvasX, canvasY); + isNewPath = false; + } else { + ctx.lineTo(canvasX, canvasY); + } + } + } + + ctx.stroke(); + } + + function processPoints(points) { + let processedPoints = []; + let currentSegment = []; + for (let i = 0; i < points.length; i++) { + if (points[i] === null) { + if (currentSegment.length > 0) { + if (currentSegment.length === 2) { + processedPoints.push({ t: 'l', p: currentSegment }); + } else { + processedPoints.push(...fitCurve(currentSegment)); + } + currentSegment = []; + } + processedPoints.push(null); + } else if (i > 0 && isSignificantBreak(points[i - 1], points[i])) { + if (currentSegment.length > 0) { + if (currentSegment.length === 2) { + processedPoints.push({ t: 'l', p: currentSegment }); + } else { + processedPoints.push(...fitCurve(currentSegment)); + } + currentSegment = []; + } + currentSegment.push(points[i]); + } else { + currentSegment.push(points[i]); + if (currentSegment.length >= 4) { + processedPoints.push(...fitCurve(currentSegment)); + currentSegment = [currentSegment[currentSegment.length - 1]]; + } + } + } + if (currentSegment.length > 0) { + if (currentSegment.length === 2) { + processedPoints.push({ t: 'l', p: currentSegment }); + } else { + processedPoints.push(...fitCurve(currentSegment)); + } + } + return processedPoints; + } + + function isSignificantBreak(point1, point2) { + const distance = Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2)); + return distance > 0.05; // Increased threshold + } + + function processCurrentSegment(segment) { + if (segment.length < 3) return segment; + + if (isSharpTurn(segment)) { + return segment; + } else { + return fitCurve(segment); + } + } + + function drawBezierCurve(points) { + const [start, control1, control2, end] = points; + ctx.moveTo(start.x * canvas.width, start.y * canvas.height); + ctx.bezierCurveTo( + control1.x * canvas.width, control1.y * canvas.height, + control2.x * canvas.width, control2.y * canvas.height, + end.x * canvas.width, end.y * canvas.height + ); + } + + function sendDrawingData(alt = false) { + if (alt === "clear") { + drawingData = []; + if (video.id === "videosource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing) { + session.sendMessage({ draw: "clear" }, UUID); + } + } + } else if (video.id === "screensharesource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { + session.sendMessage({ draw: "clear", altUUID: true }, UUID); + } + } + } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { + session.sendRequest({ draw: "clear" }, video.dataset.UUID); + } + return; + } + if (alt === "cleanup") { + drawingData = []; + if (video.id === "videosource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing) { + session.sendMessage({ draw: "cleanup" }, UUID); + } + } + } else if (video.id === "screensharesource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { + session.sendMessage({ draw: "cleanup", altUUID: true }, UUID); + } + } + } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { + session.sendRequest({ draw: "cleanup" }, video.dataset.UUID); + } + return; + } + if (alt === "undo") { + if (drawingData.length > 0) { + // Find the last segment to remove + let found = false; + for (let i = drawingData.length - 1; i >= 0; i--) { + if (drawingData[i] === null) { + drawingData = drawingData.slice(0, i); + if (found) { + drawingData.push(null); + break; + } + } else { + found = true; + } + if (i === 0) { // Handle case when there's no null in drawingData + drawingData = []; + } + } + redrawCanvas(); + + // Send the undo command + if (video.id === "videosource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing) { + session.sendMessage({ draw: "undo" }, UUID); + } + } + } else if (video.id === "screensharesource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { + session.sendMessage({ draw: "undo", altUUID: true }, UUID); + } + } + } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { + session.sendRequest({ draw: "undo" }, video.dataset.UUID); + } + + } + return; + } + if (alt === "sync") { + if (!drawingData.length) { return; } + // Send the processed points + if (video.id === "videosource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing) { + if (!session.pcs[UUID].initialDrawing) { + session.pcs[UUID].initialDrawing = true; + session.sendMessage({ draw: { p: drawingData } }, UUID); + } + } + } + } else if (video.id === "screensharesource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { + if (!session.pcs[UUID].initialDrawing2) { + session.pcs[UUID].initialDrawing2 = true; + session.sendMessage({ draw: { p: drawingData }, altUUID: true }, UUID); + } + } + } + } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { + if (!session.rpcs[video.dataset.UUID].initialDrawing) { + session.rpcs[video.dataset.UUID].initialDrawing = true; + session.sendRequest({ draw: { p: drawingData } }, video.dataset.UUID); + } + } + return; + } + if (lastPoints.length > 0) { + var processedPoints = processPoints(lastPoints); + lastPoints = []; + lastSentTime = Date.now(); + + var dataToSend = { + p: processedPoints + }; + + drawingData.push(...processedPoints); // Store only points in drawingData + + // Send the processed points with timestamp + if (video.id === "videosource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing) { + if (session.pcs[UUID].initialDrawing) { + session.sendMessage({ draw: dataToSend }, UUID); + } else { + session.pcs[UUID].initialDrawing = true; + session.sendMessage({ draw: { p: drawingData } }, UUID); + } + } + } + } else if (video.id === "screensharesource") { + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowDrawing && !session.pcs[UUID].realUUID) { + if (session.pcs[UUID].initialDrawing2) { + session.sendMessage({ draw: dataToSend, altUUID: true }, UUID); + } else { + session.pcs[UUID].initialDrawing2 = true; + session.sendMessage({ draw: { p: drawingData }, altUUID: true }, UUID); + } + } + } + } else if (session.rpcs[video.dataset.UUID] && session.rpcs[video.dataset.UUID].allowDrawing) { + if (session.rpcs[video.dataset.UUID].initialDrawing) { + session.sendRequest({ draw: dataToSend }, video.dataset.UUID); + } else { + session.rpcs[video.dataset.UUID].initialDrawing = true; + session.sendRequest({ draw: { p: drawingData } }, video.dataset.UUID); + } + } + } + } + + function resizeCanvas() { + canvas.width = video.offsetWidth; + canvas.height = video.offsetHeight; + redrawCanvas(); + } + + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + video.addEventListener('resize', resizeCanvas); + + enableDrawingBtn.addEventListener('click', () => { + drawingEnabled = !drawingEnabled; + enableDrawingBtn.textContent = drawingEnabled ? 'Disable Drawing' : 'Enable Drawing'; + canvas.style.pointerEvents = drawingEnabled ? "auto" : "none"; + }); + + clearDrawingBtn.addEventListener('click', () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawingData = []; + lastPoints = []; + sendDrawingData("clear"); + }); + + undoDrawingBtn.addEventListener('click', () => { + sendDrawingData("undo"); + }); + + function stopDrawing() { + if (isDrawing) { + + isDrawing = false; + ctx.beginPath(); + lastPoint = null; + lastPoints.push(null); // Add null to mark end of path + sendDrawingData(); + } + } + + canvas.addEventListener('mousedown', startDrawing); + canvas.addEventListener('mousemove', draw); + canvas.addEventListener('mouseup', stopDrawing); + canvas.addEventListener('mouseout', stopDrawing); + canvas.addEventListener('mouseleave', stopDrawing); + canvas.addEventListener('mouseenter', (e) => { + if (e.buttons !== 1) { // If left mouse button is not pressed + stopDrawing(); + } + }); + + function createCleanupFunction() { + return function cleanup() { + sendDrawingData("cleanup"); + + window.removeEventListener('resize', resizeCanvas); + video.removeEventListener('resize', resizeCanvas); + + if (canvas) { + canvas.removeEventListener('mousedown', startDrawing); + canvas.removeEventListener('mousemove', draw); + canvas.removeEventListener('mouseup', stopDrawing); + canvas.removeEventListener('mouseout', stopDrawing); + canvas.removeEventListener('mouseleave', stopDrawing); + canvas.removeEventListener('mouseenter', stopDrawing); + + if (canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + } + + if (buttonContainer && buttonContainer.parentNode) { + buttonContainer.parentNode.removeChild(buttonContainer); + } + }; + } + + function syncNewConnections() { + return function syncDrawing() { + setTimeout(() => { + sendDrawingData("sync") + }, 4000); + } + } + + video.syncDrawOnVideo = syncNewConnections(); + video.clearDrawOnVideo = createCleanupFunction(); + return video.clearDrawOnVideo; + + } catch (e) { + errorlog(e); + } +} + +// END SENDING LOGIC +// var cleanUp = drawOnThis(document.getElementById('videoElement')); +// cleanUp(); +// START RECEIVING LOGIC + +function receiveDrawingOnVideo(video, UUID = false) { + try { + if (!video || !video.container) { + warnlog("no video holder; not compatible"); + return; + } + const canvas = document.createElement('canvas'); + canvas.className = "drawingCanvas"; + canvas.style.pointerEvents = "none"; + var receivedDrawingData = []; + + var container = video.parentNode; + if (!container) { + return; + } + container.appendChild(canvas); + + var color = 'red'; + if (UUID) { + color = getColorFromName(UUID); + } + + function positionCanvas() { + const videoRect = video.getBoundingClientRect(); + const computedStyle = getComputedStyle(video); + + canvas.style.width = computedStyle.width; + canvas.style.height = computedStyle.height; + canvas.style.top = `${videoRect.top + window.scrollY}px`; + canvas.style.left = `${videoRect.left + window.scrollX}px`; + canvas.width = video.clientWidth; + canvas.height = video.clientHeight; + + if (video.dataset.transform) { + canvas.style.transform = video.dataset.transform; + } + redrawCanvas(); + } + + const ctx = canvas.getContext('2d'); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.lineWidth = 3; + + function resizeCanvas() { + canvas.width = video.offsetWidth; + canvas.height = video.offsetHeight; + if (video.dataset.transform) { + canvas.style.transform = video.dataset.transform; + } + redrawCanvas(); + } + + let observer = null; + if (!(video && video.container && video.container.holder)) { + positionCanvas(); + window.addEventListener('resize', positionCanvas); + video.addEventListener('resize', positionCanvas); + observer = new ResizeObserver(positionCanvas); + observer.observe(video); + } else { + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + video.addEventListener('resize', resizeCanvas); + } + + function drawBezierCurve(points) { + const [start, control1, control2, end] = points; + ctx.moveTo(start.x * canvas.width, start.y * canvas.height); + ctx.bezierCurveTo( + control1.x * canvas.width, control1.y * canvas.height, + control2.x * canvas.width, control2.y * canvas.height, + end.x * canvas.width, end.y * canvas.height + ); + } + + function redrawCanvas() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.strokeStyle = color; + + let isNewPath = true; + + for (let i = 0; i < receivedDrawingData.length; i++) { + let segment = receivedDrawingData[i]; + + if (!segment) { + // End of a path + ctx.stroke(); + ctx.beginPath(); + isNewPath = true; + continue; + } + + switch (segment.t) { + case 'b': + let [p0, p1, p2, p3] = segment.p; + if (isNewPath) { + ctx.moveTo(p0.x * canvas.width, p0.y * canvas.height); + isNewPath = false; + } + ctx.bezierCurveTo( + p1.x * canvas.width, p1.y * canvas.height, + p2.x * canvas.width, p2.y * canvas.height, + p3.x * canvas.width, p3.y * canvas.height + ); + break; + case 'q': + let [q0, q1, q2] = segment.p; + if (isNewPath) { + ctx.moveTo(q0.x * canvas.width, q0.y * canvas.height); + isNewPath = false; + } + ctx.quadraticCurveTo( + q1.x * canvas.width, q1.y * canvas.height, + q2.x * canvas.width, q2.y * canvas.height + ); + break; + case 'l': + let [l0, l1] = segment.p; + if (isNewPath) { + ctx.moveTo(l0.x * canvas.width, l0.y * canvas.height); + isNewPath = false; + } + ctx.lineTo(l1.x * canvas.width, l1.y * canvas.height); + break; + default: + // Fallback for non-curve points + if (isNewPath) { + ctx.moveTo(segment.x * canvas.width, segment.y * canvas.height); + isNewPath = false; + } else { + ctx.lineTo(segment.x * canvas.width, segment.y * canvas.height); + } + } + } + + ctx.stroke(); + } + + function updateDrawing(newData) { + if (newData === "clear") { + receivedDrawingData = []; + ctx.clearRect(0, 0, canvas.width, canvas.height); + } else if (newData === "undo") { + if (receivedDrawingData.length > 0) { + // Find the last segment to remove + let found = false; + for (let i = receivedDrawingData.length - 1; i >= 0; i--) { + if (receivedDrawingData[i] === null) { + receivedDrawingData = receivedDrawingData.slice(0, i); + if (found) { + receivedDrawingData.push(null); + break; + } + } else { + found = true; + } + if (i === 0) { // Handle case when there's no null in receivedDrawingData + receivedDrawingData = []; + } + } + redrawCanvas(); + } + } else { + // Handle both new and old data formats + if (newData.p) { + // New format + receivedDrawingData.push(...newData.p); + } else if (Array.isArray(newData)) { + // Old format or array of points + receivedDrawingData.push(...newData); + } else if (typeof newData === 'object' && newData.x !== undefined && newData.y !== undefined) { + // Single point + receivedDrawingData.push(newData); + } else { + console.error("Unexpected data format:", newData); + return; + } + + redrawCanvas(); + } + } + + function clearDrawing() { + receivedDrawingData = []; + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + + function cleanup() { + + try { + if (observer) { + window.removeEventListener('resize', positionCanvas); + video.removeEventListener('resize', positionCanvas); + observer.disconnect(); + } else { + window.removeEventListener('resize', resizeCanvas); + video.removeEventListener('resize', resizeCanvas); + } + + clearDrawing(); + } catch (e) { + errorlog(e); + } + + container.removeChild(canvas); + } + + return { + updateDrawing, + clearDrawing, + cleanup + }; + } catch (e) { + errorlog(e); + } +} + +// END REMOTE DRAWING LOGIC + +////////// Canvas Effects /////////////// + +var drawFrameMirroredActive = false; +function drawFrameMirrored(mirror = true, flip = false) { + if (drawFrameMirroredActive) return; + drawFrameMirroredActive = true; + + if (session.effect == "2") { + mirror = true; + flip = false; + } else if (session.effect == "-2") { + mirror = true; + flip = true; + } else if (session.effect == "-1") { + mirror = false; + flip = true; + } + + try { + session.canvasCtx.save(); + if (flip) { + session.canvasCtx.scale(mirror ? -1 : 1, -1); + session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * (mirror ? -1 : 1), session.canvas.height * -1); + } else { + session.canvasCtx.scale(mirror ? -1 : 1, 1); + session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvas.width * (mirror ? -1 : 1), session.canvas.height); + } + session.canvasCtx.restore(); + } catch (e) { + errorlog(e); + } + + drawFrameMirroredActive = false; +} + +function motionDetection(video, threshold = 15, sensitivity = 75) { + var targetSize = 16; + + if (!video.motionDetector) { + video.motionDetector = {}; + video.motionDetector.canvas = document.createElement("canvas"); + video.motionDetector.canvas.width = targetSize; + video.motionDetector.canvas.height = targetSize; + try { + video.motionDetector.ctx = video.motionDetector.canvas.getContext("2d", { willReadFrequently: true }); + } catch (e) { + video.motionDetector.ctx = video.motionDetector.canvas.getContext("2d"); + } + video.motionDetector.previous = []; + for (var y = 0; y < targetSize; y++) { + for (var x = 0; x < targetSize; x++) { + video.motionDetector.previous.push(0); + } + } + } + var motionDetector = video.motionDetector; + + motionDetector.ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, targetSize, targetSize); + var data = motionDetector.ctx.getImageData(0, 0, targetSize, targetSize).data; + var matches = 0; + for (var y = 0; y < targetSize; y++) { + for (var x = 0; x < targetSize; x++) { + var pos = y * targetSize + x; + var pos2 = pos * 3; + var value = data[pos2] + data[pos2 + 1] + data[pos2 + 2]; // convert to to greyscale + if (motionDetector.previous[pos] && Math.abs(motionDetector.previous[pos] - value) > sensitivity) { + matches += 1; + } + motionDetector.previous[pos] = value; + } + } + if (matches >= threshold) { + log("MOTION DETECTED: " + matches); + if (session.motionSwitch && window.obsstudio && window.obsstudio["setCurrentScene"]) { + if (!changeSceneEnabled) { + // the bit cut scene change is already active. + if (session.obsState && session.obsState.details && session.obsState.details.thisScene && session.obsState.details.currentScene) { + if (session.obsState.details.thisScene !== session.obsState.details.currentScene.name) { + // don't trigger it multiple times; makes it hard to prep next scene + window.obsstudio["setCurrentScene"](session.obsState.details.thisScene); + } + } + } + } + pokeIframeAPI("motion-detected", true, video.dataset.UUID || true); + + if (session.infocus !== (video.dataset.UUID || true)) { + if (!session.layout) { + session.infocus = video.dataset.UUID || true; + updateMixer(); + } + } + + if (session.motionRecord) { + if (!session.motionRecordTimeout) { + session.motionRecordTimeout = setTimeout(function () { + session.motionRecordTimeout = null; + }, 1000); + saveVideoFrameToDisk(video); + } + } + } +} + +let currentOscillatorId = 0; +function clearOscillator() { + const thisOscillatorId = ++currentOscillatorId; + if (session.canvasOscillator) { + session.canvasOscillator.stop(); + session.canvasOscillator.disconnect(); + session.canvasOscillator = null; + } + + if (session.canvasSilence) { + session.canvasSilence.disconnect(); + session.canvasSilence = null; + } + if (session.stats) { + delete session.stats.canvas_draw_rate; + } +} +function setupOscillator(callbackFunction, frameRate = 30, timeOne = null, thisOscillatorId = null) { + if (!thisOscillatorId) { + thisOscillatorId = ++currentOscillatorId; + } else if (currentOscillatorId !== thisOscillatorId) { + return false; + } + + if (session.canvasOscillator) { + session.canvasOscillator.stop(); + session.canvasOscillator.disconnect(); + session.canvasOscillator = null; + } + + if (session.canvasSilence) { + session.canvasSilence.disconnect(); + session.canvasSilence = null; + } + + if (!session.audioCtx) { + session.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + + let oscillator = session.audioCtx.createOscillator(); + session.canvasOscillator = oscillator; + let silence = session.audioCtx.createGain(); + session.canvasSilence = silence; + + silence.gain.value = 0; + oscillator.connect(silence); + silence.connect(session.audioCtx.destination); + + if (!timeOne) { + timeOne = session.audioCtx.currentTime; + } + + oscillator.onended = () => { + oscillator.disconnect(); + silence.disconnect(); + if (currentOscillatorId === thisOscillatorId) { + let timeTwo = session.audioCtx.currentTime; + + if (typeof callbackFunction === "function") { + callbackFunction(); + } + + if (session.stats) { + let actualRate = 1 / (timeTwo - timeOne); // Calculate the actual FPS + session.stats.canvas_draw_rate = parseInt(actualRate * 10) / 10; + } + setupOscillator(callbackFunction, frameRate, timeTwo, thisOscillatorId); + } + }; + + oscillator.start(timeOne); + oscillator.stop(timeOne + 1 / frameRate); + + return function (check = false) { + if (check && currentOscillatorId !== thisOscillatorId) { + clearOscillator(); + return true; + } else if (check) { + return false; + } + if (currentOscillatorId === thisOscillatorId) { + clearOscillator(); // clear only if needs to be cleared + } + return false; + }; +} + +function setupCanvas() { + clearOscillator(); + + log("SETUP CANVAS"); + + if (session.canvas === null) { + session.canvas = document.createElement("canvas"); + session.canvas.width = 512; + session.canvas.height = 288; + try { + session.canvasCtx = session.canvas.getContext("2d", { alpha: true, willReadFrequently: true }); + } catch (e) { + errorlog(e); + session.canvasCtx = session.canvas.getContext("2d"); + } + + session.canvasCtx.fillStyle = "black"; + session.canvasCtx.fillRect(0, 0, 512, 288); + session.canvasSource = createVideoElement(); + session.canvasSource.autoplay = true; + session.canvasSource.srcObject = createMediaStream(); + session.canvasSource.id = "effectsVideoSource"; + + if (session.canvasSource.srcObject.getVideoTracks().length) { + session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; + session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; + } + + if (iOS || iPad) { + session.canvasSource.style.position = "absolute"; + session.canvasSource.style.left = "0"; + session.canvasSource.style.top = "0"; + session.canvasSource.controls = session.showControls || false; + session.canvasSource.style.maxWidth = "1px"; + session.canvasSource.style.maxHeight = "1px"; + session.canvasSource.setAttribute("playsinline", ""); + document.body.appendChild(session.canvasSource); + //session.canvasSource.play(); + } + } else { + session.canvasSource.srcObject.getVideoTracks().forEach(function (trk) { + session.canvasSource.srcObject.removeTrack(trk); + }); + } +} + +function applyEffects(track) { + // video only please. do not touch audio. Run update Render Outpipe () instead of this directly. + log("applyEffects()"); + + if (session.effect == "0" || !session.effect) { + // auto align face + return track; + } else if (session.effect == "1") { + // auto align face + setupCanvas(); + session.canvasSource.srcObject.addTrack(track); + + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + drawFace(); + } else if (session.effect == "7") { + // manual zoom + setupCanvas(); + session.canvasSource.srcObject.addTrack(track); + + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + digitalZoom(true); + } else if (["8", "overlay"].includes(session.effect)) { + // manual zoom + setupCanvas(); + session.canvasSource.srcObject.addTrack(track); + + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + simpleDraw(); + } else if (["-2", "-1", "2"].includes(session.effect)) { + // mirror video at a canvas level + setupCanvas(); + session.canvasSource.srcObject.addTrack(track); + + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + setupOscillator(drawFrameMirrored, track.getSettings().frameRate || 30); + } else if (session.effect == "3" || session.effect == "4" || session.effect == "5") { + // blur & greenscreen (low and high) + setupCanvas(); + session.canvasSource.srcObject.addTrack(track); + + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + TFLiteWorker(); + } else if (session.effect == "6") { + setupCanvas(); + session.canvasSource.srcObject.addTrack(track); + + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + if (session.canvasSource.readyState >= 3) { + mainMeshMask(); + } else { + session.canvasSource.onloadeddata = mainMeshMask; + } + } else if (session.effect == "13") { + // ha, no way to turn it off once it's started, except to change cameras? not sure. + try { + track + .applyConstraints({ backgroundBlur: true }) + .then(() => { + const settings = track.getSettings(); + log(`Background blur is ${settings.backgroundBlur ? "ON" : "OFF"}`); + }) + .catch(errorlog); + } catch (e) { + errorlog(e); + } + return track; + } else if (session.effect == "14" || session.effect == "15") { + // chroma key effects + setupCanvas(); + session.canvasSource.srcObject.addTrack(track); + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + chromaKey(); + } else { + if (session.canvasource) { + session.canvasSource.srcObject.getVideoTracks().forEach(function (trk) { + session.canvasSource.srcObject.removeTrack(trk); + }); + } else { + session.canvasSource = createVideoElement(); + session.canvasSource.srcObject = createMediaStream(); + } + + session.canvasSource.autoplay = true; + session.canvasSource.id = "effectsVideoSource"; + session.canvasSource.srcObject.addTrack(track); + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + session.canvas.width = 512; + session.canvas.height = 288; + + if (iOS || iPad) { + session.canvasSource.style.position = "absolute"; + session.canvasSource.style.left = "0"; + session.canvasSource.style.top = "0"; + session.canvasSource.style.maxWidth = "1px"; + session.canvasSource.style.maxHeight = "1px"; + session.canvasSource.controls = session.showControls || false; + + session.canvasSource.setAttribute("playsinline", ""); + document.body.appendChild(session.canvasSource); + //session.canvasSource.play(); + } + + try { + JEELIZFACEFILTER.destroy(); + } catch (e) { } + if (session.canvasWebGL) { + session.canvasWebGL.remove(); + session.canvasWebGL = null; + } + session.canvasWebGL = document.createElement("canvas"); + session.canvasWebGL.width = track.getSettings().width || 1280; + session.canvasWebGL.height = track.getSettings().height || 720; + session.canvasWebGL.id = "effectsCanvasTarget"; + session.canvasWebGL.style.position = "fixed"; + session.canvasWebGL.style.top = "-9999px"; + session.canvasWebGL.style.left = "-9999px"; + + document.body.appendChild(session.canvasWebGL); + loadEffect(session.effect); + + return session.canvasWebGL.captureStream().getVideoTracks()[0]; + } + try { + return session.canvas.captureStream().getVideoTracks()[0]; + + } catch (e) { + if (!session.cleanOutput) { + warnUser(getTranslation("not-clean-session"), false, false); + } + return track; + } +} + +function dataURItoArraybuffer(dataURI) { + var byteString = atob(dataURI.split(",")[1]); + var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0]; + var ab = new ArrayBuffer(byteString.length); + var ia = new Uint8Array(ab); + for (var i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + return ab; +} + +var makeImagesActive = null; +async function makeImages(startup = false) { + if (!session.webp) { + return; + } else if (makeImagesActive === true) { + return; + } else if (!session.videoElement) { + return; + } else if (session.videoMuted) { + return; + } + + var stream = session.getLocalStream(); + + if (!stream || !stream.getVideoTracks().length) { + errorlog("No video element; can't make images for webp mode"); + if (makeImagesActive) { + var exit = true; + for (var i in session.pcs) { + if (session.pcs[i].allowWebp) { + // just for safety, to avoid a race condition, double check that it's still not active. + exit = false; + } + } + if (exit) { + makeImagesActive = false; + return; + } + + if (session.webPcanvas.makeImagesTimeout) { + session.webPcanvas.makeImagesTimeout.onended = null; + session.webPcanvas.makeImagesTimeout = null; + } + + var osc = session.webPcanvas.aCtx.createOscillator(); + osc.connect(session.webPcanvas.silence); + session.webPcanvas.makeImagesTimeout = osc; + osc.start(0); + osc.onended = function () { + this.disconnect(); + makeImages(); + }; + osc.stop(session.webPcanvas.aCtx.currentTime + 0.5); + } + return; + } + + if (makeImagesActive === null) { + makeImagesActive = true; + session.webPcanvas = document.createElement("canvas"); + session.webPcanvas.makeImagesTimeout = null; + + session.webPcanvas.aCtx = new AudioContext(); + session.webPcanvas.silence = session.webPcanvas.aCtx.createGain(); + session.webPcanvas.silence.gain.value = 0; + session.webPcanvas.silence.connect(session.webPcanvas.aCtx.destination); + + session.webPcanvas.nowTime = new Date().getTime(); + + session.webPcanvasCtx = session.webPcanvas.getContext("2d", { alpha: session.alpha }); + } else { + if (session.webPcanvas.makeImagesTimeout) { + session.webPcanvas.makeImagesTimeout.onended = null; + } + makeImagesActive = true; + } + + if (startup) { + var exit = true; + for (var i in session.pcs) { + if (session.pcs[i].allowWebp) { + // just for safety, to avoid a race condition, double check that it's still not active. + exit = false; + } + } + if (exit) { + makeImagesActive = false; + return; + } + log("MAKE IMAGES STARTING?"); + } + + var track = stream.getVideoTracks()[0]; + var settings = track.getSettings(); + + try { + var broadcasting = false; + var arrayBuffer = false; + + var width = 480; + var height = 270; + var timeout = settings.frameRate > 24 ? 1000 / 24 : 1000 / settings.frameRate; // the answer to everything. + var quality = 0.66; + + if (session.webPquality === 0) { + width = 1920; + height = 1080; + timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; + } else if (session.webPquality === 1) { + width = 1280; + height = 720; + timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; + } else if (session.webPquality === 2) { + width = 960; + height = 540; + timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; + } else if (session.webPquality === 3) { + width = 853; + height = 480; + timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; + } else if (session.webPquality === 4) { + width = 640; + height = 360; + timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; + } else if (session.webPquality === 5) { + width = 480; + height = 270; + timeout = settings.frameRate > 30 ? 1000 / 30 : 1000 / settings.frameRate; + } else if (session.webPquality === 6) { + width = 480; + height = 270; + timeout = 1000 / 15; + } else if (session.webPquality === 7) { + width = 480; + height = 270; + timeout = 1000 / 5; + } else if (session.webPquality === 8) { + width = 480; + height = 270; + timeout = 1000 / 3; + } else if (session.webPquality === 9) { + width = 640; + height = 360; + timeout = 1000; + } + + session.webPcanvas.timeout = timeout; + session.webPcanvas.quality = quality; + + if (settings.width < width) { + session.webPcanvas.width = settings.width; + session.webPcanvasCtx.width = settings.width; + } else { + session.webPcanvas.width = width; + session.webPcanvasCtx.width = width; + } + + if (settings.height < height) { + session.webPcanvas.height = settings.height; + session.webPcanvasCtx.height = settings.height; + } else { + session.webPcanvas.height = height; + session.webPcanvasCtx.height = height; + } + + var ar = session.webPcanvas.width / session.webPcanvas.height; + + if (session.forceAspectRatio && session.forceAspectRatio > ar) { + session.webPcanvas.width = session.webPcanvas.height * session.forceAspectRatio; + } else if (session.forceAspectRatio && session.forceAspectRatio <= ar) { + session.webPcanvas.height = session.webPcanvas.width * session.forceAspectRatio; + } + + for (var i in session.pcs) { + try { + if (session.pcs[i].allowWebp) { + // only publish to those seeking this stream + broadcasting = true; + if (!session.pcs[i].sendChannel.bufferedAmount) { + try { + if (!arrayBuffer) { + session.webPcanvasCtx.drawImage(session.videoElement, 0, 0, session.webPcanvas.width, session.webPcanvas.height); + arrayBuffer = dataURItoArraybuffer(session.webPcanvas.toDataURL("image/" + session.webp, session.webPcanvas.quality)); + } + session.pcs[i].sendChannel.send(arrayBuffer); + } catch (e) { + errorlog(e); + } + } + } + } catch (e) { } + } + } catch (e) { + errorlog(e); + makeImagesActive = false; + return; + } + makeImagesActive = false; + + session.webPcanvas.lastTime = session.webPcanvas.nowTime; + session.webPcanvas.nowTime = new Date().getTime(); + + if (broadcasting) { + // wait a bit of time, now that we sent a frame out. + + var time = session.webPcanvas.timeout - (session.webPcanvas.nowTime - session.webPcanvas.lastTime); + if (time <= 0) { + var osc = session.webPcanvas.aCtx.createOscillator(); + osc.connect(session.webPcanvas.silence); + session.webPcanvas.makeImagesTimeout = osc; + osc.start(0); + osc.onended = function () { + this.disconnect(); + makeImages(); + }; + osc.stop(session.webPcanvas.aCtx.currentTime); + + //session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},0); + } else { + var osc = session.webPcanvas.aCtx.createOscillator(); + osc.connect(session.webPcanvas.silence); + session.webPcanvas.makeImagesTimeout = osc; + osc.start(0); + osc.onended = function () { + this.disconnect(); + makeImages(); + }; + osc.stop(session.webPcanvas.aCtx.currentTime + time / 1000); + + //session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},time); + } + } else { + // just double check that we shoulnd't be broadcasting. + for (var i in session.pcs) { + if (session.pcs[i].allowWebp) { + var osc = session.webPcanvas.aCtx.createOscillator(); + osc.connect(session.webPcanvas.silence); + session.webPcanvas.makeImagesTimeout = osc; + osc.start(0); + osc.onended = function () { + this.disconnect(); + makeImages(); + }; + osc.stop(session.webPcanvas.aCtx.currentTime + time / 1000); + //session.webPcanvas.makeImagesTimeout = setTimeout(function(){makeImages();},0); + return; + } + } + log("Stopping webP broadcast."); + } +} + +var updateUserListTimeout = null; +var updateUserListActive = false; +function updateUserList() { + if (session.showList === true) { + // continue + } else if (session.showList !== true && (session.cleanOutput || session.scene !== false || !session.roomid || session.director || session.showList === false)) { + return; + } + clearInterval(updateUserListTimeout); + updateUserListTimeout = setTimeout(function () { + if (updateUserListActive) { + return; + } + updateUserListActive = true; + try { + var added = false; + getById("userList").innerHTML = ""; + + for (var UUID in session.rpcs) { + if ((session.rpcs[UUID].videoElement && session.rpcs[UUID].streamSrc && session.rpcs[UUID].streamSrc.getTracks().length) || session.rpcs[UUID].canvas || session.rpcs[UUID].imageElement) { + if (session.rpcs[UUID].videoElement && document.body.contains(session.rpcs[UUID].videoElement)) { + continue; + } else if (session.rpcs[UUID].canvas && document.body.contains(session.rpcs[UUID].canvas)) { + continue; + } else if (session.rpcs[UUID].imageElement && document.body.contains(session.rpcs[UUID].imageElement)) { + continue; + } + } + if (session.rpcs[UUID].virtualHangup) { + // end of screen share / director ? + continue; + } + if (session.rpcs[UUID].videoMuted || (!session.rpcs[UUID].imageElement && !session.rpcs[UUID].canvas) || (session.infocus && session.infocus !== UUID) || (!session.rpcs[UUID].defaultSpeaker && session.activeSpeaker)) { + if (session.directorList.indexOf(UUID) >= 0) { + if (!session.rpcs[UUID].streamSrc) { + // director not active yet, so we won't bother showing it. + continue; + } + } + + var insert = document.createElement("div"); + if (session.rpcs[UUID].label) { + insert.innerText = session.rpcs[UUID].label.split("\\n")[0] + ""; + } else if (session.directorList.indexOf(UUID) >= 0) { + miniTranslate(insert, "director"); + //insert.innerHTML = getTranslation("director"); + } else { + miniTranslate(insert, "unknown-user"); + //insert.innerHTML = getTranslation("unknown-user"); + } + try { + insert.dataset.UUID = UUID; + insert.dataset.sid = session.rpcs[UUID].streamID; + insert.title = "Stream ID: " + session.rpcs[UUID].streamID; + insert.addEventListener("click", function (e) { + // show stats of video if double clicked + log("clicked"); + try { + if (session.statsMenu !== false) { + var uid = e.currentTarget.dataset.UUID; + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, uid); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + e.stopPropagation(); + return false; + } + } + } catch (e) { + errorlog(e); + } + }); + if (session.statsMenu) { + if ("stats" in session.rpcs[UUID]) { + if (getById("menuStatsBox")) { + clearInterval(getById("menuStatsBox").interval); + getById("menuStatsBox").remove(); + } + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, UUID); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID); + } + } + } catch (e) { } + + getById("userList").appendChild(insert); + + if (session.rpcs[UUID].videoElement) { + var volumeBarInsert = document.createElement("input"); + volumeBarInsert.type = "range"; + volumeBarInsert.className = "hidden"; + volumeBarInsert.setAttribute("orient", "vertical"); + volumeBarInsert.max = 100; + volumeBarInsert.min = 0; + volumeBarInsert.step = 1; + volumeBarInsert.dataset.UUID = UUID; + volumeBarInsert.setAttribute("value", parseFloat(session.rpcs[UUID].videoElement.volume) * 100); + volumeBarInsert.title = volumeBarInsert.value + "%"; + volumeBarInsert.oninput = function (e) { + session.rpcs[this.dataset.UUID].videoElement.volume = parseInt(this.value) / 100 || 0; + if (!session.rpcs[this.dataset.UUID].videoElement.volume) { + this.parentNode.classList.add("red"); + } else { + this.parentNode.classList.remove("red"); + } + }; + volumeBarInsert.onchange = function (e) { + this.parentNode.querySelector("input").classList.toggle("hidden"); + }; + var volumeButton = document.createElement("i"); + volumeButton.className = "las la-volume-up"; + volumeButton.onclick = function () { + this.parentNode.querySelector("input").classList.toggle("hidden"); + this.parentNode.volumeBarInsert.focus(); + }; + var volumeInsert = document.createElement("div"); + volumeInsert.className = "volume-control-userlist"; + volumeInsert.volumeBarInsert = volumeBarInsert; + insert.appendChild(volumeInsert); + volumeInsert.appendChild(volumeBarInsert); + volumeInsert.appendChild(volumeButton); + } + + if (session.rpcs[UUID].remoteMuteState || !session.rpcs[UUID].streamSrc) { + var muteInsert = document.createElement("div"); + muteInsert.className = "video-mute-state-userlist"; + muteInsert.innerHTML = ''; + insert.appendChild(muteInsert); + } else if (session.rpcs[UUID].voiceMeter) { + insert.appendChild(session.rpcs[UUID].voiceMeter); + } + //getById("userList").innerHTML += "
    "; + added = true; + } + } + + if (!added) { + getById("connectUsers").style.display = "none"; + } else { + getById("connectUsers").style.display = "block"; + } + } catch (e) { } + updateUserListActive = false; + }, 200); +} + +function resetCanvas() { + log("resetCanvas();"); + if (!session.streamSrc) { + checkBasicStreamsExist(); + return; + } + session.streamSrc.getVideoTracks().forEach(track => { + session.canvasSource.width = track.getSettings().width || 1280; + session.canvasSource.height = track.getSettings().height || 720; + }); +} + +function initEffectsImage() { + if (!session.effectsImage) { + if (!session.selectedImage_contents) { + session.selectedImage_contents = getById("selectImage_contents"); + } + if (session.selectedImage_contents.querySelector("img")) { + session.effectsImage = session.selectedImage_contents.querySelector("img"); + session.effectsImage.classList.add("selectedContentEffectsImage"); + } else if (session.defaultBackgroundImages && session.defaultBackgroundImages.length) { + session.effectsImage = document.createElement("img"); + session.effectsImage.onload = function () { + URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory + }; + session.effectsImage.src = session.defaultBackgroundImages[0]; + session.effectsImage.classList.add("selectedContentEffectsImage"); + } else { + session.effectsImage = document.createElement("img"); + session.effectsImage.onload = function () { + URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory + }; + session.effectsImage.src = "./media/bg_sample.webp"; + } + } +} + +var LaunchTFWorkerCallback = false; +function TFLiteWorker() { + if (session.tfliteModule == false) { + LaunchTFWorkerCallback = true; + return; + } + if (TFLITELOADING) { + LaunchTFWorkerCallback = true; + return; + } + LaunchTFWorkerCallback = false; + log("TFLiteWorker() called"); + + initEffectsImage(); + //if (session.tfliteModule.looping){return;} + + const segmentationWidth = 256; + const segmentationHeight = 144; + const segmentationPixelCount = segmentationWidth * segmentationHeight; + const inputMemoryOffset = session.tfliteModule._getInputMemoryOffset() / 4; + const outputMemoryOffset = session.tfliteModule._getOutputMemoryOffset() / 4; + const segmentationMask = new ImageData(segmentationWidth, segmentationHeight); + const segmentationMaskCanvas = document.createElement("canvas"); + segmentationMaskCanvas.width = segmentationWidth; + segmentationMaskCanvas.height = segmentationHeight; + const segmentationMaskCtx = segmentationMaskCanvas.getContext("2d", { alpha: true, willReadFrequently: true }); + session.tfliteModule.nowTime = new Date().getTime(); + session.tfliteModule.offsetTime = 0; + + var slow = 0; + var slower = false; + + async function process() { + if (!(session.effect == "3" || session.effect == "4" || session.effect == "5")) { + //session.tfliteModule.looping=false; + errorlog("shouldn't happen"); + return; + } + + if (session.tfliteModule.activelyProcessing) { + return; + } + session.tfliteModule.activelyProcessing = true; + + if (session.mobile) { + if (screenWidth !== window.innerWidth) { + screenWidth = window.innerWidth; + + //session.tfliteModule.looping=false; + session.tfliteModule.activelyProcessing = false; + + setTimeout(function () { + updateRenderOutpipe(); + }, 200); + return; + } + } + + try { + segmentationMaskCtx.filter = "none"; + segmentationMaskCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, segmentationWidth, segmentationHeight); + + const imageData = segmentationMaskCtx.getImageData(0, 0, segmentationWidth, segmentationHeight); + + for (let i = 0; i < segmentationPixelCount; i++) { + session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3] = imageData.data[i * 4] / 255; + session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 1] = imageData.data[i * 4 + 1] / 255; + session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 2] = imageData.data[i * 4 + 2] / 255; + } + + session.tfliteModule._runInference(); + + for (let i = 0; i < segmentationPixelCount; i++) { + const background = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2]; + const person = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2 + 1]; + const shift = Math.max(background, person); + const backgroundExp = Math.exp(background - shift); + const personExp = Math.exp(person - shift); + segmentationMask.data[i * 4 + 3] = Math.min(Math.pow((255 * personExp) / (backgroundExp + personExp), 1.5) - 10, 255); // softmax + } + + segmentationMaskCtx.putImageData(segmentationMask, 0, 0); + + session.canvasCtx.globalCompositeOperation = "copy"; + if ((session.mobile && !session.flagship) || slower) { + session.canvasCtx.filter = "blur(4px)"; + } else { + session.canvasCtx.filter = "blur(8px)"; + } + + session.canvasCtx.drawImage(segmentationMaskCanvas, 0, 0, segmentationWidth, segmentationHeight, 0, 0, session.canvasSource.width, session.canvasSource.height); + + session.canvasCtx.globalCompositeOperation = "source-in"; + session.canvasCtx.filter = "none"; + session.canvasCtx.drawImage(session.canvasSource, 0, 0); + + session.canvasCtx.globalCompositeOperation = "destination-over"; + + if (session.effect == "4") { + // greenscreen + session.canvasCtx.filter = "none"; + session.canvasCtx.fillStyle = "#0F0"; + session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height); + } else if (session.effect == "5") { + session.canvasCtx.filter = "none"; + if (session.effectsImage.complete) { + try { + session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height); + } catch (e) { } + } + } else if (session.effect == "3") { + // BLUR + if (session.effectValue) { + session.canvasCtx.filter = "blur(" + parseInt(session.effectValue) * 2 + "px)"; + } else { + session.canvasCtx.filter = "blur(4px)"; // Does not work on Safari + } + session.canvasCtx.drawImage(session.canvasSource, 0, 0); + session.canvasCtx.filter = "none"; + } else { + session.tfliteModule.activelyProcessing = false; + //session.tfliteModule.looping=false; + return; + } + + ////// + } catch (e) { + errorlog(e); + session.tfliteModule.activelyProcessing = false; + //session.tfliteModule.looping=false; + return; + } + session.tfliteModule.lastTime = session.tfliteModule.nowTime; + session.tfliteModule.nowTime = new Date().getTime(); + + var time = 30 - (session.tfliteModule.nowTime - session.tfliteModule.lastTime || 0); + time = time + (session.tfliteModule.offsetTime || 0); + session.tfliteModule.activelyProcessing = false; + + slow -= 1; + + if (time <= 0) { + if (time < -40) { + slow += 1; + if (slow > 100) { + slower = true; + } + } + session.tfliteModule.offsetTime = 0; + } else { + slow -= 2; + session.tfliteModule.offsetTime = time || 0; + } + } + + async function processiOS() { + if (!(session.effect == "3" || session.effect == "4" || session.effect == "5")) { + errorlog("shouldn't happen"); + //session.tfliteModule.looping=false; + return; + } + if (session.tfliteModule.activelyProcessing) { + return; + } + session.tfliteModule.activelyProcessing = true; + + if (screenWidth !== window.innerWidth) { + screenWidth = window.innerWidth; + setTimeout(function () { + updateRenderOutpipe(); + }, 200); + //session.tfliteModule.looping=false; + session.tfliteModule.activelyProcessing = false; + return; + } + + try { + segmentationMaskCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, segmentationWidth, segmentationHeight); + + var imageData = segmentationMaskCtx.getImageData(0, 0, segmentationWidth, segmentationHeight); + + for (let i = 0; i < segmentationPixelCount; i++) { + session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3] = imageData.data[i * 4] / 255; + session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 1] = imageData.data[i * 4 + 1] / 255; + session.tfliteModule.HEAPF32[inputMemoryOffset + i * 3 + 2] = imageData.data[i * 4 + 2] / 255; + } + + session.tfliteModule._runInference(); + + for (let i = 0; i < segmentationPixelCount; i++) { + const background = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2]; + const person = session.tfliteModule.HEAPF32[outputMemoryOffset + i * 2 + 1]; + const shift = Math.max(background, person); + const backgroundExp = Math.exp(background - shift); + const personExp = Math.exp(person - shift); + segmentationMask.data[i * 4 + 3] = 255 - (255 * personExp) / (backgroundExp + personExp); // softmax + } + + segmentationMaskCtx.putImageData(segmentationMask, 0, 0); + + session.canvasCtx.globalCompositeOperation = "copy"; + session.canvasCtx.drawImage(session.canvasSource, 0, 0); + + session.canvasCtx.globalCompositeOperation = "destination-out"; + session.canvasCtx.drawImage(segmentationMaskCanvas, 0, 0, segmentationWidth, segmentationHeight, 0, 0, session.canvasSource.width, session.canvasSource.height); + + session.canvasCtx.globalCompositeOperation = "destination-over"; + + if (session.effect == "4") { + // greenscreen + session.canvasCtx.fillStyle = "#0F0"; + session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height); + } else if (session.effect == "5") { + if (session.effectsImage.complete) { + try { + session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height); + } catch (e) { } + } + } else if (session.effect == "3") { + // BLUR + + const width = canvasBG.width; + const height = canvasBG.height; + ctxBG.drawImage(session.canvasSource, 0, 0, width, height); + imageData = ctxBG.getImageData(0, 0, width, height); + + const { data } = imageData; + + // THE BELOW BLUR CODE polyfil is by David Enke + // MIT License: Copyright (c) 2019 + // https://github.com/steveseguin/context-filter-polyfill/blob/master/src/filters/blur.filter.ts + const wm = width - 1; + const hm = height - 1; + const rad1 = amount + 1; + const r = []; + const g = []; + const b = []; + //const a = []; + + const vmin = []; + const vmax = []; + + let iterations = 3; // 1 - 3 + let p, p1, p2; + while (iterations-- > 0) { + let yw = 0; + let yi = 0; + + for (let y = 0; y < height; y++) { + let rsum = data[yw] * rad1; + let gsum = data[yw + 1] * rad1; + let bsum = data[yw + 2] * rad1; + for (let i = 1; i <= amount; i++) { + p = yw + ((i > wm ? wm : i) << 2); + rsum += data[p++]; + gsum += data[p++]; + bsum += data[p++]; + } + for (let x = 0; x < width; x++) { + r[yi] = rsum; + g[yi] = gsum; + b[yi] = bsum; + if (y === 0) { + vmin[x] = ((p = x + rad1) < wm ? p : wm) << 2; + vmax[x] = (p = x - amount) > 0 ? p << 2 : 0; + } + p1 = yw + vmin[x]; + p2 = yw + vmax[x]; + rsum += data[p1++] - data[p2++]; + gsum += data[p1++] - data[p2++]; + bsum += data[p1++] - data[p2++]; + yi++; + } + yw += width << 2; + } + + for (let x = 0; x < width; x++) { + let yp = x; + let rsum = r[yp] * rad1; + let gsum = g[yp] * rad1; + let bsum = b[yp] * rad1; + for (let i = 1; i <= amount; i++) { + yp += i > hm ? 0 : width; + rsum += r[yp]; + gsum += g[yp]; + bsum += b[yp]; + } + yi = x << 2; + for (let y = 0; y < height; y++) { + data[yi] = (rsum * mulSum) >>> shgSum; + data[yi + 1] = (gsum * mulSum) >>> shgSum; + data[yi + 2] = (bsum * mulSum) >>> shgSum; + if (x === 0) { + vmin[y] = ((p = y + rad1) < hm ? p : hm) * width; + vmax[y] = (p = y - amount) > 0 ? p * width : 0; + } + p1 = x + vmin[y]; + p2 = x + vmax[y]; + rsum += r[p1] - r[p2]; + gsum += g[p1] - g[p2]; + bsum += b[p1] - b[p2]; + yi += width << 2; + } + } + } + ////////////// END OF BLUR CODE - MIT LICENCED. + ctxBG.putImageData(imageData, 0, 0); + session.canvasCtx.drawImage(canvasBG, 0, 0, width, height, 0, 0, session.canvas.width, session.canvas.height); + } else { + session.tfliteModule.activelyProcessing = false; + //session.tfliteModule.looping=false; + return; + } + } catch (e) { + session.tfliteModule.activelyProcessing = false; + //session.tfliteModule.looping=false; + errorlog(e); + return; + } + + session.tfliteModule.lastTime = session.tfliteModule.nowTime; + session.tfliteModule.nowTime = new Date().getTime(); + + var time = 30 - (session.tfliteModule.nowTime - session.tfliteModule.lastTime || 0); + time = time + (session.tfliteModule.offsetTime || 0); + + slow -= 1; + if (time <= 0) { + if (time < -40) { + slow += 1; + if (slow > 100) { + slower = true; + } + } + session.tfliteModule.offsetTime = 0; + } else { + slow -= 2; + session.tfliteModule.offsetTime = time || 0; + } + session.tfliteModule.activelyProcessing = false; + } + //session.tfliteModule.looping=true; + + var screenWidth = window.innerWidth; + + if (iOS || iPad || SafariVersion) { + var canvasBG = document.createElement("canvas"); + var ctxBG = canvasBG.getContext("2d", { alpha: false }); + var amount = 1.0; + var mulTable = [1, 57, 41, 21, 203, 34, 97, 73, 227, 91, 149, 62, 105, 45, 39, 137, 241, 107, 3, 173, 39, 71, 65, 238, 219, 101, 187, 87, 81, 151, 141, 133, 249, 117, 221, 209, 197, 187, 177, 169, 5, 153, 73, 139, 133, 127, 243, 233, 223, 107, 103, 99, 191, 23, 177, 171, 165, 159, 77, 149, 9, 139, 135, 131, 253, 245, 119, 231, 224, 109, 211, 103, 25, 195, 189, 23, 45, 175, 171, 83, 81, 79, 155, 151, 147, 9, 141, 137, 67, 131, 129, 251, 123, 30, 235, 115, 113, 221, 217, 53, 13, 51, 50, 49, 193, 189, 185, 91, 179, 175, 43, 169, 83, 163, 5, 79, 155, 19, 75, 147, 145, 143, 35, 69, 17, 67, 33, 65, 255, 251, 247, 243, 239, 59, 29, 229, 113, 111, 219, 27, 213, 105, 207, 51, 201, 199, 49, 193, 191, 47, 93, 183, 181, 179, 11, 87, 43, 85, 167, 165, 163, 161, 159, 157, 155, 77, 19, 75, 37, 73, 145, 143, 141, 35, 138, 137, 135, 67, 33, 131, 129, 255, 63, 250, 247, 61, 121, 239, 237, 117, 29, 229, 227, 225, 111, 55, 109, 216, 213, 211, 209, 207, 205, 203, 201, 199, 197, 195, 193, 48, 190, 47, 93, 185, 183, 181, 179, 178, 176, 175, 173, 171, 85, 21, 167, 165, 41, 163, 161, 5, 79, 157, 78, 154, 153, 19, 75, 149, 74, 147, 73, 144, 143, 71, 141, 140, 139, 137, 17, 135, 134, 133, 66, 131, 65, 129, 1]; + var mulSum = mulTable[amount]; + var shgTable = [0, 9, 10, 10, 14, 12, 14, 14, 16, 15, 16, 15, 16, 15, 15, 17, 18, 17, 12, 18, 16, 17, 17, 19, 19, 18, 19, 18, 18, 19, 19, 19, 20, 19, 20, 20, 20, 20, 20, 20, 15, 20, 19, 20, 20, 20, 21, 21, 21, 20, 20, 20, 21, 18, 21, 21, 21, 21, 20, 21, 17, 21, 21, 21, 22, 22, 21, 22, 22, 21, 22, 21, 19, 22, 22, 19, 20, 22, 22, 21, 21, 21, 22, 22, 22, 18, 22, 22, 21, 22, 22, 23, 22, 20, 23, 22, 22, 23, 23, 21, 19, 21, 21, 21, 23, 23, 23, 22, 23, 23, 21, 23, 22, 23, 18, 22, 23, 20, 22, 23, 23, 23, 21, 22, 20, 22, 21, 22, 24, 24, 24, 24, 24, 22, 21, 24, 23, 23, 24, 21, 24, 23, 24, 22, 24, 24, 22, 24, 24, 22, 23, 24, 24, 24, 20, 23, 22, 23, 24, 24, 24, 24, 24, 24, 24, 23, 21, 23, 22, 23, 24, 24, 24, 22, 24, 24, 24, 23, 22, 24, 24, 25, 23, 25, 25, 23, 24, 25, 25, 24, 22, 25, 25, 25, 24, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 23, 25, 23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 24, 22, 25, 25, 23, 25, 25, 20, 24, 25, 24, 25, 25, 22, 24, 25, 24, 25, 24, 25, 25, 24, 25, 25, 25, 25, 22, 25, 25, 25, 24, 25, 24, 25, 18]; + var shgSum = shgTable[amount]; + + log("session.canvas: " + session.canvas.width + "x" + session.canvas.height); + canvasBG.width = parseInt(session.canvas.width / 12); + canvasBG.height = parseInt(session.canvas.height / 12); + ctxBG.width = canvasBG.width; + ctxBG.height = canvasBG.height; + try { + session.tfliteModule.stopOscillator = setupOscillator(processiOS, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + errorlog(e); + session.tfliteModule.stopOscillator = setupOscillator(processiOS, 30); + } + } else { + try { + session.tfliteModule.stopOscillator = setupOscillator(process, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + errorlog(e); + session.tfliteModule.stopOscillator = setupOscillator(process, 30); + } + } +} + +var insertableStreamWorker = null; + +function setupSenderTransform(sender, UUID = false) { + if (!insertableStreamWorker) { + insertableStreamWorker = new Worker("./insertableStreamWorker.js", { name: "Insertable Stream worker" }); + insertableStreamWorker.onmessage = event => { + if (event.data === "insertableStreamWorkerLoaded") { + if (session.encodedInsertableStreams == "e2ee") { + if (session.password) { + insertableStreamWorker.postMessage({ cryptoPhrase: session.password + session.salt + "aDdedSaLt123" }); // salt ⚔️ rainbow + } else { + insertableStreamWorker.postMessage({ cryptoKey: "aabbccddeeff00112233445566778899" }); + } + } else if (session.encodedInsertableStreams == "lyra") { + insertableStreamWorker.postMessage({ + lyraCodecModule: session.lyraCodecModule + }); + } + } else { + console.log(event.data); + } + }; + } + + try { + const senderStreams = sender.createEncodedStreams(); + const { readable, writable } = senderStreams; + let operation = "pass"; + + if (session.encodedInsertableStreams == "e2ee") { + operation = "encode"; + } else if (session.encodedInsertableStreams == "red") { + operation = "redencode"; + } else if (session.encodedInsertableStreams == "lyra" && sender.track && sender.track.kind === "audio") { + operation = "lyraencode"; + } else if (UUID && session.pcs[UUID] && session.pcs[UUID].preferAudioCodec == "lyra" && sender.track && sender.track.kind === "audio") { + operation = "lyraencode"; + } + + insertableStreamWorker.postMessage( + { + operation: operation, + readable, + writable + }, + [readable, writable] + ); + } catch (e) { + errorlog(e); + } +} + +function setupReceiverTransform(receiver, UUID = false) { + if (!insertableStreamWorker) { + insertableStreamWorker = new Worker("./insertableStreamWorker.js", { name: "Insertable Stream worker" }); + insertableStreamWorker.onmessage = event => { + if (event.data === "insertableStreamWorkerLoaded") { + if (session.encodedInsertableStreams == "e2ee") { + if (session.password) { + insertableStreamWorker.postMessage({ cryptoPhrase: session.password + session.salt + "aDdedSaLt123" }); // salt ⚔️ rainbow + } else { + insertableStreamWorker.postMessage({ cryptoKey: "aabbccddeeff00112233445566778899" }); + } + } else if (session.encodedInsertableStreams == "lyra") { + insertableStreamWorker.postMessage({ + lyraCodecModule: session.lyraCodecModule + }); + } + } else { + console.log(event.data); + } + }; + } + + try { + let operation = "pass"; + + if (session.encodedInsertableStreams == "e2ee") { + operation = "decode"; + } else if (session.encodedInsertableStreams == "red") { + operation = "reddecode"; + } else if (session.encodedInsertableStreams == "lyra" && receiver.track && receiver.track.kind === "audio") { + operation = "lyradecode"; + } + + const receiverStreams = receiver.createEncodedStreams(); + const { readable, writable } = receiverStreams; + + insertableStreamWorker.postMessage( + { + operation: operation, + readable, + writable + }, + [readable, writable] + ); + } catch (e) { + errorlog(e); + } +} + +function mainMeshMask() { + if (session.TFJSModel === null || session.TFJSModel === true) { + setTimeout(function () { + mainMeshMask(); + }, 1000); + return; + } + function heatMapColorforValue(value) { + var h = parseInt((1.0 - value) * 240); + if (h < 0) { + h = 0; + } + if (h > 240) { + h = 240; + } + return "hsl(" + h + ", 100%, 50%)"; + } + async function process() { + if (session.TFJSModel.activelyProcessing) { + return; + } + session.TFJSModel.activelyProcessing = true; + + if (session.effect !== "6") { + if (session.TFJSModel.timeoutDraw) { + session.TFJSModel.timeoutDraw(); + session.TFJSModel.timeoutDraw = null; + } + session.TFJSModel.activelyProcessing = false; + return; + } + + const predictions = await session.TFJSModel.estimateFaces({ + input: session.canvasSource + }); + + var output = []; + if (predictions.length > 0) { + for (let j = 0; j < predictions.length; j++) { + const fp = predictions[j].annotations; + session.canvasCtx.fillStyle = "#000000"; + session.canvasCtx.fillRect(0, 0, session.canvas.width, session.canvas.height); + const keypoints = predictions[j].scaledMesh; + for (let i = 0; i < keypoints.length; i++) { + var [x, y, z] = keypoints[i]; + x = parseInt(x); + y = parseInt(y); + z = parseInt(z); + if (session.pushEffectsData) { + output.push(x); + output.push(y); + } + session.canvasCtx.fillStyle = heatMapColorforValue((z + 40) / 60); + session.canvasCtx.fillRect(x, y, 5, 5); + } + } + } + + if (session.pushEffectsData) { + //output = FastIntegerCompression.compress(output); + //log(output); + if (isIFrame) { + parent.postMessage( + { + effectsData: output, + eID: session.pushEffectsData + }, + session.iframetarget + ); + } else { + for (var i in session.pcs) { + if (!session.pcs[i].sendChannel.bufferedAmount) { + // don't overload things. + session.sendMessage({ effectsData: output, eID: session.effect }, i); + } + } + } + } + + if (!session.TFJSModel.timeoutDraw) { + try { + session.TFJSModel.timeoutDraw = setupOscillator(process, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + session.TFJSModel.timeoutDraw = setupOscillator(process, 30); // setTimeout(function(){draw();},33); + } + } + session.TFJSModel.activelyProcessing = false; + } + process(); +} + +var faceDetector = false; +var faceAlignment = false; +var activeDetection = false; + +function drawFace() { + if (session.effect !== "1") { + return; + } + if (faceAlignment) { + faceAlignment(); + return; + } else if (faceAlignment === null) { + return; + } + faceAlignment = null; + + var timers = {}; + timers.activelyProcessingDraw = false; + + var ctx = session.canvasCtx; + + function fde1() { + warnlog("LOADED drawFace()"); + + var lastFace = {}; + + //session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; + //session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; + + lastFace.x = session.canvasSource.width / 2; + lastFace.y = session.canvasSource.height / 2; + lastFace.w = session.canvasSource.width; + lastFace.h = session.canvasSource.height; + + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + function detectFace() { + if (activeDetection) { + return; + } + activeDetection = true; + if (session.effect !== "1") { + return; + } + try { + faceDetector + .detect(session.canvasSource) + .then(faces => { + if (faces.length) { + for (let face of faces) { + lastFace.x = face.boundingBox.x; + lastFace.y = face.boundingBox.y; + lastFace.w = face.boundingBox.width; + lastFace.h = face.boundingBox.height; + break; + } + } + //setTimeout(function(){draw();},0); + }) + .catch(e => { + errorlog("Boo, Face Detection failed: " + e); + }); + } catch (e) { } + setTimeout(function () { + detectFace(); + }, 200); + activeDetection = false; + } + + var wh = null; + var xa = null; + var ya = null; + + function draw() { + if (timers.activelyProcessingDraw) { + return; + } + timers.activelyProcessingDraw = true; + + if (session.effect !== "1") { + timers.activelyProcessingDraw = false; + if (timers.timeoutDraw) { + timers.timeoutDraw(); + timers.timeoutDraw = null; + } + return; + } + + try { + if (!session.canvasSource.width) { + timers.activelyProcessingDraw = false; + return; + } + if (wh === null && session.canvasSource.width) { + wh = Math.pow((session.canvasSource.width * session.canvasSource.width) / 36, 0.5); + + xa = 0; + ya = 0; + } + + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + if (lastFace.w) { + wh = wh * 0.999 + Math.pow(lastFace.w * lastFace.h, 0.5) * 0.001; + + var w = wh * 6; + + if (w > session.canvasSource.width) { + w = session.canvasSource.width; + } + if (session.canvasSource.height > session.canvasSource.width) { + if (w > session.canvasSource.height && session.canvasSource.height > session.canvasSource.width) { + w = session.canvasSource.height; + } + } else if (w > session.canvasSource.width) { + w = session.canvasSource.width; + } + + var h = (w / session.canvasSource.width) * session.canvasSource.height; + + xa = xa * 0.998 + 0.002 * (lastFace.x + lastFace.w / 2); + ya = ya * 0.998 + 0.002 * (lastFace.y + lastFace.h / 2); + + var x = xa - w / 2; + var y = ya - h / 2; + + if (x < 0) { + x = 0; + } + if (y < 0) { + y = 0; + } + + if (x > session.canvasSource.width - w) { + x = session.canvasSource.width - w; + } + if (y > session.canvasSource.height - h) { + y = session.canvasSource.height - h; + } + + if (x < 0) { + x = 0; + } + if (y < 0) { + y = 0; + } + } + //console.log(x, y, w, h, session.canvasSource.width, session.canvasSource.height); + ctx.drawImage(session.canvasSource, x, y, w, h, 0, 0, session.canvasSource.width, session.canvasSource.height); + //ctx.beginPath(); + //ctx.rect(lastFace.x, lastFace.y, lastFace.w, lastFace.h); + // ctx.stroke(); + } catch (e) { } + + if (!timers.timeoutDraw) { + try { + timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33); + } + } else { + var res = timers.timeoutDraw("check"); + if (res) { + try { + timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33); + } + } + } + timers.activelyProcessingDraw = false; + } + + if (window.FaceDetector == undefined) { + if (!session.cleanOutput) { + warnUser("Face Detection API not detected.\n\nYou may be able to enable it here: chrome://flags/#enable-experimental-web-platform-features"); + } + faceDetector = false; + } else { + faceDetector = new FaceDetector(); + } + + function fde2() { + if (!timers.activelyProcessingDraw) { + draw(); + } + if (!activeDetection) { + detectFace(); + } + } + fde2(); + return fde2; + } + faceAlignment = fde1(); +} +//////// END CANVAS EFFECTS /////////////////// + +var getFacesActive = false; +async function getFaces() { + if (getFacesActive) { + return; + } + getFacesActive = true; + + if (session.grabFaceData) { + if (!faceDetector) { + if (window.FaceDetector == undefined) { + if (!session.cleanOutput) { + warnUser("Face Detection API not detected.\n\nYou may be able to enable it here: chrome://flags/#enable-experimental-web-platform-features"); + } + session.grabFaceData = false; + faceDetector = false; + + getFacesActive = false; + return; + } else { + session.grabFaceData = 1; + faceDetector = new FaceDetector(); + } + } + try { + var videos = {}; + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement) { + await faceDetector + .detect(session.rpcs[UUID].videoElement) + .then(faces => { + videos[session.rpcs[UUID].streamID] = {}; + videos[session.rpcs[UUID].streamID].videoWidth = session.rpcs[UUID].videoElement.videoWidth; + videos[session.rpcs[UUID].streamID].videoHeight = session.rpcs[UUID].videoElement.videoHeight; + videos[session.rpcs[UUID].streamID].faces = faces; + }) + .catch(e => { + //errorlog("Boo, Face Detection failed: " + e); + }); + } + } + log(videos); + } catch (e) { } + pokeIframeAPI("face-tracking-data", videos); + setTimeout(function () { + getFaces(); + }, 200); + } + + getFacesActive = false; +} + +////// + +var simpleDrawMain = false; +function simpleDraw(reinit = false) { + let supported = ["8", "overlay"]; + if (!supported.includes(session.effect)) { + errorlog("not a valid effeect?"); + return; + } + if (simpleDrawMain) { + simpleDrawMain(reinit); + return; + } else if (simpleDrawMain === null) { + return; + } + simpleDrawMain = null; + + var timers = {}; + timers.activelyProcessingDraw = false; + + function fde1() { + try { + log("LOADED simpleDraw()"); + + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + function draw() { + if (timers.activelyProcessingDraw) { + return; + } + timers.activelyProcessingDraw = true; + if (!supported.includes(session.effect)) { + if (timers.timeoutDraw) { + timers.timeoutDraw(); + timers.timeoutDraw = null; + } + timers.activelyProcessingDraw = false; + return; + } + try { + if (!session.canvasSource.width) { + timers.activelyProcessingDraw = false; + return; + } + + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + session.canvasCtx.drawImage(session.canvasSource, 0, 0, session.canvasSource.width, session.canvasSource.height, 0, 0, session.canvasSource.width, session.canvasSource.height); + + if (session.effect === "overlay" && session.foregroundImg && session.foregroundImg.complete) { + session.canvasCtx.drawImage(session.foregroundImg, 0, 0, session.canvas.width, session.canvas.height); + } + } catch (e) { + errorlog(e); + } + + if (!timers.timeoutDraw) { + try { + timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33); + } + } else { + var res = timers.timeoutDraw("check"); + if (res) { + try { + timers.timeoutDraw = setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + timers.timeoutDraw = setupOscillator(draw, 40); // setTimeout(function(){draw();},33); + } + } + } + timers.activelyProcessingDraw = false; + } + } catch (e) { + errorlog(e); + timers.activelyProcessingDraw = false; + } + + function fde2(reinit = false) { + if (reinit) { + if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) { + session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; + session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; + } + } + if (!timers.activelyProcessingDraw) { + draw(); + } + } + fde2(); + + return fde2; + } + simpleDrawMain = fde1(); +} +//////// END CANVAS EFFECTS /////////////////// +////// +function changeZoomPosition(event, ele) { + const value = parseFloat(ele.value); + + // Determine which slider was moved and update its pair + if (ele.id === "zoomPositionX1" || ele.id === "zoomPositionX") { + xPosition = value; + // Update other horizontal slider + const otherSlider = ele.id === "zoomPositionX1" ? + getById("zoomPositionX") : + getById("zoomPositionX1"); + if (otherSlider) { + otherSlider.value = value; + } + } else if (ele.id === "zoomPositionY1" || ele.id === "zoomPositionY") { + yPosition = value; + // Update other vertical slider + const otherSlider = ele.id === "zoomPositionY1" ? + getById("zoomPositionY") : + getById("zoomPositionY1"); + if (otherSlider) { + otherSlider.value = value; + } + } +} + +var xPosition = 0.5; // Center position horizontally (0 to 1) +var yPosition = 0.5; // Center position vertically (0 to 1) +var digitalZoomMain = false; +function digitalZoom(resetZoom = false) { + if (session.effect !== "7") { + return; + } + if (digitalZoomMain) { + digitalZoomMain(resetZoom); + return; + } else if (digitalZoomMain === null) { + return; + } + digitalZoomMain = null; + + var activelyProcessingDraw = false; + + function fde1() { + try { + warnlog("LOADED digitalZoom()"); + + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + var xa = null; + var ya = null; + var zz = 1; + + // Modify the draw function to use the position values + function draw() { + if (activelyProcessingDraw) { + return; + } + activelyProcessingDraw = true; + + if (session.effect !== "7") { + zz = 1.0; + xa = 0; + ya = 0; + activelyProcessingDraw = false; + return; + } + + try { + if (!session.canvasSource) { + activelyProcessingDraw = false; + return; + } + + // Use videoWidth/videoHeight for actual current dimensions + const srcWidth = session.canvasSource.videoWidth || session.canvasSource.width; + const srcHeight = session.canvasSource.videoHeight || session.canvasSource.height; + + if (!srcWidth) { + activelyProcessingDraw = false; + return; + } + + session.canvas.height = 2 * parseInt(srcHeight / 2); + session.canvas.width = 2 * parseInt(srcWidth / 2); + + if (session.effectValue) { + // Smooth out the zoom factor + zz = 0.9 * zz + session.effectValue * 0.1; + // Calculate the scaled dimensions + const scaledWidth = srcWidth / zz; + const scaledHeight = srcHeight / zz; + // Calculate the offset based on position sliders + // This centers the zoom on the selected position + xa = (srcWidth - scaledWidth) * xPosition; + ya = (srcHeight - scaledHeight) * yPosition; + // Draw the zoomed region + session.canvasCtx.drawImage( + session.canvasSource, + xa, ya, + scaledWidth, + scaledHeight, + 0, 0, + srcWidth, + srcHeight + ); + } else { + // If no zoom, draw the full image + session.canvasCtx.drawImage( + session.canvasSource, + 0, 0, + srcWidth, + srcHeight, + 0, 0, + srcWidth, + srcHeight + ); + } + } catch (e) { + errorlog(e); + } + activelyProcessingDraw = false; + } + } catch (e) { + errorlog(e); + activelyProcessingDraw = false; + } + + function fde2(resetZoom = false) { + if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) { + session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; + session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; + } + + if (resetZoom) { + xa = null; + ya = null; + zz = 1; + } + + try { + setupOscillator(draw, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + setupOscillator(draw, 40); // setTimeout(function(){draw();},33); + } + + draw(); + } + + fde2(); + + return fde2; + } + digitalZoomMain = fde1(); + digitalZoomMain(resetZoom); +} + +function rgbToHsv(r, g, b) { + r /= 255; g /= 255; b /= 255; + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h, s, v = max; + const d = max - min; + s = max === 0 ? 0 : d / max; + + if (max === min) { + h = 0; + } else { + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return [h, s, v]; +} + +// real green scree filter +var chromaKeyMain = false; +function chromaKey(reinit = false) { + let supported = ["14", "15"]; + if (!supported.includes(session.effect)) { + warnlog("not a valid effect"); + return; + } + if (chromaKeyMain) { + chromaKeyMain(reinit); + return; + } else if (chromaKeyMain === null) { + return; + } + chromaKeyMain = null; + var timers = {}; + timers.activelyProcessingDraw = false; + + function fde1() { + try { + log("LOADED chromaKey()"); + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + + function processChromaKey() { + if (timers.activelyProcessingDraw) return; + timers.activelyProcessingDraw = true; + + if (!supported.includes(session.effect)) { + if (timers.timeoutDraw) { + timers.timeoutDraw(); + timers.timeoutDraw = null; + } + timers.activelyProcessingDraw = false; + return; + } + + try { + if (!session.canvasSource.width) { + timers.activelyProcessingDraw = false; + return; + } + + session.canvas.height = 2 * parseInt(session.canvasSource.height / 2); + session.canvas.width = 2 * parseInt(session.canvasSource.width / 2); + session.canvasCtx.drawImage(session.canvasSource, 0, 0); + + const imageData = session.canvasCtx.getImageData(0, 0, session.canvas.width, session.canvas.height); + const data = imageData.data; + + const threshold = parseInt(session.effectValue) || 80; + const smoothing = 15; + + // Green range in HSV (normalized to 0-1) + const targetHue = 0.33; // Pure green + const hueRange = 0.15; // Range around pure green + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + // Quick pre-check for obvious non-green pixels + if (g <= r || g <= b) { + continue; + } + const [h, s, v] = rgbToHsv(r, g, b); + // Calculate how "green" the pixel is + const hueDiff = Math.abs(h - targetHue); + const isInGreenRange = hueDiff <= hueRange || hueDiff >= (1 - hueRange); + if (!isInGreenRange) { + continue; + } + // Green intensity calculation + const greenDominance = (g - Math.max(r, b)) / 255; + const saturationBoost = s * 0.7; // Reduce impact of washed-out greens + const keyValue = (greenDominance * 0.6 + saturationBoost * 0.4) * 100; + let alpha = 255; + if (keyValue > threshold) { + const smoothFactor = Math.min((keyValue - threshold) / smoothing, 1); + alpha = (1 - smoothFactor) * 255; + } + data[i + 3] = alpha; + } + + session.canvasCtx.putImageData(imageData, 0, 0); + + if (session.effect === "15" && session.effectsImage && session.effectsImage.complete) { + session.canvasCtx.globalCompositeOperation = 'destination-over'; + session.canvasCtx.drawImage(session.effectsImage, 0, 0, session.canvas.width, session.canvas.height); + session.canvasCtx.globalCompositeOperation = 'source-over'; + } + } catch (e) { + errorlog(e); + } + + if (!timers.timeoutDraw) { + try { + timers.timeoutDraw = setupOscillator(processChromaKey, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + timers.timeoutDraw = setupOscillator(processChromaKey, 40); + } + } else { + var res = timers.timeoutDraw("check"); + if (res) { + try { + timers.timeoutDraw = setupOscillator(processChromaKey, session.canvasSource.srcObject.getVideoTracks()[0].getSettings().frameRate || 30); + } catch (e) { + timers.timeoutDraw = setupOscillator(processChromaKey, 40); + } + } + } + timers.activelyProcessingDraw = false; + } + } catch (e) { + errorlog(e); + timers.activelyProcessingDraw = false; + } + + function fde2(reinit = false) { + if (reinit) { + if (session.canvasSource && session.canvasSource.srcObject && session.canvasSource.srcObject.getVideoTracks().length) { + session.canvasSource.width = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().width || 1280; + session.canvasSource.height = session.canvasSource.srcObject.getVideoTracks()[0].getSettings().height || 720; + } + } + if (!timers.activelyProcessingDraw) { + processChromaKey(); + } + } + fde2(); + return fde2; + } + chromaKeyMain = fde1(); +} +//////// END CANVAS EFFECTS /////////////////// + +function getNativeOutputResolution() { + try { + if (session.videoElement && session.videoElement.srcObject) { + var tracks = session.videoElement.srcObject.getVideoTracks(); + if (tracks.length && tracks[0].getSettings) { + return tracks[0].getSettings(); + } else { + return false; + } + } else if (session.videoElement && session.videoElement.videoWidth && session.videoElement.videoHeight) { + return { width: session.videoElement.videoWidth, height: session.videoElement.videoHeight }; + } + } catch (e) { + return false; + } +} + +function toggleSceneStats(button) { + var UUID = button.dataset.UUID; + + if (button.value == 1) { + button.value = 0; + button.classList.remove("pressed"); + button.ariaPressed = "false"; + if (UUID) { + session.rpcs[UUID].allowGraphs = false; + } else { + session.allowDirectorGraph = false; + } + } else { + button.value = 1; + button.classList.add("pressed"); + button.ariaPressed = "true"; + if (UUID) { + session.rpcs[UUID].allowGraphs = true; + } else { + session.allowDirectorGraph = true; + } + } + + if (UUID) { + var controls = getById("container_" + UUID); + } else { + var controls = getById("container_director"); + } + + if (button.value == 1) { + controls.querySelectorAll("[data-no-scenes]").forEach(ele => { + ele.classList.remove("hidden"); + if (ele.dataset.message) { + ele.innerHTML = "Requesting data .."; + } + }); + + if (controls.querySelector('[data-action-type="stats-graphs-bitrate"]')) { + controls.querySelector('[data-action-type="stats-graphs-bitrate"]').classList.remove("hidden"); + } + if (controls.querySelector('[data-action-type="stats-graphs-details"]')) { + controls.querySelector('[data-action-type="stats-graphs-details"]').classList.remove("hidden"); + } + if (UUID) { + session.sendRequest({ requestStatsContinuous: true }, UUID); + } + } else { + if (UUID) { + session.sendRequest({ requestStatsContinuous: false }, UUID); + } + if (controls.querySelector('[data-action-type="stats-graphs-bitrate"]')) { + controls.querySelector('[data-action-type="stats-graphs-bitrate"]').classList.add("hidden"); + } + if (controls.querySelector('[data-action-type="stats-graphs-details"]')) { + controls.querySelector('[data-action-type="stats-graphs-details"]').classList.add("hidden"); + } + } +} +function getColor(value) { + var hue = (value * 120).toString(10); + return ["hsl(", hue, ",100%,50%)"].join(""); +} + +function plotData(info, UUID, uuid) { + // type = "bitrate" or "nacks" + log("plot data"); + + var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-bitrate"]'); + + if (!container) { + log("container not found"); + return; + } + var canvas = getById("container_" + UUID).querySelector('canvas[data-uid="' + uuid + '"]'); + var canvasNew = false; + if (!canvas) { + canvasNew = true; + canvas = document.createElement("canvas"); + canvas.height = 50; + canvas.width = 124; + canvas.className = "canvasStats"; + canvas.history_nacks = []; + canvas.history_bitrate = []; + canvas.target = 4000; + if (info.scene) { + canvas.title = "Scene: " + info.scene + ". Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments."; + } else if (info.label) { + canvas.title = "Label: " + info.label + ". Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments"; + } else { + canvas.title = "Red/orange implies packet loss. Y-axis is marked with 2500-kbps increments"; + } + + canvas.dataset.uid = uuid; + container.appendChild(canvas); + } + + selfDestructElement(UUID, uuid); + + var context = canvas.getContext("2d"); + + var bitrate = 0; + if ("video_bitrate_kbps" in info) { + bitrate = info.video_bitrate_kbps; + } + if (isNaN(bitrate)) { + bitrate = 0; + } + + if (bitrate < 0) { + bitrate = 0; + } + + var nacks = 0; + if ("nacks_per_second" in info) { + nacks = info.nacks_per_second; + } + if (isNaN(nacks)) { + nacks = 0; + } + if (nacks < 0) { + nacks = 0; + } + + var height = context.canvas.height; + var width = context.canvas.width; + + canvas.history_nacks.push(nacks); + canvas.history_bitrate.push(bitrate); + + canvas.history_nacks = canvas.history_nacks.slice(-125); + canvas.history_bitrate = canvas.history_bitrate.slice(-125); + + var maxBitrate = Math.max(...canvas.history_bitrate); + + var target = canvas.target || 4000; + if (target && maxBitrate > target) { + canvas.target = maxBitrate * 1.5; // set it higher than it needs to be, so it doens't jump around a lot + var yScale = height / canvas.target; + context.clearRect(0, 0, width, height); + var x = width - 1; + var w = 1; + + for (var i = 0; i < canvas.history_bitrate.length; i++) { + var nacks = canvas.history_nacks[i]; + var bitrate = canvas.history_bitrate[i]; + + var val = (10 - nacks) / 10; + if (val > 1) { + val = 1; + } else if (val < 0) { + val = 0; + } + var color = getColor(val); + var y = height - bitrate * yScale; + context.fillStyle = color; + context.fillRect(x, y, w, height); + context.fillStyle = "#DDD5"; + context.fillRect(x, y - 2, w, 4); + + if (y - 5 > 0) { + context.fillStyle = "#FFF3"; + context.fillRect(x, y + 2, w, 1); + } + + var imageData = context.getImageData(1, 0, width - 1, height); + context.putImageData(imageData, 0, 0); + context.clearRect(width - 1, 0, 1, height); + } + + for (var tt = 2500; tt < canvas.target; tt += 2500) { + var y = parseInt(height - tt * yScale); + context.fillStyle = "#0555"; + context.fillRect(0, y, width, 1); + } + log("finished plotting a new y-axis"); + return; + } + //if (info.available_outgoing_bitrate_kbps){ + // limit target, but requires a history + //} + var val = (10 - nacks) / 10; + if (val > 1) { + val = 1; + } else if (val < 0) { + val = 0; + } + var color = getColor(val); + + var yScale = height / target; + + var x = width - 1; + var y = height - bitrate * yScale; + var w = 1; + + context.fillStyle = color; + context.fillRect(x, y, w, height); + context.fillStyle = "#DDD5"; + context.fillRect(x, y - 2, w, 4); + + if (y - 5 > 0) { + context.fillStyle = "#FFF3"; + context.fillRect(x, y + 2, w, 1); + } + + context.fillStyle = "#0555"; + if (canvasNew) { + for (var tt = 2500; tt < target; tt += 2500) { + var y = parseInt(height - tt * yScale); + context.fillRect(0, y, width, 1); + } + } else { + for (var tt = 2500; tt < target; tt += 2500) { + var y = parseInt(height - tt * yScale); + context.fillRect(x, y, 1, 1); + } + } + + var imageData = context.getImageData(1, 0, width - 1, height); + context.putImageData(imageData, 0, 0); + context.clearRect(width - 1, 0, 1, height); + + log("finished plotting"); +} + +function selfDestructElement(UUID, uid) { + getById("container_" + UUID) + .querySelectorAll('[data-uid="' + uid + '"]') + .forEach(ele => { + ele.classList.remove("greyout"); + clearTimeout(ele.selfFadeout); + ele.selfFadeout = setTimeout( + function (ele) { + ele.classList.add("greyout"); + }, + 4000, + ele + ); + + clearTimeout(ele.selfDestruct); + ele.selfDestruct = setTimeout( + function (ele) { + ele.remove(); + }, + 10000, + ele + ); + }); +} + +function directorGraphStats() { + if (!(session.allowDirectorGraph || session.allowGraphs)) { + return; + } + + if (session.director) { + var UUID = "director"; + var maincon = getById("container_director"); + + var sceneStats = {}; + for (var uuid in session.pcs) { + if (session.pcs[uuid].scene !== false) { + sceneStats[uuid] = {}; + sceneStats[uuid].label = session.pcs[uuid].label; + sceneStats[uuid].scene = session.pcs[uuid].scene; + sceneStats[uuid].resolution = session.pcs[uuid].stats.resolution; + sceneStats[uuid].video_bitrate_kbps = session.pcs[uuid].stats.video_bitrate_kbps; + sceneStats[uuid].video_encoder = session.pcs[uuid].stats.video_encoder; + } + } + if (!Object.keys(sceneStats).length) { + maincon.querySelectorAll("[data-no-scenes]").forEach(ele => { + ele.classList.remove("hidden"); + if (ele.dataset.message) { + ele.innerHTML = "No scenes active"; + } + }); + + log("zero size"); + return; + } + maincon.querySelectorAll("[data-no-scenes]").forEach(ele => { + ele.classList.add("hidden"); + }); + + for (var uuid in sceneStats) { + var container = maincon.querySelector('[data-action-type="stats-graphs-details-container"][data-uid="' + uuid + '"]'); + if (!container) { + container = maincon.querySelector('[data-action-type="stats-graphs-details-container"]').cloneNode(true); + container.dataset.uid = uuid; + container.classList.remove("hidden"); + maincon.querySelector('[data-action-type="stats-graphs-details"]').appendChild(container); + } + plotData(sceneStats[uuid], UUID, uuid); + + if ("video_bitrate_kbps" in sceneStats[uuid] && sceneStats[uuid].video_bitrate_kbps !== "video_bitrate_kbps") { + var span = container.querySelector("[data-bitrate]"); + if (span) { + span.classList.remove("hidden"); + span.innerHTML = "video bitrate: " + parseInt(sceneStats[uuid].video_bitrate_kbps) + " (kbps)"; + span.style.cursor = "pointer"; + span.title = "Click to adjust bitrate"; + span.onclick = async function (e) { + e.preventDefault(); + e.stopPropagation(); + var currentUUID = this.closest('[data-action-type="stats-graphs-details-container"]').dataset.uid; + const result = await promptAlt("Select target bitrate (kbps)", false, false, false, false, false, false, { + type: 'select', + options: ['50', '500', '1000', '2000', '5000', '10000', '20000', '[Custom]'], + placeholder: 'Enter custom bitrate in kbps' + }); + if (result) { + var msg = { + targetBitrate: parseInt(result), + UUID: currentUUID, + requestAs: uuid + }; + if (isIFrame) { + parent.postMessage(msg, session.iframetarget); + } + session.sendRequest(msg); + } + }; + } + } + + var span = container.querySelector("[data-scene-name]"); + if (span && "label" in sceneStats[uuid] && sceneStats[uuid].label) { + span.classList.remove("hidden"); + span.innerHTML = "stats for viewer: " + sceneStats[uuid].label; + } else if (span && "scene" in sceneStats[uuid] && sceneStats[uuid].scene !== false) { + span.classList.remove("hidden"); + span.innerHTML = "stats for scene: " + sceneStats[uuid].scene; + } else if (uuid === "meshcast") { + span.classList.remove("hidden"); + span.innerHTML = "stats for meshcast ingest"; + span.title = "You can use &label=xxxx to give your view links a unique label"; + } else { + span.classList.remove("hidden"); + span.innerHTML = "stats for some viewer"; + span.title = "You can use &label=xxxx to give your view links a unique label"; + } + + if ("resolution" in sceneStats[uuid]) { + var span = container.querySelector("[data-resolution]"); + if (span) { + span.classList.remove("hidden"); + span.innerHTML = sceneStats[uuid].resolution; + span.style.cursor = "pointer"; + span.title = "Click to adjust resolution"; + span.onclick = async function (e) { + e.preventDefault(); + e.stopPropagation(); + var currentUUID = this.closest('[data-action-type="stats-graphs-details-container"]').dataset.uid; + const result = await promptAlt("Select target resolution", false, false, false, false, false, false, { + type: 'select', + options: ['360', '720', '1080', '[Custom]'], + placeholder: 'Enter custom height in pixels' + }); + if (result) { + session.requestResolution(currentUUID, 4096, result || 2160, false, uuid); + } + }; + } + } + + if ("video_encoder" in sceneStats[uuid]) { + var span = container.querySelector("[data-video-codec]"); + if (span) { + span.classList.remove("hidden"); + span.innerHTML = "video codec: " + sceneStats[uuid].video_encoder; + } + } + } + } +} + +function remoteStats(msg, UUID) { + + var rpc = session.rpcs && session.rpcs[UUID] ? session.rpcs[UUID] : null; + + if (isIFrame && rpc) { + parent.postMessage({ remoteStats: msg.remoteStats, streamID: rpc.streamID, UUID: UUID }, session.iframetarget); + } + + if (!rpc) { + return; + } + + var allowUI = rpc.allowGraphs || session.allowGraphs; + if (allowUI && session.director) { + var size = 0; + for (var key in msg.remoteStats) { + if (msg.remoteStats.hasOwnProperty(key)) { + size++; + } + } + + if (!size) { + getById("container_" + UUID) + .querySelectorAll("[data-no-scenes]") + .forEach(ele => { + ele.classList.remove("hidden"); + if (ele.dataset.message) { + ele.innerHTML = "No scenes active"; + } + }); + log("zero size"); + } else { + getById("container_" + UUID) + .querySelectorAll("[data-no-scenes]") + .forEach(ele => { + ele.classList.add("hidden"); + }); + + for (var uuid in msg.remoteStats) { + var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-details-container"][data-uid="' + uuid + '"]'); + if (!container) { + container = getById("container_" + UUID) + .querySelector('[data-action-type="stats-graphs-details-container"]') + .cloneNode(true); + container.dataset.uid = uuid; + container.classList.remove("hidden"); + getById("container_" + UUID) + .querySelector('[data-action-type="stats-graphs-details"]') + .appendChild(container); + } + + plotData(msg.remoteStats[uuid], UUID, uuid); + + if ("video_bitrate_kbps" in msg.remoteStats[uuid] && msg.remoteStats[uuid].video_bitrate_kbps !== "video_bitrate_kbps") { + var span = container.querySelector("[data-bitrate]"); + if (span) { + span.classList.remove("hidden"); + span.innerHTML = "video bitrate: " + parseInt(msg.remoteStats[uuid].video_bitrate_kbps) + " (kbps)"; + span.style.cursor = "pointer"; + span.title = "Click to adjust bitrate"; + span.onclick = async function (e) { + e.preventDefault(); + e.stopPropagation(); + const result = await promptAlt("Select target bitrate (kbps)", false, false, false, false, false, false, { + type: 'select', + options: ['50', '500', '1000', '2000', '5000', '10000', '20000', '[Custom]'], + placeholder: 'Enter custom bitrate in kbps' + }); + if (result) { + var msg = { + targetBitrate: parseInt(result), + UUID: UUID, + requestAs: uuid + }; + if (isIFrame) { + parent.postMessage(msg, session.iframetarget); + } + session.sendRequest(msg); + } + }; + } + } + + var span = container.querySelector("[data-scene-name]"); + if (span && "label" in msg.remoteStats[uuid] && msg.remoteStats[uuid].label) { + span.classList.remove("hidden"); + span.innerHTML = "stats for viewer: " + msg.remoteStats[uuid].label; + } else if (span && "scene" in msg.remoteStats[uuid] && msg.remoteStats[uuid].scene !== false) { + span.classList.remove("hidden"); + span.innerHTML = "stats for scene: " + msg.remoteStats[uuid].scene; + } else if (uuid === "meshcast") { + span.classList.remove("hidden"); + span.innerHTML = "stats for meshcast ingest"; + span.title = "You can use &label=xxxx to give your view links a unique label"; + } else { + span.classList.remove("hidden"); + span.innerHTML = "stats for some viewer"; + span.title = "You can use &label=xxxx to give your view links a unique label"; + } + + if ("resolution" in msg.remoteStats[uuid]) { + var span = container.querySelector("[data-resolution]"); + if (span) { + span.classList.remove("hidden"); + span.innerHTML = msg.remoteStats[uuid].resolution; + span.style.cursor = "pointer"; + span.title = "Click to adjust resolution"; + span.onclick = async function (e) { + e.preventDefault(); + e.stopPropagation(); + const result = await promptAlt("Select target resolution", false, false, false, false, false, false, { + type: 'select', + options: ['360', '720', '1080', '1440', '2160', '[Custom]'], + placeholder: 'Enter custom height in pixels' + }); + if (result) { + session.requestResolution(UUID, 4096, result || 2160, false, uuid); + } + }; + } + } + + if ("video_encoder" in msg.remoteStats[uuid]) { + var span = container.querySelector("[data-video-codec]"); + if (span) { + span.classList.remove("hidden"); + span.innerHTML = "video codec: " + msg.remoteStats[uuid].video_encoder; + } + } + } + } + } +} + +function processStats(UUID) { + // for (pc in session.pcs){session.pcs[pc].getStats().then(function(stats) {stats.forEach(stat=>{if (stat.id.includes("RTCIce")){console.log(stat)}})})}; + + if (!session.rpcs || !(UUID in session.rpcs)) { + return; + } + + try { + if (session.rpcs[UUID].videoElement.paused) { + if (session.firstPlayTriggered) { + if (session.audioCtx.state == "suspended") { + // added oct 9th 2022 + try { + session.audioCtx.resume(); + } catch (e) { + warnlog(e); + } + } + if (session.audioCtx.state == "running") { + // NOTE: I Don't know why this was + log("trying to play"); + session.rpcs[UUID].videoElement + .play() + .then(_ => { + log("playing 8"); + //if ((session.audioEffects===true) || session.pushLoudness){ + // updateIncomingAudioElement(UUID); + //} + }) + .catch(warnlog); + } + } + } + } catch (e) { } + + try { + if (session.rpcs[UUID].realUUID && session.rpcs[session.rpcs[UUID].realUUID]) { + var node = session.rpcs[session.rpcs[UUID].realUUID]; + } else { + var node = session.rpcs[UUID]; + } + + var validTrackIds = []; + if (session.rpcs[UUID].streamSrc) { + session.rpcs[UUID].streamSrc.getTracks().forEach(trk => { + validTrackIds.push(trk.id); + }); + } + + if (session.rpcs[UUID].whep) { + processMeshcastStats(UUID); + if (!node.getStats) { + clearTimeout(session.rpcs[UUID].getStatsTimeout); + session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, session.statsInterval, UUID); + + //setTimeout(processStats, session.statsInterval, UUID); // no p2p, so lets do WHEP again manually. + } + } + + if (node.getStats) { + node.getStats().then(function (stats) { + if (!(UUID in session.rpcs)) { + return; + } + + clearTimeout(session.rpcs[UUID].getStatsTimeout); + session.rpcs[UUID].getStatsTimeout = setTimeout(processStats, session.statsInterval, UUID); + + if (!session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"] = {}; + } + + var nominatedCandidate = false; + var candidates = {}; + var ipleakingAllowedRemote = false; + var ipleakingAllowedLocal = false; + + stats.forEach(stat => { + try { + if (stat.id && stat.id.startsWith("DEPRECATED_")) { + return; + } + var trackID = stat.trackIdentifier || stat.id || false; + if (stat.type == "track" && stat.remoteSource) { + if (stat.trackIdentifier && !validTrackIds.includes(stat.trackIdentifier)) { + return; + } + if (stat.id in session.rpcs[UUID].stats) { + session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier; + session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0; + session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0; + session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0; + if ("frameWidth" in stat) { + if ("frameHeight" in stat) { + session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth + " x " + stat.frameHeight; + session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth; + session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight; + } + } + } else { + var media = {}; + media._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0; + media._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0; + media.Jitter_Buffer_ms = 0; + media._trackID = stat.trackIdentifier; + session.rpcs[UUID].stats[stat.id] = media; + if (stat.kind && stat.kind == "audio") { + session.rpcs[UUID].stats[stat.id].type = "Audio Track"; + session.rpcs[UUID].stats[stat.id]._type = "audio"; + } else if (stat.kind && stat.kind == "video") { + session.rpcs[UUID].stats[stat.id].type = "Video Track"; + session.rpcs[UUID].stats[stat.id]._type = "video"; + } + } + } else if (stat.type == "remote-candidate") { + candidates[stat.id] = stat; + if (stat.candidateType != "relay") { + ipleakingAllowedRemote = true; + } + } else if (stat.type == "local-candidate") { + candidates[stat.id] = stat; + if (stat.candidateType != "relay") { + ipleakingAllowedLocal = true; + } + } else if (stat.type == "candidate-pair" && stat.nominated) { + if (!nominatedCandidate) { + nominatedCandidate = stat; + } else if (nominatedCandidate.priority < stat.priority) { + nominatedCandidate = stat; + } + } else if (stat.type == "transport") { + if ("bytesReceived" in stat) { + if ("_bytesReceived" in session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) { + if (session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp) { + if (stat.timestamp) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].total_recv_bitrate_kbps = parseInt((8 * (stat.bytesReceived - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._bytesReceived)) / (stat.timestamp - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp)); + hideStreamLowBandwidth(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].total_recv_bitrate_kbps, UUID); + //changeSceneLowBandwidth(session.rpcs[UUID].stats['Peer-to-Peer_Connection'].total_recv_bitrate_kbps, UUID); + } + } + } + session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._bytesReceived = stat.bytesReceived; + } + if ("timestamp" in stat) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestamp = stat.timestamp; + if (!session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart = stat.timestamp; + } else { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._timestampStart) / 600) / 100; + } + } + } else if (stat.type == "inbound-rtp" && trackID) { + if (stat.trackIdentifier && !validTrackIds.includes(stat.trackIdentifier)) { + return; + } + session.rpcs[UUID].stats[trackID] = session.rpcs[UUID].stats[trackID] || {}; + if (stat.trackIdentifier) { + session.rpcs[UUID].stats[trackID]._trackID = stat.trackIdentifier; + } + if ("jitterBufferDelay" in stat) { + session.rpcs[UUID].stats[trackID].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[trackID]._jitter_delay_2)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[trackID]._jitter_count_2)) || 0; + session.rpcs[UUID].stats[trackID]._jitter_delay_2 = parseFloat(stat.jitterBufferDelay) || 0; + session.rpcs[UUID].stats[trackID]._jitter_count_2 = parseInt(stat.jitterBufferEmittedCount) || 0; + } + if ("frameWidth" in stat) { + if ("frameHeight" in stat) { + session.rpcs[UUID].stats[trackID].Resolution = stat.frameWidth + " x " + stat.frameHeight; + session.rpcs[UUID].stats[trackID]._frameWidth = stat.frameWidth; + session.rpcs[UUID].stats[trackID]._frameHeight = stat.frameHeight; + } + } + session.rpcs[UUID].stats[trackID].Bitrate_in_kbps = parseInt((8 * (stat.bytesReceived - (session.rpcs[UUID].stats[trackID]._last_bytes || 0))) / (stat.timestamp - session.rpcs[UUID].stats[trackID]._last_time)); + session.rpcs[UUID].stats[trackID]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[trackID]._last_bytes; + session.rpcs[UUID].stats[trackID]._last_time = stat.timestamp || session.rpcs[UUID].stats[trackID]._last_time; + if (stat.mediaType == "video") { + session.rpcs[UUID].stats._codecId = stat.codecId; + session.rpcs[UUID].stats._codecIdTrackId = trackID; + session.rpcs[UUID].stats[trackID].type = "Video Stream"; + session.rpcs[UUID].stats[trackID]._type = "video"; + if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP8") { + session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0; + session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0; + log("OBS PLI FIX MODE ON"); + if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix) { + // heavy packet loss with no pliCount? + session.requestKeyframe(UUID); + session.rpcs[UUID].stats[trackID].nackTrigger = 0; + log("TRYING KEYFRAME"); + } else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) { + session.rpcs[UUID].stats[trackID].nackTrigger = 0; + } + } else if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP9") { + session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0; + session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0; + log("OBS PLI FIX MODE ON"); + if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix * 4) { + // heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often. + session.requestKeyframe(UUID); + session.rpcs[UUID].stats[trackID].nackTrigger = 0; + log("TRYING KEYFRAME"); + } else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) { + session.rpcs[UUID].stats[trackID].nackTrigger = 0; + } + } + session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0; + session.rpcs[UUID].stats[trackID].streamErrors_nackCount = stat.nackCount || 0; + //warnlog(stat); + if ("framesPerSecond" in stat) { + session.rpcs[UUID].stats[trackID].FPS = parseInt(stat.framesPerSecond); + } else if ("framesDecoded" in stat && stat.timestamp) { + var lastFramesDecoded = 0; + var lastTimestamp = 0; + try { + lastFramesDecoded = session.rpcs[UUID].stats[trackID]._framesDecoded; + lastTimestamp = session.rpcs[UUID].stats[trackID]._timestamp; + } catch (e) { } + session.rpcs[UUID].stats[trackID].FPS = parseInt((10 * (stat.framesDecoded - lastFramesDecoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10; + //session.rpcs[UUID].stats[trackID].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp)); + session.rpcs[UUID].stats[trackID]._framesDecoded = stat.framesDecoded; + session.rpcs[UUID].stats[trackID]._timestamp = stat.timestamp / 1000; + } + } else if (stat.mediaType == "audio") { + //log("AUDIO LEVEL: "+stat.audioLevel); + session.rpcs[UUID].stats._audioCodecId = stat.codecId; + session.rpcs[UUID].stats._audioCodecIdTrackId = trackID; + session.rpcs[UUID].stats[trackID].type = "Audio Stream"; + session.rpcs[UUID].stats[trackID]._type = "audio"; + if ("audioLevel" in stat) { + session.rpcs[UUID].stats[trackID].audio_level = parseInt(parseFloat(stat.audioLevel) * 10000) / 10000.0; + } + } + if ("packetsLost" in stat && "packetsReceived" in stat) { + if (!("_packetsLost" in session.rpcs[UUID].stats[trackID])) { + session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost; + } + if (!("_packetsReceived" in session.rpcs[UUID].stats[trackID])) { + session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived; + } + if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[trackID])) { + session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = 0; + } + let packetLoss = ((stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost) * 100.0) / (stat.packetsReceived - session.rpcs[UUID].stats[trackID]._packetsReceived + (stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost)) || 0; + /* if (session.rpcs[UUID].stats[trackID]._type && (session.rpcs[UUID].stats[trackID]._type =="video")){ + if (packetLoss>1){ + var data = {}; + data.bitrate = parseInt(session.rpcs[UUID].stats[trackID].Bitrate_in_kbps*0.8); + session.sendRequest(data,UUID); + } else { + var data = {}; + data.bitrate = parseInt(session.rpcs[UUID].stats[trackID].Bitrate_in_kbps*1.1); + session.sendRequest(data,UUID); + } + } */ + session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage * 0.35 + 0.65 * packetLoss; + if (session.rpcs[UUID].signalMeter && session.rpcs[UUID].stats[trackID]._type === "video") { + if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.01) { + if (session.rpcs[UUID].stats[trackID].Bitrate_in_kbps == 0) { + session.rpcs[UUID].signalMeter.dataset.level = 0; + } else { + session.rpcs[UUID].signalMeter.dataset.level = 5; + } + } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.3) { + session.rpcs[UUID].signalMeter.dataset.level = 4; + } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 1.0) { + session.rpcs[UUID].signalMeter.dataset.level = 3; + } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 3.5) { + session.rpcs[UUID].signalMeter.dataset.level = 2; + } else { + session.rpcs[UUID].signalMeter.dataset.level = 1; + } + } + session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived; + session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost; + } + } else if ("_codecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._codecId) { + if ("mimeType" in stat) { + if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]) { + session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType; + } else { + session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {}; + session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType; + } + } + if ("frameHeight" in stat) { + if ("frameWidth" in stat) { + session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth) + " x " + parseInt(stat.frameHeight); + } + } + } else if ("_audioCodecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._audioCodecId) { + if ("mimeType" in stat) { + var addOnDescription = stat.mimeType; + addOnDescription = addOnDescription.replace("audio/", ""); + if ("sdpFmtpLine" in stat) { + if (stat.sdpFmtpLine.includes("useinbandfec=1")) { + addOnDescription += ", /w fec"; + } + } + if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId]) { + } else { + session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId] = {}; + } + session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].codec = addOnDescription; + if (stat.clockRate) { + session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].clockRate = stat.clockRate; + if (stat.channels) { + session.rpcs[UUID].stats[session.rpcs[UUID].stats._audioCodecIdTrackId].clockRate += " / " + stat.channels; + } + } + } + } else if (Firefox) { + if ("frameWidth" in stat) { + session.rpcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight; + if ("framesPerSecond" in stat) { + session.rpcs[UUID].stats.resolution += " @ " + stat.framesPerSecond; + } + } + if ("mimeType" in stat && "type" in stat && "id" in stat && stat.type == "codec") { + if (stat.mimeType.includes("video")) { + session.rpcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1]; + } else if (stat.mimeType.includes("audio")) { + session.rpcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1]; + if (stat.clockRate) { + session.rpcs[UUID].stats.audio_clockRate = stat.clockRate; + if (stat.channels) { + session.rpcs[UUID].stats.audio_clockRate += " / " + stat.channels; + } + } else if (stat.sdpFmtpLine) { + session.rpcs[UUID].stats.fmtp = stat.sdpFmtpLine; + } + } + } + /* if ("jitter" in stat){ + if (("kind" in stat) && (stat.kind=="video")){ + session.rpcs[UUID].stats.video_jitter_ms = parseInt(stat.jitter*1000); + } else if (("kind" in stat) && (stat.kind=="audio")){ + session.rpcs[UUID].stats.audio_jitter_ms = parseInt(stat.jitter*1000); + } + } */ + if ("bytesReceived" in stat) { + if ("kind" in stat && stat.kind == "video") { + if ("_bytesReceived_video" in session.rpcs[UUID].stats) { + session.rpcs[UUID].stats.videoBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_video) / ((1024 * session.statsInterval) / 8000)); + } + session.rpcs[UUID].stats._bytesReceived_video = stat.bytesReceived; + } else if ("kind" in stat && stat.kind == "audio") { + if ("_bytesReceived_audio" in session.rpcs[UUID].stats) { + session.rpcs[UUID].stats.audioBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_audio) / ((1024 * session.statsInterval) / 8000)); + } + session.rpcs[UUID].stats._bytesReceived_audio = stat.bytesReceived; + } + } + } + } catch (e) { + errorlog(e); + } + }); + + ////////// + + if (nominatedCandidate) { + if (nominatedCandidate.localCandidateId && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id !== nominatedCandidate.localCandidateId) { + if ("candidateType" in nominatedCandidate) { + try { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_local; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol; + } catch (e) { } + } + } + if (nominatedCandidate.remoteCandidateId && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id !== nominatedCandidate.remoteCandidateId) { + if ("candidateType" in nominatedCandidate) { + try { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_remote; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol; + } catch (e) { } + } + } + if (nominatedCandidate.localCandidateId) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._local_ice_id = nominatedCandidate.localCandidateId; + } + if (nominatedCandidate.remoteCandidateId) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"]._remote_ice_id = nominatedCandidate.remoteCandidateId; + } + if ("currentRoundTripTime" in nominatedCandidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime * 1000; + } + } + + if (nominatedCandidate && nominatedCandidate.localCandidateId) { + if (candidates[nominatedCandidate.localCandidateId]) { + var candidate = candidates[nominatedCandidate.localCandidateId]; + if ("candidateType" in candidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_local = candidate.candidateType; + if (candidate.candidateType === "relay") { + if ("relayProtocol" in candidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol = candidate.relayProtocol; + } else { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol; + } + if ("ip" in candidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP = candidate.ip; + } else { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP; + } + // Store URL for QoS hostname extraction (may not exist in all browsers) + if ("url" in candidate && candidate.url) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_url = candidate.url; + } else { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_url; + } + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_ip_blocking = !ipleakingAllowedLocal; + } else { + try { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_IP; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_protocol; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_relay_url; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_ip_blocking; + } catch (e) { } + } + } + if ("networkType" in candidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].local_networkType = candidate.networkType; + } + } + } + if (nominatedCandidate && nominatedCandidate.remoteCandidateId) { + if (candidates[nominatedCandidate.remoteCandidateId]) { + var candidate = candidates[nominatedCandidate.remoteCandidateId]; + if ("candidateType" in candidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].candidateType_remote = candidate.candidateType; + if (candidate.candidateType === "relay") { + if ("ip" in candidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP = candidate.ip; + } else { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP; + } + if ("relayProtocol" in candidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol = candidate.relayProtocol; + } else { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol; + } + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_ip_blocking = !ipleakingAllowedRemote; + } else { + try { + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_IP; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_relay_protocol; + delete session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_ip_blocking; + } catch (e) { } + } + } + if ("networkType" in candidate) { + session.rpcs[UUID].stats["Peer-to-Peer_Connection"].remote_networkType = candidate.networkType; + } + } + } + + // QoS data accumulation + if (session.qosEnabled && session.qosData && !session.qosData.sent) { + try { + var qd = session.qosData; + var peerStats = session.rpcs[UUID].stats["Peer-to-Peer_Connection"]; + + // Accumulate RTT samples + if (peerStats && peerStats.Round_Trip_Time_ms) { + qd.rttSamples.push(peerStats.Round_Trip_Time_ms); + if (qd.rttSamples.length > 500) qd.rttSamples.shift(); + } + + // Track transport type and TURN servers + if (peerStats && peerStats.candidateType_local) { + if (peerStats.candidateType_local === "relay") { + qd.transportType = "turn"; + // Use URL-based hostname instead of IP (graceful degradation if unavailable) + if (peerStats.local_relay_url && session.qosTurnAllowlist && session.qosTurnAllowlist.length) { + var host = extractTurnHostnameFromUrl(peerStats.local_relay_url); + if (host && session.qosTurnAllowlist.includes(host) && !qd.turnServersUsed.includes(host)) { + qd.turnServersUsed.push(host); + } + } + // Safari/Firefox may not have url - we still get transportType="turn" + } else if (!qd.transportType) { + qd.transportType = "p2p"; + } + if (!qd.candidateTypesLocal.includes(peerStats.candidateType_local)) { + qd.candidateTypesLocal.push(peerStats.candidateType_local); + } + } + if (peerStats && peerStats.candidateType_remote && !qd.candidateTypesRemote.includes(peerStats.candidateType_remote)) { + qd.candidateTypesRemote.push(peerStats.candidateType_remote); + } + + // Accumulate packet loss and jitter from tracks + for (var tid in session.rpcs[UUID].stats) { + var trackStat = session.rpcs[UUID].stats[tid]; + if (trackStat && typeof trackStat === "object") { + if (trackStat._type === "video") { + if (trackStat.packetLoss_in_percentage !== undefined) { + qd.packetLossVideoSamples.push(trackStat.packetLoss_in_percentage); + if (qd.packetLossVideoSamples.length > 500) qd.packetLossVideoSamples.shift(); + } + if (trackStat.Bitrate_in_kbps) { + qd.bitrateSamples.push(trackStat.Bitrate_in_kbps); + if (qd.bitrateSamples.length > 500) qd.bitrateSamples.shift(); + } + if (trackStat.Jitter_Buffer_ms) { + qd.jitterSamples.push(trackStat.Jitter_Buffer_ms); + if (qd.jitterSamples.length > 500) qd.jitterSamples.shift(); + } + if (trackStat.codec) qd.lastVideoCodec = trackStat.codec; + if (trackStat.Resolution) qd.lastResolution = trackStat.Resolution; + } else if (trackStat._type === "audio") { + if (trackStat.packetLoss_in_percentage !== undefined) { + qd.packetLossAudioSamples.push(trackStat.packetLoss_in_percentage); + if (qd.packetLossAudioSamples.length > 500) qd.packetLossAudioSamples.shift(); + } + if (trackStat.codec) qd.lastAudioCodec = trackStat.codec; + } + } + } + } catch (e) { warnlog("QoS accumulation error: " + e); } + } + + playoutdelay(UUID); + + setTimeout(function () { + session.directorSpeakerMute(); + session.directorDisplayMute(); + }, 0); + }); + } + } catch (e) { + errorlog(e); + } + + pokeIframeAPI("view-stats-updated", true, UUID); +} + +// QoS stats collection for publisher outbound connections (session.pcs) +// This runs periodically when QoS is enabled to gather stats from publisher connections +function processPcsQosStats(UUID) { + if (!session.qosEnabled || !session.qosData || session.qosData.sent) return; + if (!session.pcs || !(UUID in session.pcs)) return; + + try { + session.pcs[UUID].getStats().then(function(stats) { + if (!(UUID in session.pcs)) return; + if (!session.qosEnabled || !session.qosData || session.qosData.sent) return; + + var qd = session.qosData; + var nominatedCandidate = null; + var candidates = {}; + + stats.forEach(function(stat) { + try { + if (stat.id && stat.id.startsWith("DEPRECATED_")) return; + + if (stat.type === "outbound-rtp") { + if (stat.kind === "video") { + if (stat.qualityLimitationReason) { + session.pcs[UUID].stats.quality_limitation_reason = stat.qualityLimitationReason; + } + if (stat.frameWidth && stat.frameHeight) { + var res = stat.frameWidth + " x " + stat.frameHeight; + if (stat.framesPerSecond) { + res += " @ " + stat.framesPerSecond; + } + session.pcs[UUID].stats.resolution = res; + if (!qd.lastResolution) qd.lastResolution = res; + } + if (stat.encoderImplementation) { + session.pcs[UUID].stats.encoder = stat.encoderImplementation; + if (!qd.lastVideoCodec) qd.lastVideoCodec = stat.encoderImplementation; + } + // Track bitrate for publishers + if (stat.bytesSent !== undefined && stat.timestamp) { + if (session.pcs[UUID].stats._lastBytesSent !== undefined) { + var timeDiff = stat.timestamp - session.pcs[UUID].stats._lastTimestamp; + if (timeDiff > 0) { + var bitrate = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._lastBytesSent)) / timeDiff); + if (bitrate > 0) { + qd.bitrateSamples.push(bitrate); + if (qd.bitrateSamples.length > 500) qd.bitrateSamples.shift(); + } + } + } + session.pcs[UUID].stats._lastBytesSent = stat.bytesSent; + session.pcs[UUID].stats._lastTimestamp = stat.timestamp; + } + } + } else if (stat.type === "remote-candidate") { + candidates[stat.id] = stat; + if (stat.relayProtocol && stat.ip) { + session.pcs[UUID].stats.remote_relay_IP = stat.ip; + session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol; + } + if (stat.candidateType) { + session.pcs[UUID].stats.candidateType_remote = stat.candidateType; + } + } else if (stat.type === "local-candidate") { + candidates[stat.id] = stat; + if (stat.relayProtocol && stat.ip) { + session.pcs[UUID].stats.local_relayIP = stat.ip; + session.pcs[UUID].stats.local_relayProtocol = stat.relayProtocol; + } + if (stat.candidateType) { + session.pcs[UUID].stats.candidateType_local = stat.candidateType; + } + } else if (stat.type === "candidate-pair" && stat.nominated) { + if (!nominatedCandidate || nominatedCandidate.priority < stat.priority) { + nominatedCandidate = stat; + } + } else if (stat.type === "remote-inbound-rtp") { + // Packet loss from remote peer + if (stat.packetsLost !== undefined && stat.packetsReceived !== undefined) { + var total = stat.packetsLost + stat.packetsReceived; + if (total > 0) { + var lossPercent = (stat.packetsLost / total) * 100; + if (stat.kind === "video") { + qd.packetLossVideoSamples.push(lossPercent); + if (qd.packetLossVideoSamples.length > 500) qd.packetLossVideoSamples.shift(); + } else if (stat.kind === "audio") { + qd.packetLossAudioSamples.push(lossPercent); + if (qd.packetLossAudioSamples.length > 500) qd.packetLossAudioSamples.shift(); + } + } + } + // Jitter from remote peer + if (stat.jitter !== undefined) { + var jitterMs = stat.jitter * 1000; + qd.jitterSamples.push(jitterMs); + if (qd.jitterSamples.length > 500) qd.jitterSamples.shift(); + } + } + } catch (e) { } + }); + + // Process nominated candidate for RTT and transport type + if (nominatedCandidate) { + // RTT + if (nominatedCandidate.totalRoundTripTime && nominatedCandidate.responsesReceived) { + var rtt = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000); + session.pcs[UUID].stats.average_roundTripTime_ms = rtt; + qd.rttSamples.push(rtt); + if (qd.rttSamples.length > 500) qd.rttSamples.shift(); + } + if (nominatedCandidate.currentRoundTripTime) { + var currentRtt = parseInt(nominatedCandidate.currentRoundTripTime * 1000); + qd.rttSamples.push(currentRtt); + if (qd.rttSamples.length > 500) qd.rttSamples.shift(); + } + + // Transport type from nominated candidate + var localCandidate = candidates[nominatedCandidate.localCandidateId]; + var remoteCandidate = candidates[nominatedCandidate.remoteCandidateId]; + + if (localCandidate) { + if (localCandidate.candidateType === "relay") { + qd.transportType = "turn"; + // Use stat.url to get TURN hostname (official servers only) + // Note: stat.url may not be available in all browsers (graceful degradation) + if (localCandidate.url && session.qosTurnAllowlist && session.qosTurnAllowlist.length) { + var host = extractTurnHostnameFromUrl(localCandidate.url); + if (host && session.qosTurnAllowlist.includes(host) && !qd.turnServersUsed.includes(host)) { + qd.turnServersUsed.push(host); + } + } + // If url unavailable (Safari/Firefox), we still track transportType="turn" + // but skip hostname - better no data than wrong/private data + } else if (!qd.transportType || qd.transportType === "unknown") { + qd.transportType = "p2p"; + } + if (!qd.candidateTypesLocal.includes(localCandidate.candidateType)) { + qd.candidateTypesLocal.push(localCandidate.candidateType); + } + } + if (remoteCandidate && !qd.candidateTypesRemote.includes(remoteCandidate.candidateType)) { + qd.candidateTypesRemote.push(remoteCandidate.candidateType); + } + } + + // Schedule next stats collection (every 5 seconds) + if (UUID in session.pcs && session.qosEnabled && session.qosData && !session.qosData.sent) { + clearTimeout(session.pcs[UUID].qosStatsTimeout); + session.pcs[UUID].qosStatsTimeout = setTimeout(processPcsQosStats, 5000, UUID); + } + }).catch(function(e) { + warnlog("QoS pcs stats error: " + e); + }); + } catch (e) { + warnlog("QoS pcs stats error: " + e); + } +} + +function createConnectionDetailsEle(UUID) { + if (!session.rpcs[UUID]) { + return false; + } + + session.rpcs[UUID].connectionDetails = document.createElement("div"); + session.rpcs[UUID].connectionDetails.id = "remoteConnections_" + UUID; + if (session.rpcs[UUID].stats.info && "total_outbound_p2p_connections" in session.rpcs[UUID].stats.info) { + session.rpcs[UUID].connectionDetails.innerText = "🔗" + session.rpcs[UUID].stats.info.total_outbound_p2p_connections; + session.rpcs[UUID].connectionDetails.dataset.value = session.rpcs[UUID].stats.info.total_outbound_p2p_connections; + } + session.rpcs[UUID].connectionDetails.dataset.UUID = UUID; + session.rpcs[UUID].connectionDetails.title = getTranslation("viewer-count"); + session.rpcs[UUID].connectionDetails.className = "rem-con-count"; + + session.rpcs[UUID].connectionDetails.addEventListener("click", function (e) { + // show stats of video if double clicked + log("clicked connectionDetails icon "); + try { + e.preventDefault(); + if (session.statsMenu !== false) { + var uid = e.currentTarget.dataset.UUID; + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, uid); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + e.stopPropagation(); + return false; + } catch (e) { + errorlog(e); + } + }); + return true; +} + +function playoutdelay(UUID) { + // applies a delay to all videos + try { + if ((session.rpcs[UUID].buffer !== false) || (session.buffer != false) || (session.audioBuffer !== false)) { + + // if buffer is set, then session.sync will be set; at least to 0. + var receivers = getReceivers2(UUID).reverse() || []; //session.rpcs[UUID].getReceivers().reverse(); + + if (session.rpcs[UUID].whep) { + receivers = receivers.concat(getReceiversMC(UUID).reverse()); // if I try to reuse getReceivers2, I get some confused stats (not able to tell tracks apart) TODO: see if this issue is a problem else where, esp with screen shares. sstype==3 + } + receivers.forEach(function (receiver) { + try { + for (var tid in session.rpcs[UUID].stats) { + if (typeof session.rpcs[UUID].stats[tid] == "object" && "_trackID" in session.rpcs[UUID].stats[tid] && session.rpcs[UUID].stats[tid]._trackID === receiver.track.id && session.rpcs[UUID].stats[tid]._type == receiver.track.kind && "Jitter_Buffer_ms" in session.rpcs[UUID].stats[tid]) { + //if (ChromiumVersion<=103){ // I don't know the exact version, except I know OBS Studio is 103 and it uses the old way still.netwqor + var sync_offset = 0.0; + if (session.rpcs[UUID].stats[tid]._sync_offset) { + sync_offset = session.rpcs[UUID].stats[tid]._sync_offset || 0; + } else { + session.rpcs[UUID].stats[tid]._sync_offset = 0; + } + var target_buffer = 0; + if (session.rpcs[UUID].stats[tid]._type == "audio") { + if (session.audioBuffer !== false) { + target_buffer = parseFloat(session.audioBuffer || 0); + } else { + target_buffer = parseFloat(session.buffer || 0); + } + } else { + target_buffer = parseFloat(session.buffer || 0); + } + if (session.rpcs[UUID].buffer !== false) { + target_buffer = parseFloat(session.rpcs[UUID].buffer); + } + sync_offset += target_buffer; + sync_offset -= session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0; + if (session.includeRTT && session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) { + sync_offset -= parseInt(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms / 2) || 0; // I can't be sure what the actual one-way delay is + } + if (sync_offset > target_buffer) { + sync_offset = target_buffer || 0; + } + if (sync_offset < 0) { + sync_offset = 0; + } + session.rpcs[UUID].stats[tid].Added_Buffer_Delay_ms = sync_offset || 0; + if (session.rpcs[UUID].stats["Peer-to-Peer_Connection"]) { + session.rpcs[UUID].stats[tid].Total_Playout_Delay_ms = sync_offset + parseInt(session.rpcs[UUID].stats["Peer-to-Peer_Connection"].Round_Trip_Time_ms / 2) + session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0; + } + if (session.rpcs[UUID].stats[tid]._type == "audio") { + session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0; + if ("jitterBufferTarget" in receiver) { + receiver.jitterBufferTarget = parseFloat(sync_offset) || 0; + } else { + receiver.playoutDelayHint = parseFloat(sync_offset / 1000) || 0; + } + // receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe + if (session.sync !== false) { + var audio_delay = session.sync || 0; // video is typically showing greater delay than audio. + audio_delay += target_buffer - session.rpcs[UUID].stats[tid].Jitter_Buffer_ms || 0; + if (receiver.track.kind == "audio" && receiver.track.id in session.rpcs[UUID].inboundAudioPipeline) { + if (session.rpcs[UUID].inboundAudioPipeline[receiver.track.id] && session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode) { + if (audio_delay < 0) { + audio_delay = 0; + } + try { + session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.linearRampToValueAtTime(parseFloat(audio_delay / 1000.0), session.audioCtx.currentTime + parseFloat(session.statsInterval / 9000)); + } catch (e) { + session.rpcs[UUID].inboundAudioPipeline[receiver.track.id].delayNode.delayTime.setValueAtTime(parseFloat(audio_delay / 1000.0), session.audioCtx.currentTime + 1); + } + session.rpcs[UUID].stats[tid].Audio_Sync_Delay_ms = audio_delay || 0; + } + } + } + } else if (session.rpcs[UUID].stats[tid]._type == "video") { + session.rpcs[UUID].stats[tid]._sync_offset = sync_offset || 0; + if ("jitterBufferTarget" in receiver) { + receiver.jitterBufferTarget = parseFloat(sync_offset) || 0; + } else { + receiver.playoutDelayHint = parseFloat(sync_offset / 1000) || 0; + } + // receiver.jitterBufferDelayhint = parseFloat(sync_offset/1000); // This is deprecated I believe + } + } + } + } catch (e) { + errorlog(e); + } + }); + } + } catch (e) { + errorlog(e); + warnlog("device does not support playout delay"); + } +} + +function printViewStats(menu, UUID) { + // Stats for viewing a remote video + if (session.statsMenu === false) { + return false; + } + + if (!session.rpcs[UUID]) { + menu.innerHTML = "


    Remote Publisher Disconnected"; + return false; + } + + var statsObj = session.rpcs[UUID].stats; + var streamID = session.rpcs[UUID].streamID; + var scrollLeft = menu.scrollLeft; + var scrollTop = menu.scrollTop; + menu.innerHTML = "StreamID: " + streamID + "
    "; + + if (statsObj.chunked_mode_video && typeof statsObj.chunked_mode_video.buffer_buffer !== "undefined") { + var chunkVideo = statsObj.chunked_mode_video; + var chunkSummary = "Video Buffer: " + parseInt(chunkVideo.buffer_buffer || 0) + " ms / Δ " + parseInt(chunkVideo.buffer_delta || 0) + " ms"; + if (chunkVideo.rebuffering) { + chunkSummary += " (rebuffering)"; + } + menu.innerHTML += chunkSummary + "
    "; + if (typeof chunkVideo.fec_repairs !== "undefined" || typeof chunkVideo.nacks_sent !== "undefined") { + var repairSummary = []; + if (typeof chunkVideo.fec_repairs !== "undefined") { + repairSummary.push("FEC " + parseInt(chunkVideo.fec_repairs || 0)); + } + if (typeof chunkVideo.nacks_sent !== "undefined") { + repairSummary.push("NACK " + parseInt(chunkVideo.nacks_sent || 0)); + } + if (repairSummary.length) { + menu.innerHTML += "Video Repairs: " + repairSummary.join(" / ") + "
    "; + } + } + } + if (statsObj.chunked_mode_audio && typeof statsObj.chunked_mode_audio.buffer_buffer !== "undefined") { + var chunkAudio = statsObj.chunked_mode_audio; + var audioSummary = "Audio Buffer: " + parseInt(chunkAudio.buffer_buffer || 0) + " ms / Δ " + parseInt(chunkAudio.buffer_delta || 0) + " ms"; + if (chunkAudio.rebuffering) { + audioSummary += " (rebuffering)"; + } + menu.innerHTML += audioSummary + "
    "; + } + + //// doesn't work on viewer side. + //if (session.rpcs && session.rpcs[UUID] && session.rpcs[UUID] && session.rpcs[UUID].restartIce){ // only show if available + // menu.innerHTML += ""; + //} + + menu.innerHTML += printValues(statsObj); + menu.scrollTop = scrollTop; + menu.scrollLeft = scrollLeft; + return true; +} + +function plotDataSimple(canvas, bitrate, nacks = 0) { + canvas.height = 50; + canvas.width = 124; + canvas.className = "canvasStats"; + var context = canvas.getContext("2d"); + if (isNaN(bitrate)) { + bitrate = 0; + } + if (isNaN(nacks)) { + nacks = 0; + } + var height = context.canvas.height; + var width = context.canvas.width; + + var val = (10 - nacks) / 10; + if (val > 1) { + val = 1; + } else if (val < 0) { + val = 0; + } + + var yScale = height / 4000; + var x = width - 1; + var y = height - bitrate * yScale; + var w = 1; + + context.fillStyle = getColor(val); + context.fillRect(x, y, w, height); + context.fillStyle = "#FFFFFF55"; + context.fillRect(x, y - 2, w, 4); + + if (y - 5 > 0) { + context.fillStyle = "#FFFFFF44"; + context.fillRect(x, y + 2, w, 1); + } + + context.putImageData(context.getImageData(1, 0, width - 1, height), 0, 0); + context.clearRect(width - 1, 0, 1, height); +} + +function printValues(obj, sort = false) { + // see: printViewStats + var out = ""; + + var keys = Object.keys(obj); + + if (sort) { + keys.sort(); + } + var lat = false; + var lon = false; + + keys.forEach(key => { + if (key.startsWith("_")) return; + if (typeof obj[key] === "object" && obj[key] !== null) { + let tmp = sanitizeChat(key); + out += `
  • ${tmp}

  • `; + if (key == "info") { + out += printValues(obj[key]); + } else if (key == "meta") { + out += `
  • `; + Object.entries(obj[key]).forEach(([category, data]) => { + if (data.type === "file" && data.filetype && data.filetype.startsWith("image/")) { + out += `
    + ${data.label || category} +
    + ${category} + ${formatFileSize(data.size)} +
    +
    `; + } else if (data.type === "url") { + out += ``; + } else { + out += `
    + ${category}: + ${data.value || ''} +
    `; + } + }); + + out += `
  • `; + } else { + out += printValues(obj[key], true); + } + } else { + try { + var unit = ""; + + var value = obj[key]; + + var stat = sanitizeChat(key); + + stat = stat.charAt(0).toUpperCase() + stat.slice(1); + + var hint = ""; + + if (typeof obj[key] == "string") { + value = sanitizeChat(value); + } + + if (key == "useragent") { + value = "" + value + ""; + } + + if (key == "Bitrate_in_kbps") { + var unit = " kbps"; + stat = "Bitrate"; + hint = "You can refer to the documentation for ways to increase the target bitrate"; + } else if (key == "type") { + var unit = ""; + stat = "Type"; + + if (value == "Audio Track") { + value = "🔊 " + value; + //out += ""; + } + + if (value == "Video Track") { + value = "📺 " + value; + } + } else if (key == "packetLoss_in_percentage") { + var unit = " %"; + stat = "Packet Loss 📶"; + value = parseInt(parseFloat(value) * 10000) / 10000.0; + hint = "A high packet loss will lower quality of the media"; + } else if (key == "local_relay_IP") { + value = "" + value + ""; + } else if (key == "remote_relay_IP") { + value = "" + value + ""; + } else if (key == "local_ip_blocking" && value) { + console.warn("Your system or connection is blocking p2p traffic"); + value = "⚠️ You're blocking"; + hint = "no direct p2p connection made because of YOUR browser or system setting"; + } else if (key == "remote_ip_blocking" && value) { + console.warn("A remote client is blocking p2p traffic"); + value = "⚠️ They're blocking"; + hint = "no direct p2p connection made because of THEIR browser or system setting"; + } else if (key == "candidateType_local" && value == "relay") { + value = "💸 relay server"; + hint = "no direct p2p connection made; using the TURN relay servers."; + stat = "Candidate type - Local"; + } else if (key == "candidateType_remote" && value == "relay") { + value = "💸 relay server"; + hint = "no direct p2p connection made; using the TURN relay servers."; + stat = "Candidate type - Remote"; + } else if (key == "candidateType_local" && value == "host") { + hint = "No NAT firewall, typical of LAN to LAN"; + stat = "Candidate type - Local"; + } else if (key == "candidateType_remote" && value == "host") { + hint = "No NAT firewall, typical of LAN to LAN"; + stat = "Candidate type - Remote"; + } else if (key == "candidateType_local" && value == "srflx") { + hint = "direct p2p, but NAT firewall likely"; + stat = "Candidate type - Local"; + } else if (key == "candidateType_remote" && value == "srflx") { + hint = "direct p2p, but NAT firewall likely"; + stat = "Candidate type - Remote"; + } else if (key == "height_url") { + if (value == false) { + return; + } + } else if (key == "width_url") { + if (value == false) { + return; + } + } else if (key == "height_url") { + if (value == false) { + return; + } + } else if (key == "version") { + stat = "VDO.Ninja Version"; + } else if (key == "platform") { + stat = "Platform (OS)"; + } else if (key == "iPhone12Up") { + stat = "iPhone 12 and up"; + } else if (key == "aec_url") { + stat = "Echo-Cancellation"; + } else if (key == "agc_url") { + stat = "Auto-Gain (agc)"; + } else if (key == "denoise_url") { + stat = "De-noising "; + } else if (key == "audio_level") { + stat = "Audio Level"; + } else if (key == "Jitter_Buffer_ms") { + var unit = " ms"; + stat = "Jitter Buffer Delay"; + } else if (key == "Added_Buffer_Delay_ms") { + var unit = " ms"; + stat = "Added Buffer Delay"; + hint = "Value of playout buffer delay added if using &buffer"; + } else if (key == "Total_Playout_Delay_ms") { + // doesn't include bluetooth / monitor / capture delay, etc. + var unit = " ms"; + stat = "Total Playout Delay"; + hint = "Network latency + Jitter buffer + any manually added playout delay"; + } else if (value === null) { + value = "null"; + } else if (key == "stereo_url") { + stat = "Pro-Audio
    (Stereo-mode)"; + if (value == 3) { + value = "3 (outbound hi-fi)
    Use Headphones"; + } else if (value == 1) { + value = "1 (in & out hi-fi)
    Use Headphones"; + } else if (value == 2) { + value = "3 (inbound hi-fi)"; + } else if (value == 4) { + value = "3 (multichannel)
    Use Headphones"; + } else if (value == 5) { + value = "5 (auto-mode)
    Use Headphones"; + } + } else if (value === false) { + return; + } else if (value === "false") { + return; + } else if (key == "lat") { + lat = value; + if (lat && lon) { + const mapWidth = 250; + const mapHeight = 250; + const x = (lon + 180) * (mapWidth / 360); + const mapRatio = mapHeight / mapWidth; + const latRad = (lat * Math.PI) / 180; + const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2)); + const y = mapHeight / 2 - ((mapWidth * mercN) / (2 * Math.PI)) * mapRatio; + out += + '
    \ + World Map\ +
    \ +
    '; + } + } else if (key == "lon") { + lon = value; + if (lat && lon) { + const mapWidth = 250; + const mapHeight = 250; + const x = (lon + 180) * (mapWidth / 360); + const mapRatio = mapHeight / mapWidth; + const latRad = (lat * Math.PI) / 180; + const mercN = Math.log(Math.tan(Math.PI / 4 + latRad / 2)); + const y = mapHeight / 2 - ((mapWidth * mercN) / (2 * Math.PI)) * mapRatio; + out += + '
    \ + World Map\ +
    \ +
    '; + out += 'Open Location in Google Maps'; + } + } + + stat = stat.replaceAll("_", " "); + stat = stat.trim(); + + if (hint) { + out += "
  • " + stat + "" + value + unit + "
  • "; + } else { + out += "
  • " + stat + "" + value + unit + "
  • "; + } + } catch (e) { + warnlog(e); + } + + } + }); + return out; +} + +function processMeshcastStats(UUID) { + try { + session.rpcs[UUID].whep.getStats().then(function (stats) { + if (!(UUID in session.rpcs)) { + return; + } + + if (!session.rpcs[UUID].stats["WHEP_Connection"]) { + // meshcast + session.rpcs[UUID].stats["WHEP_Connection"] = {}; + } + + // var qos = false;] + + var nominatedCandidate = false; + var candidates = {}; + var ipleakingAllowedRemote = false; + var ipleakingAllowedLocal = false; + + stats.forEach(stat => { + if (stat.id && stat.id.startsWith("DEPRECATED_")) { + return; + } + + var trackID = stat.trackIdentifier || stat.id || false; + + if (stat.type == "remote-candidate") { + candidates[stat.id] = stat; + if (stat.candidateType != "relay") { + ipleakingAllowedRemote = true; + } + } else if (stat.type == "local-candidate") { + candidates[stat.id] = stat; + if (stat.candidateType != "relay") { + ipleakingAllowedLocal = true; + } + } else if (stat.type == "candidate-pair" && stat.nominated) { + if (!nominatedCandidate) { + nominatedCandidate = stat; + } else if (nominatedCandidate.priority < stat.priority) { + nominatedCandidate = stat; + } + } else if (stat.type == "track" && stat.remoteSource) { + if (stat.id in session.rpcs[UUID].stats) { + session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier; + session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0; + session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0; + session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0; + if ("frameWidth" in stat) { + if ("frameHeight" in stat) { + session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth + " x " + stat.frameHeight; + session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth; + session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight; + } + } + } else { + session.rpcs[UUID].stats[stat.id] = {}; + session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0; + session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0; + session.rpcs[UUID].stats[stat.id].Jitter_Buffer_ms = 0; + session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier; + if (stat.kind && stat.kind == "audio") { + session.rpcs[UUID].stats[stat.id].type = "Audio Track"; + session.rpcs[UUID].stats[stat.id]._type = "audio"; + } else if (stat.kind && stat.kind == "video") { + session.rpcs[UUID].stats[stat.id].type = "Video Track"; + session.rpcs[UUID].stats[stat.id]._type = "video"; + } + } + } else if (stat.type == "transport") { + if ("bytesReceived" in stat) { + if ("_bytesReceived" in session.rpcs[UUID].stats["WHEP_Connection"]) { + if (session.rpcs[UUID].stats["WHEP_Connection"]._timestamp) { + if (stat.timestamp) { + session.rpcs[UUID].stats["WHEP_Connection"].total_recv_bitrate_kbps = parseInt((8 * (stat.bytesReceived - session.rpcs[UUID].stats["WHEP_Connection"]._bytesReceived)) / (stat.timestamp - session.rpcs[UUID].stats["WHEP_Connection"]._timestamp)); + } + } + } + session.rpcs[UUID].stats["WHEP_Connection"]._bytesReceived = stat.bytesReceived; + } + if ("timestamp" in stat) { + session.rpcs[UUID].stats["WHEP_Connection"]._timestamp = stat.timestamp; + if (!session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart) { + session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart = stat.timestamp; + } else { + session.rpcs[UUID].stats["WHEP_Connection"].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats["WHEP_Connection"]._timestampStart) / 600) / 100; + } + } + } else if (stat.type == "inbound-rtp" && trackID) { + session.rpcs[UUID].stats[trackID] = session.rpcs[UUID].stats[trackID] || {}; + + if (stat.trackIdentifier) { + session.rpcs[UUID].stats[trackID]._trackID = stat.trackIdentifier; + } + if ("jitterBufferDelay" in stat) { + session.rpcs[UUID].stats[trackID].Jitter_Buffer_ms = parseInt((1000 * (parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[trackID]._jitter_delay_2)) / (parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[trackID]._jitter_count_2)) || 0; + session.rpcs[UUID].stats[trackID]._jitter_delay_2 = parseFloat(stat.jitterBufferDelay) || 0; + session.rpcs[UUID].stats[trackID]._jitter_count_2 = parseInt(stat.jitterBufferEmittedCount) || 0; + } + + if ("frameWidth" in stat) { + if ("frameHeight" in stat) { + session.rpcs[UUID].stats[trackID].Resolution = stat.frameWidth + " x " + stat.frameHeight; + session.rpcs[UUID].stats[trackID]._frameWidth = stat.frameWidth; + session.rpcs[UUID].stats[trackID]._frameHeight = stat.frameHeight; + } + } + + session.rpcs[UUID].stats[trackID].Bitrate_in_kbps = parseInt((8 * (stat.bytesReceived - (session.rpcs[UUID].stats[trackID]._last_bytes || 0))) / (stat.timestamp - session.rpcs[UUID].stats[trackID]._last_time)); + + session.rpcs[UUID].stats[trackID]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[trackID]._last_bytes; + session.rpcs[UUID].stats[trackID]._last_time = stat.timestamp || session.rpcs[UUID].stats[trackID]._last_time; + + session.rpcs[UUID].stats._codecId = stat.codecId; + session.rpcs[UUID].stats._codecIdTrackId = trackID; + + if (stat.mediaType == "video") { + session.rpcs[UUID].stats[trackID].type = "Video Stream"; + session.rpcs[UUID].stats[trackID]._type = "video"; + if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP8") { + session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0; + session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0; + log("OBS PLI FIX MODE ON"); + if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix) { + // heavy packet loss with no pliCount? + session.requestKeyframe(UUID); + session.rpcs[UUID].stats[trackID].nackTrigger = 0; + log("TRYING KEYFRAME"); + } else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) { + session.rpcs[UUID].stats[trackID].nackTrigger = 0; + } + } else if (session.obsfix && "codec" in session.rpcs[UUID].stats && session.rpcs[UUID].stats.codec == "video/VP9") { + session.rpcs[UUID].stats[trackID].pliDelta = stat.pliCount - session.rpcs[UUID].stats[trackID].keyFramesRequested_pli || 0; + session.rpcs[UUID].stats[trackID].nackTrigger = stat.nackCount - session.rpcs[UUID].stats[trackID].streamErrors_nackCount + session.rpcs[UUID].stats[trackID].nackTrigger || 0; + log("OBS PLI FIX MODE ON"); + if (session.rpcs[UUID].stats[trackID].pliDelta === 0 && session.rpcs[UUID].stats[trackID].nackTrigger >= session.obsfix * 4) { + // heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often. + session.requestKeyframe(UUID); + session.rpcs[UUID].stats[trackID].nackTrigger = 0; + log("TRYING KEYFRAME"); + } else if (session.rpcs[UUID].stats[trackID].pliDelta > 0) { + session.rpcs[UUID].stats[trackID].nackTrigger = 0; + } + } + session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0; + session.rpcs[UUID].stats[trackID].streamErrors_nackCount = stat.nackCount || 0; + //warnlog(stat); + if ("framesPerSecond" in stat) { + session.rpcs[UUID].stats[trackID].FPS = parseInt(stat.framesPerSecond); + } else if ("framesDecoded" in stat && stat.timestamp) { + var lastFramesDecoded = 0; + var lastTimestamp = 0; + try { + lastFramesDecoded = session.rpcs[UUID].stats[trackID]._framesDecoded; + lastTimestamp = session.rpcs[UUID].stats[trackID]._timestamp; + } catch (e) { } + session.rpcs[UUID].stats[trackID].FPS = parseInt((10 * (stat.framesDecoded - lastFramesDecoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10; + //session.rpcs[UUID].stats[trackID].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp)); + session.rpcs[UUID].stats[trackID]._framesDecoded = stat.framesDecoded; + session.rpcs[UUID].stats[trackID]._timestamp = stat.timestamp / 1000; + } + } else if (stat.mediaType == "audio") { + //log("AUDIO LEVEL: "+stat.audioLevel); + session.rpcs[UUID].stats[trackID].type = "Audio Stream"; + session.rpcs[UUID].stats[trackID]._type = "audio"; + if ("audioLevel" in stat) { + session.rpcs[UUID].stats[trackID].audio_level = parseInt(parseFloat(stat.audioLevel) * 10000) / 10000.0; + } + } + + if ("packetsLost" in stat && "packetsReceived" in stat) { + if (!("_packetsLost" in session.rpcs[UUID].stats[trackID])) { + session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost; + } + if (!("_packetsReceived" in session.rpcs[UUID].stats[trackID])) { + session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived; + } + if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[trackID])) { + session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = 0; + } + session.rpcs[UUID].stats[trackID].packetLoss_in_percentage = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage * 0.35 + (0.65 * ((stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost) * 100.0)) / (stat.packetsReceived - session.rpcs[UUID].stats[trackID]._packetsReceived + (stat.packetsLost - session.rpcs[UUID].stats[trackID]._packetsLost)) || 0; + if (session.rpcs[UUID].stats[trackID]._type === "video") { + qos = session.rpcs[UUID].stats[trackID].packetLoss_in_percentage; // packet loss of video track + } + if (session.rpcs[UUID].signalMeter && session.rpcs[UUID].stats[trackID]._type === "video") { + if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.01) { + if (session.rpcs[UUID].stats[trackID].Bitrate_in_kbps == 0) { + session.rpcs[UUID].signalMeter.dataset.level = 0; + } else { + session.rpcs[UUID].signalMeter.dataset.level = 5; + } + } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 0.3) { + session.rpcs[UUID].signalMeter.dataset.level = 4; + } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 1.0) { + session.rpcs[UUID].signalMeter.dataset.level = 3; + } else if (session.rpcs[UUID].stats[trackID].packetLoss_in_percentage < 3.5) { + session.rpcs[UUID].signalMeter.dataset.level = 2; + } else { + session.rpcs[UUID].signalMeter.dataset.level = 1; + } + } + session.rpcs[UUID].stats[trackID]._packetsReceived = stat.packetsReceived; + session.rpcs[UUID].stats[trackID]._packetsLost = stat.packetsLost; + } + } else if ("_codecId" in session.rpcs[UUID].stats && stat.id == session.rpcs[UUID].stats._codecId) { + if ("mimeType" in stat) { + if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]) { + session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType; + } else { + session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {}; + session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType; + } + } + if ("frameHeight" in stat) { + if ("frameWidth" in stat) { + session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth) + " x " + parseInt(stat.frameHeight); + } + } + } else if (Firefox) { + if ("mimeType" in stat && "type" in stat && "id" in stat && stat.type == "codec") { + if (stat.mimeType.includes("video")) { + session.rpcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1]; + } else if (stat.mimeType.includes("audio")) { + session.rpcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1]; + if (stat.clockRate) { + session.rpcs[UUID].stats.audio_clockRate = stat.clockRate; + if (stat.channels) { + session.rpcs[UUID].stats.audio_clockRate += " / " + stat.channels; + } + } else if (stat.sdpFmtpLine) { + session.rpcs[UUID].stats.fmtp = stat.sdpFmtpLine; + } + } + } + + if ("frameWidth" in stat) { + session.rpcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight; + if ("framesPerSecond" in stat) { + session.rpcs[UUID].stats.resolution += " @ " + stat.framesPerSecond; + } + } + + if ("bytesReceived" in stat) { + if ("kind" in stat && stat.kind == "video") { + if ("_bytesReceived_video" in session.rpcs[UUID].stats) { + session.rpcs[UUID].stats.videoBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_video) / ((1024 * session.statsInterval) / 8000)); + } + session.rpcs[UUID].stats._bytesReceived_video = stat.bytesReceived; + } else if ("kind" in stat && stat.kind == "audio") { + if ("_bytesReceived_audio" in session.rpcs[UUID].stats) { + session.rpcs[UUID].stats.audioBitrate_kbps = parseInt((stat.bytesReceived - session.rpcs[UUID].stats._bytesReceived_audio) / ((1024 * session.statsInterval) / 8000)); + } + session.rpcs[UUID].stats._bytesReceived_audio = stat.bytesReceived; + } + } + } + }); + + //////////// + + if (nominatedCandidate) { + if ("currentRoundTripTime" in nominatedCandidate) { + session.rpcs[UUID].stats["WHEP_Connection"].Round_Trip_Time_ms = nominatedCandidate.currentRoundTripTime * 1000; + } + } + + if (nominatedCandidate && nominatedCandidate.remoteCandidateId) { + if (candidates[nominatedCandidate.remoteCandidateId]) { + var candidate = candidates[nominatedCandidate.remoteCandidateId]; + if ("candidateType" in candidate) { + session.rpcs[UUID].stats["WHEP_Connection"].candidateType_remote = candidate.candidateType; + if (candidate.candidateType === "relay") { + if ("relayProtocol" in candidate) { + session.rpcs[UUID].stats["WHEP_Connection"].remote_relay_protocol = candidate.relayProtocol; + } + if ("ip" in candidate) { + session.rpcs[UUID].stats["WHEP_Connection"].remote_relay_IP = candidate.ip; + } + } else { + try { + delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP; + delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol; + } catch (e) { } + } + if ("networkType" in candidate) { + session.rpcs[UUID].stats["WHEP_Connection"].remote_networkType = candidate.networkType; + } + } + } + } + if (nominatedCandidate && nominatedCandidate.localCandidateId) { + if (candidates[nominatedCandidate.localCandidateId]) { + var candidate = candidates[nominatedCandidate.localCandidateId]; + if ("candidateType" in candidate) { + session.rpcs[UUID].stats["WHEP_Connection"].candidateType_local = candidate.candidateType; + if (candidate.candidateType === "relay") { + if ("relayProtocol" in candidate) { + session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol = candidate.relayProtocol; + } + if ("ip" in candidate) { + session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP = candidate.ip; + } + } else { + try { + delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_IP; + delete session.rpcs[UUID].stats["WHEP_Connection"].local_relay_protocol; + } catch (e) { } + } + } + + if ("networkType" in candidate) { + session.rpcs[UUID].stats["WHEP_Connection"].local_networkType = candidate.networkType; + } + } + } + + /* try{ // we want to let meshcast know if our node is getting overloaded, to avoid making it worse + if ((qos!==false) && session.rpcs[UUID].settings && session.rpcs[UUID].settings.url){ + var request = new XMLHttpRequest(); + var node = session.rpcs[UUID].settings.url.split("https://")[1].split(".meshcast.io")[0]; + if (node){ + request.open('POST', " https://qos.meshcast.io/?name="+node); + request.send(qos); + } + } + } catch(e){ + errorlog(e); + } */ + + //if (session.buffer!==false){ + playoutdelay(UUID); // it will handle itself for now on I guess + //} + }); + } catch (e) { + errorlog(e); + } +} + +function safeAppendToMenu(menu, text) { + const li = document.createElement("li"); + const h2 = document.createElement("h2"); + h2.title = text; + h2.textContent = text; // Safely assigns text content, avoiding HTML parsing + li.appendChild(h2); + menu.appendChild(li); +} + +function printMyStats(menu, screenshare = false) { + // see: setupStatsMenu + + if (!session) { + return; + } + + var scrollLeft = getById("menuStatsBox").scrollLeft; + var scrollTop = getById("menuStatsBox").scrollTop; + menu.innerHTML = ""; + + try { + session.stats.outbound_connections = Object.keys(session.pcs).length; + session.stats.inbound_connections = Object.keys(session.rpcs).length; + } catch (e) { } + + try { + var obscam = false; + if (document.querySelector("select#videoSource3")) { + var videoSelect = document.querySelector("select#videoSource3").options; + if (videoSelect.length) { + if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) { + // OBS Virtualcam + obscam = true; + } else if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) { + // OBS Virtualcam + obscam = true; + } + } + } + + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(function (track) { + session.currentCameraConstraints = track.getSettings(); + + if (screen && screen.orientation && screen.orientation.type) { + if (!screen.orientation.type.includes("portrait")) { + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + } else if (!window.matchMedia("(orientation: portrait)").matches) { + // legacy + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + + if (obscam && parseInt(session.currentCameraConstraints.frameRate) == 30) { + session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0); + } else { + var frameRateFPS = session.currentCameraConstraints.frameRate; + if (frameRateFPS) { + session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0) + " @ " + parseInt(frameRateFPS * 100) / 100.0 + "fps"; + } else { + session.stats.video_settings = (session.currentCameraConstraints.width || 0) + "x" + (session.currentCameraConstraints.height || 0); + } + } + }); + } + } catch (e) { + errorlog(e); + } + + function printViewValues(obj, UUID = false) { + if (!document.getElementById("menuStatsBox")) { + return; + } + + var keys = Object.keys(obj).sort(); + + keys.forEach(key => { + if (typeof obj[key] === "object") { + try { + let sanitizedKey = sanitizeChat(key); + if (sanitizedKey === "info") { + sanitizedKey = "Remote Peer Info"; + } + safeAppendToMenu(menu, sanitizedKey); + } catch (e) { + errorlog(e); + } + + try { + printViewValues(obj[key]); + } catch (e) { + errorlog(e); + } + + menu.innerHTML += "
    "; + } + }); + + if (session.promptAccess) { + if (UUID && session.pcs[UUID]) { + menu.innerHTML += ""; + } + } + + if (UUID && session.pcs[UUID] && session.pcs[UUID].restartIce) { + // only show if available + menu.innerHTML += ""; + } + + keys.forEach(key => { + if (typeof obj[key] !== "object") { + if (key.startsWith("_")) { + return; + } + + let unit = ""; + let hint = ""; + + var stat = sanitizeChat(key); + + stat = stat.charAt(0).toUpperCase() + stat.slice(1); + + var value = obj[key]; + if (typeof value == "string") { + value = sanitizeChat(value); + } + + if (value === false) { + return; + } + + if (key == "useragent") { + value = "" + value + ""; + } + + if (key == "local_relay_IP") { + value = "" + value + ""; + } + if (key == "remote_relay_IP") { + value = "" + value + ""; + } + if (key == "watch_URL") { + value = "" + value + ""; + } + if (key == "candidateType_local" && value == "relay") { + stat = "Candidate type - Local"; + value = "💸

    relay server

    "; + } else if (key == "candidateType_remote" && value == "relay") { + stat = "Candidate type - Remote"; + value = "💸

    relay server

    "; + } else if (key == "candidateType_local" && value == "host") { + stat = "Candidate type - Local"; + value = "

    host

    "; + } else if (key == "candidateType_remote" && value == "host") { + stat = "Candidate type - Remote"; + value = "

    host

    "; + } else if (key == "candidateType_local" && value == "srflx") { + stat = "Candidate type - Local"; + value = "

    srflx

    "; + } else if (key == "candidateType_remote" && value == "srflx") { + stat = "Candidate type - Remote"; + value = "

    srflx

    "; + } else if (key == "local_ip_blocking" && value) { + value = "⚠️ You're blocking"; + hint = "no direct p2p connection made because of YOUR browser or system setting"; + } else if (key == "remote_ip_blocking" && value) { + value = "⚠️ They're blocking"; + hint = "no direct p2p connection made because of THEIR browser or system setting"; + } else if (key == "label" && value) { + value = "" + value + ""; + } + + stat = stat.replaceAll("_", " "); + + if (hint) { + menu.innerHTML += "
  • " + stat + "" + value + unit + "
  • "; + } else { + menu.innerHTML += "
  • " + stat + "" + value + unit + "
  • "; + } + } + }); + + if (UUID && session.pcs[UUID]) { + if (session.pcs[UUID].maxBandwidth) { + menu.innerHTML += "
  • max bandwidth target" + session.pcs[UUID].maxBandwidth + "
  • "; + } + if (session.pcs[UUID].setBitrate) { + menu.innerHTML += '
  • init bitrate target" + session.pcs[UUID].setBitrate + "
  • "; + } + if (session.pcs[UUID].savedBitrate) { + menu.innerHTML += "
  • current bitrate target" + session.pcs[UUID].savedBitrate + "
  • "; + } + if (session.showSlider || (!session.roomid && !session.pcs[UUID].whipout && session.meshcast !== "audio")) { + menu.innerHTML += "
  • adjust video bitrate
  • "; + + if (!session.hidehome) { + menu.innerHTML += "
    More info on setting bitrates higher here
    "; + } + } + } + } + + printViewValues(session.stats); + menu.innerHTML += ""; + + if (!screenshare && session.meshcast && session.whipOut && session.whipOut.stats) { + printViewValues({ Meshcast_connection: session.whipOut.stats }); + menu.innerHTML += "
    "; + } else if (!screenshare && session.whipOut && session.whipOut.stats) { + printViewValues({ Whip_Out_connection: session.whipOut.stats }); + menu.innerHTML += "
    "; + } + if (!screenshare && session.whepIn && session.whepIn.stats) { + printViewValues({ Whep_In_connection: session.whepIn.stats }); + menu.innerHTML += "
    "; + } + + for (var uuid in session.pcs) { + if (screenshare) { + if (session.pcs[uuid].realUUID) { + printViewValues(session.pcs[uuid].stats, uuid); + menu.innerHTML += "
    "; + } + } else if (!session.pcs[uuid].realUUID) { + printViewValues(session.pcs[uuid].stats, uuid); + menu.innerHTML += "
    "; + } + } + if (iOS || iPad) { + menu.innerHTML += "
    "; + } + try { + getById("menuStatsBox").scrollLeft = scrollLeft; + getById("menuStatsBox").scrollTop = scrollTop; + } catch (e) { } +} + +function updateLocalStats() { + if (!session) { + return; + } + + var totalBitrate = 0; + var totalBitrate2 = 0; + var cpuLimited = false; + var relayUsed = false; + var totalVideo = 0; + var totalAudio = 0; + var totalScenes = 0; + var meshcastActive = false; + var nackRate = 0; + var totalStreams = 0; + + var miscSenders = []; + + if (session.whipOut && session.whipOut.getSenders && session.whipOut.stats) { + miscSenders.push(session.whipOut); + } + + miscSenders.forEach(data => { + try { + var atot = 0; + var senders = data.getSenders(); // for any connected peer, update the video they have if connected with a video already. + senders.forEach(sender => { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track && sender.track.kind == "video" && sender.track.enabled) { + meshcastActive = true; + } else if (sender.track && sender.track.kind == "audio" && sender.track.enabled && !session.muted) { + meshcastActive = true; + } + }); + //totalAudio += atot; + + if ("video_bitrate_kbps" in data.stats) { + totalBitrate += data.stats.video_bitrate_kbps || 0; + } + if ("audio_bitrate_kbps" in data.stats) { + totalBitrate += data.stats.audio_bitrate_kbps || 0; + } + if ("total_sending_bitrate_kbps" in data.stats) { + totalBitrate2 += data.stats.total_sending_bitrate_kbps || 0; + } + + if ("quality_limitation_reason" in data.stats) { + if (data.stats.quality_limitation_reason == "cpu") { + cpuLimited = true; + } + } + + if ("nacks_per_second" in data.stats) { + nackRate += data.stats.nacks_per_second; + totalStreams += 1; + } + + setTimeout( + function (data) { + if (!data) { + return; + } + data.getStats().then(function (stats) { + if ("audio_bitrate_kbps" in data.stats) { + data.stats.audio_bitrate_kbps = 0; + } + var nominatedCandidate = false; + var candidates = {}; + var ipleakingAllowedRemote = false; + var ipleakingAllowedLocal = false; + stats.forEach(stat => { + if (stat.id && stat.id.startsWith("DEPRECATED_")) { + return; + } + if (stat.type == "transport") { + if ("bytesSent" in stat) { + if ("_bytesSent" in data.stats) { + if (data.stats._timestamp) { + if (stat.timestamp) { + data.stats.total_sending_bitrate_kbps = parseInt((8 * (stat.bytesSent - data.stats._bytesSent)) / (stat.timestamp - data.stats._timestamp)); + } + } + } + data.stats._bytesSent = stat.bytesSent; + } + if ("timestamp" in stat) { + data.stats._timestamp = stat.timestamp; + } + } else if (stat.type == "outbound-rtp") { + if (stat.kind == "video") { + if ("framesPerSecond" in stat) { + data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond; + } else if ("frameHeight" in stat) { + if ("framesEncoded" in stat && stat.timestamp) { + var lastFramesEncoded = 0; + var lastTimestamp = 0; + try { + lastFramesEncoded = data.stats._framesEncoded; + lastTimestamp = data.stats._timestamp; + } catch (e) { } + data.stats._FPS = parseInt((10 * (stat.framesEncoded - lastFramesEncoded)) / (stat.timestamp / 1000 - lastTimestamp)) / 10 || "?"; + data.stats._framesEncoded = stat.framesEncoded; + data.stats._timestamp = stat.timestamp / 1000; + data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + data.stats._FPS; + } else { + data.stats.resolution = stat.frameWidth + " x " + stat.frameHeight; + } + } + if ("encoderImplementation" in stat) { + data.stats.video_encoder = stat.encoderImplementation; + if (stat.encoderImplementation == "ExternalEncoder") { + data.stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly + data.encoder = true; + } else if (stat.encoderImplementation == "MediaFoundationVideoEncodeAccelerator") { + data.stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly + data.encoder = true; + } else { + data.encoder = false; // this may not be actually accurate, but lets assume so. + } + } + if ("qualityLimitationReason" in stat) { + if (data.stats.quality_limitation_reason) { + if (data.stats.quality_limitation_reason !== stat.qualityLimitationReason) { + try { + var miniInfo = {}; + miniInfo.qlr = stat.qualityLimitationReason; + if ("_hardwareEncoder" in data.stats) { + miniInfo.hw_enc = data.stats._hardwareEncoder; + } else { + miniInfo.hw_enc = null; + } + session.sendMessage({ miniInfo: miniInfo }); + } catch (e) { + warnlog(e); + } + } + } + data.stats.quality_limitation_reason = stat.qualityLimitationReason; + } + if ("bytesSent" in stat) { + if ("_bytesSentVideo" in data.stats) { + if (data.stats._timestamp1) { + data.stats.video_bitrate_kbps = parseInt((8 * (stat.bytesSent - data.stats._bytesSentVideo)) / (stat.timestamp - data.stats._timestamp1)); + if (stat.timestamp) { + } + } + } + data.stats._bytesSentVideo = stat.bytesSent; + } + if ("nackCount" in stat) { + if ("_nackCount" in data.stats) { + if (data.stats._timestamp1) { + if (stat.timestamp) { + data.stats.nacks_per_second = parseInt((10000 * (stat.nackCount - data.stats._nackCount)) / (stat.timestamp - data.stats._timestamp1)) / 10; + } + } + } + } + if ("retransmittedBytesSent" in stat) { + if ("_retransmittedBytesSent" in data.stats) { + if (data.stats._timestamp1) { + if (stat.timestamp) { + data.stats.retransmitted_kbps = parseInt((8 * (stat.retransmittedBytesSent - data.stats._retransmittedBytesSent)) / (stat.timestamp - data.stats._timestamp1)); + } + } + } + } + if ("nackCount" in stat) { + data.stats._nackCount = stat.nackCount; + } + if ("retransmittedBytesSent" in stat) { + data.stats._retransmittedBytesSent = stat.retransmittedBytesSent; + } + if ("timestamp" in stat) { + data.stats._timestamp1 = stat.timestamp; + } + if ("pliCount" in stat) { + data.stats.total_pli_count = stat.pliCount; + } + if ("keyFramesEncoded" in stat) { + data.stats.total_key_frames_encoded = stat.keyFramesEncoded; + } + } else if (stat.kind == "audio") { + if ("bytesSent" in stat) { + if (data.stats._bytesSentAudio) { + if (data.stats._timestamp2) { + if (stat.timestamp) { + if ("audio_bitrate_kbps" in data.stats) { + data.stats.audio_bitrate_kbps += parseInt((8 * (stat.bytesSent - data.stats._bytesSentAudio)) / (stat.timestamp - data.stats._timestamp2)); + } else { + data.stats.audio_bitrate_kbps = 0; + } + } + } + } + } + if ("timestamp" in stat) { + data.stats._timestamp2 = stat.timestamp; + } + if ("bytesSent" in stat) { + data.stats._bytesSentAudio = stat.bytesSent; + } + } + } else if (stat.type == "remote-candidate") { + candidates[stat.id] = stat; + if (stat.candidateType != "relay") { + ipleakingAllowedRemote = true; + } + } else if (stat.type == "local-candidate") { + candidates[stat.id] = stat; + if (stat.candidateType != "relay") { + ipleakingAllowedLocal = true; + } + } else if (stat.type == "candidate-pair" && stat.nominated) { + if (!nominatedCandidate) { + nominatedCandidate = stat; + } else if (nominatedCandidate.priority < stat.priority) { + nominatedCandidate = stat; + } + } else if ("mimeType" in stat && "type" in stat && stat.type == "codec") { + if (stat.mimeType.includes("video")) { + data.stats.video_codec = stat.mimeType.split("video/")[1]; + } else if (stat.mimeType.includes("audio")) { + data.stats.audio_codec = stat.mimeType.split("audio/")[1]; + if (stat.clockRate) { + data.stats.audio_clockRate = stat.clockRate; + if (stat.channels) { + data.stats.audio_clockRate += " / " + stat.channels; + } + } else if (stat.sdpFmtpLine) { + data.stats.fmtp = stat.sdpFmtpLine; + } + } + } + return; + }); + if (nominatedCandidate) { + if ("availableOutgoingBitrate" in nominatedCandidate) { + data.stats.available_outgoing_bitrate_kbps = parseInt(nominatedCandidate.availableOutgoingBitrate / 1024); + if (session.maxBandwidth !== false) { + session.limitMaxBandwidth(data.stats.available_outgoing_bitrate_kbps, session.pcs[UUID], false); + } + } + if ("totalRoundTripTime" in nominatedCandidate) { + if ("responsesReceived" in nominatedCandidate) { + data.stats.average_roundTripTime_ms = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000); + } + } + } + if (nominatedCandidate && nominatedCandidate.remoteCandidateId) { + if (candidates[nominatedCandidate.remoteCandidateId]) { + var candidate = candidates[nominatedCandidate.remoteCandidateId]; + if ("candidateType" in candidate) { + data.stats.candidateType_remote = candidate.candidateType; + if (candidate.candidateType === "relay") { + if ("ip" in candidate) { + data.stats.remote_relay_IP = candidate.ip; + } + if ("relayProtocol" in candidate) { + data.stats.remote_relay_protocol = candidate.relayProtocol; + } + data.stats.remote_ip_blocking = !ipleakingAllowedRemote; + } else { + try { + delete data.stats.remote_relay_IP; + delete data.stats.remote_relay_protocol; + delete data.stats.remote_ip_blocking; + } catch (e) { } + } + } + } + } + if (nominatedCandidate && nominatedCandidate.localCandidateId) { + if (candidates[nominatedCandidate.localCandidateId]) { + var candidate = candidates[nominatedCandidate.localCandidateId]; + if ("candidateType" in candidate) { + data.stats.candidateType_local = candidate.candidateType; + if (candidate.candidateType === "relay") { + if ("ip" in candidate) { + data.stats.local_relay_IP = candidate.ip; + } + if ("relayProtocol" in candidate) { + data.stats.local_relay_protocol = candidate.relayProtocol; + } + data.stats.local_ip_blocking = !ipleakingAllowedLocal; + } else { + try { + delete data.stats.local_relay_IP; + delete data.stats.local_relay_protocol; + delete data.stats.local_ip_blocking; + } catch (e) { } + } + } + } + } + return; + }); + }, + 0, + data + ); + } catch (e) { + errorlog(e); + } + }); + + for (var uuid in session.pcs) { + if (!session.pcs[uuid].stats) { + continue; + } + + var atot = 0; + var senders = getSenders2(uuid); // for any connected peer, update the video they have if connected with a video already. + senders.forEach(sender => { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track && sender.track.kind == "video" && sender.track.enabled) { + totalVideo += 1; + } else if (sender.track && sender.track.kind == "audio" && sender.track.enabled && !session.muted) { + atot = 1; + } + }); + totalAudio += atot; + + if ("scene" in session.pcs[uuid]) { + if (session.pcs[uuid].scene !== false) { + totalScenes += 1; + } + } + + if ("video_bitrate_kbps" in session.pcs[uuid].stats) { + totalBitrate += session.pcs[uuid].stats.video_bitrate_kbps || 0; + } + if ("audio_bitrate_kbps" in session.pcs[uuid].stats) { + totalBitrate += session.pcs[uuid].stats.audio_bitrate_kbps || 0; + } + if ("total_sending_bitrate_kbps" in session.pcs[uuid].stats) { + totalBitrate2 += session.pcs[uuid].stats.total_sending_bitrate_kbps || 0; + } + + if ("quality_limitation_reason" in session.pcs[uuid].stats) { + if (session.pcs[uuid].stats.quality_limitation_reason == "cpu") { + cpuLimited = true; + } + } + + if ("nacks_per_second" in session.pcs[uuid].stats) { + nackRate += session.pcs[uuid].stats.nacks_per_second; + totalStreams += 1; + } + + if (uuid in session.rpcs) { + if (session.pcs[uuid].stats.label) { + session.pcs[uuid].stats.label = session.rpcs[uuid].label; + } + if (session.pcs[uuid].stats.streamID) { + session.pcs[uuid].stats.streamID = session.rpcs[uuid].streamID; + } + } + + var screenTracksIds = []; + if (session.screenStream !== false) { + // null if already used. false if never used. + session.screenStream.getTracks().forEach(trk => { + screenTracksIds.push(trk.id); + }); + } + + setTimeout( + function (UUID) { + if (!session.pcs[UUID]) { + return; + } + + if (session.pcs[UUID].realUUID && session.pcs[session.pcs[UUID].realUUID]) { + var thisIsAlt = true; + var node = session.pcs[session.pcs[UUID].realUUID]; + } else { + var thisIsAlt = false; + var node = session.pcs[UUID]; + } + + node.getStats().then(function (stats) { + if (!(UUID in session.pcs)) { + return; + } + + if ("audio_bitrate_kbps" in session.pcs[UUID].stats) { + session.pcs[UUID].stats.audio_bitrate_kbps = 0; + } + + var nominatedCandidate = false; + var candidates = {}; + var ipleakingAllowedRemote = false; + var ipleakingAllowedLocal = false; + + var statObject = []; + var altStreamList = {}; + stats.forEach(stat => { + statObject.push(stat); + if (screenTracksIds.includes(stat.trackIdentifier)) { + altStreamList[stat.id] = stat.trackIdentifier; + } + }); + + statObject.forEach(stat => { + if (stat.id && stat.id.startsWith("DEPRECATED_")) { + return; + } + if (stat.type == "transport") { + if ("bytesSent" in stat) { + if ("_bytesSent" in session.pcs[UUID].stats) { + if (session.pcs[UUID].stats._timestamp3) { + if (stat.timestamp) { + session.pcs[UUID].stats.total_sending_bitrate_kbps = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSent)) / (stat.timestamp - session.pcs[UUID].stats._timestamp3)); + } + } + } + session.pcs[UUID].stats._bytesSent = stat.bytesSent; + } + if ("timestamp" in stat) { + session.pcs[UUID].stats._timestamp3 = stat.timestamp; + } + } else if (stat.type == "outbound-rtp") { + if (thisIsAlt && stat.mediaSourceId && !altStreamList[stat.mediaSourceId]) { + // this isn't an alt stream, but we are in alt mode + return; + } else if (!thisIsAlt && stat.mediaSourceId && altStreamList[stat.mediaSourceId]) { + // this is an alt stream, but we are not in alt mode + return; + } + if (stat.kind == "video") { + if ("framesPerSecond" in stat) { + session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond; + } else if ("frameHeight" in stat) { + if ("framesEncoded" in stat && stat.timestamp) { + var lastFramesEncoded = 0; + var lastTimestamp = 0; + try { + lastFramesEncoded = session.pcs[UUID].stats._framesEncoded; + lastTimestamp = session.pcs[UUID].stats._timestamp; + } catch (e) { } + session.pcs[UUID].stats._FPS = parseInt((10 * (stat.framesEncoded - lastFramesEncoded)) / (stat.timestamp - lastTimestamp)) / 10; + session.pcs[UUID].stats._framesEncoded = stat.framesEncoded; + session.pcs[UUID].stats._timestamp = stat.timestamp; + session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + session.pcs[UUID].stats._FPS; + } else { + session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight; + } + } + var miniInfo = {}; + var sendMini = false; + if ("encoderImplementation" in stat) { + session.pcs[UUID].stats.video_encoder = stat.encoderImplementation; + if (stat.encoderImplementation == "ExternalEncoder") { + session.pcs[UUID].stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly + if (session.pcs[UUID].encoder !== true) { + session.pcs[UUID].encoder = true; + miniInfo.hw_enc = true; + sendMini = true; + } + } else if (stat.encoderImplementation == "MediaFoundationVideoEncodeAccelerator") { + session.pcs[UUID].stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly + if (session.pcs[UUID].encoder !== true) { + session.pcs[UUID].encoder = true; + miniInfo.hw_enc = true; + sendMini = true; + } + } else { + if (session.pcs[UUID].encoder === true) { + session.pcs[UUID].encoder = false; + miniInfo.hw_enc = false; + sendMini = true; + } + } + } + if ("qualityLimitationReason" in stat) { + if (session.pcs[UUID].stats.quality_limitation_reason) { + if (session.pcs[UUID].stats.quality_limitation_reason !== stat.qualityLimitationReason) { + try { + sendMini = true; + miniInfo.qlr = stat.qualityLimitationReason; + } catch (e) { + warnlog(e); + } + } + } + session.pcs[UUID].stats.quality_limitation_reason = stat.qualityLimitationReason; + } + if (sendMini) { + session.sendMessage({ miniInfo: miniInfo }, UUID); + } + if ("bytesSent" in stat) { + if ("_bytesSentVideo" in session.pcs[UUID].stats) { + if (session.pcs[UUID].stats._timestamp1) { + if (stat.timestamp) { + session.pcs[UUID].stats.video_bitrate_kbps = parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSentVideo)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1)); + } + } + } + session.pcs[UUID].stats._bytesSentVideo = stat.bytesSent; + } + if ("nackCount" in stat) { + if ("_nackCount" in session.pcs[UUID].stats) { + if (session.pcs[UUID].stats._timestamp1) { + if (stat.timestamp) { + session.pcs[UUID].stats.nacks_per_second = parseInt((10000 * (stat.nackCount - session.pcs[UUID].stats._nackCount)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1)) / 10; + } + } + } + } + if ("retransmittedBytesSent" in stat) { + if ("_retransmittedBytesSent" in session.pcs[UUID].stats) { + if (session.pcs[UUID].stats._timestamp1) { + if (stat.timestamp) { + session.pcs[UUID].stats.retransmitted_kbps = parseInt((8 * (stat.retransmittedBytesSent - session.pcs[UUID].stats._retransmittedBytesSent)) / (stat.timestamp - session.pcs[UUID].stats._timestamp1)); + } + } + } + } + if ("nackCount" in stat) { + session.pcs[UUID].stats._nackCount = stat.nackCount; + } + if ("retransmittedBytesSent" in stat) { + session.pcs[UUID].stats._retransmittedBytesSent = stat.retransmittedBytesSent; + } + if ("timestamp" in stat) { + session.pcs[UUID].stats._timestamp1 = stat.timestamp; + } + if ("pliCount" in stat) { + session.pcs[UUID].stats.total_pli_count = stat.pliCount; + } + if ("keyFramesEncoded" in stat) { + session.pcs[UUID].stats.total_key_frames_encoded = stat.keyFramesEncoded; + } + } else if (stat.kind == "audio") { + if ("bytesSent" in stat) { + if (session.pcs[UUID].stats._bytesSentAudio) { + if (session.pcs[UUID].stats._timestamp2) { + if (stat.timestamp) { + if ("audio_bitrate_kbps" in session.pcs[UUID].stats) { + session.pcs[UUID].stats.audio_bitrate_kbps += parseInt((8 * (stat.bytesSent - session.pcs[UUID].stats._bytesSentAudio)) / (stat.timestamp - session.pcs[UUID].stats._timestamp2)); + } else { + session.pcs[UUID].stats.audio_bitrate_kbps = 0; + } + } + } + } + } + if ("timestamp" in stat) { + session.pcs[UUID].stats._timestamp2 = stat.timestamp; + } + if ("bytesSent" in stat) { + session.pcs[UUID].stats._bytesSentAudio = stat.bytesSent; + } + } + } else if (stat.type == "remote-candidate") { + candidates[stat.id] = stat; + if (stat.candidateType != "relay") { + ipleakingAllowedRemote = true; + } + } else if (stat.type == "local-candidate") { + candidates[stat.id] = stat; + if (stat.candidateType != "relay") { + ipleakingAllowedLocal = true; + } + } else if (stat.type == "candidate-pair" && stat.nominated) { + if (!nominatedCandidate) { + nominatedCandidate = stat; + } else if (nominatedCandidate.priority < stat.priority) { + nominatedCandidate = stat; + } + } else if ("mimeType" in stat && "type" in stat && stat.type == "codec") { + if (stat.mimeType.includes("video")) { + session.pcs[UUID].stats.video_codec = stat.mimeType.split("video/")[1]; + } else if (stat.mimeType.includes("audio")) { + session.pcs[UUID].stats.audio_codec = stat.mimeType.split("audio/")[1]; + if (stat.clockRate) { + session.pcs[UUID].stats.audio_clockRate = stat.clockRate; + if (stat.channels) { + session.pcs[UUID].stats.audio_clockRate += " / " + stat.channels; + } + } else if (stat.sdpFmtpLine) { + session.pcs[UUID].stats.fmtp = stat.sdpFmtpLine; + } + } + } + return; + }); + + if (nominatedCandidate) { + if ("availableOutgoingBitrate" in nominatedCandidate) { + session.pcs[UUID].stats.available_outgoing_bitrate_kbps = parseInt(nominatedCandidate.availableOutgoingBitrate / 1024); + if (session.maxBandwidth !== false) { + session.limitMaxBandwidth(session.pcs[UUID].stats.available_outgoing_bitrate_kbps, session.pcs[UUID], false); + } + } + if ("totalRoundTripTime" in nominatedCandidate) { + if ("responsesReceived" in nominatedCandidate) { + session.pcs[UUID].stats.average_roundTripTime_ms = parseInt((nominatedCandidate.totalRoundTripTime / nominatedCandidate.responsesReceived) * 1000); + } + } + } + if (nominatedCandidate && nominatedCandidate.remoteCandidateId) { + if (candidates[nominatedCandidate.remoteCandidateId]) { + var candidate = candidates[nominatedCandidate.remoteCandidateId]; + if ("candidateType" in candidate) { + session.pcs[UUID].stats.candidateType_remote = candidate.candidateType; + if (candidate.candidateType === "relay") { + if ("ip" in candidate) { + session.pcs[UUID].stats.remote_relay_IP = candidate.ip; + } + if ("relayProtocol" in candidate) { + session.pcs[UUID].stats.remote_relay_protocol = candidate.relayProtocol; + } + session.pcs[UUID].stats.remote_ip_blocking = !ipleakingAllowedRemote; + } else { + try { + delete session.pcs[UUID].stats.remote_relay_IP; + delete session.pcs[UUID].stats.remote_relay_protocol; + delete session.pcs[UUID].stats.remote_ip_blocking; + } catch (e) { } + } + } + } + } + if (nominatedCandidate && nominatedCandidate.localCandidateId) { + if (candidates[nominatedCandidate.localCandidateId]) { + var candidate = candidates[nominatedCandidate.localCandidateId]; + if ("candidateType" in candidate) { + session.pcs[UUID].stats.candidateType_local = candidate.candidateType; + if (candidate.candidateType === "relay") { + if ("ip" in candidate) { + session.pcs[UUID].stats.local_relay_IP = candidate.ip; + } + if ("relayProtocol" in candidate) { + session.pcs[UUID].stats.local_relay_protocol = candidate.relayProtocol; + } + session.pcs[UUID].stats.local_ip_blocking = !ipleakingAllowedLocal; + } else { + try { + delete session.pcs[UUID].stats.local_relay_IP; + delete session.pcs[UUID].stats.local_relay_protocol; + delete session.pcs[UUID].stats.local_ip_blocking; + } catch (e) { } + } + } + } + } + + return; + }); + }, + 0, + uuid + ); + } + + try { + var totalCon = Object.keys(session.pcs).length || 0; + var headerStats = "🔗 "; + headerStats += totalCon; + if (meshcastActive) { + if (totalAudio) { + headerStats += ", 👂 " + totalAudio; + } + if (totalVideo) { + headerStats += ", 👀 " + totalVideo; + } + headerStats += ", 📡Broadcast"; + } else { + headerStats += ", 👂 " + totalAudio; + headerStats += ", 👀 " + totalVideo; + } + if (session.roomid) { + headerStats += ", 🎬 " + totalScenes + ""; + } + + var changed = false; + if (!session.info.out) { + session.info.out = {}; + session.info.out.v = totalVideo; + session.info.out.a = totalAudio; + session.info.out.c = totalCon; + session.info.out.s = totalScenes; + changed = true; + } else { + if (session.info.out.a !== totalAudio) { + session.info.out.a = totalAudio; + // changed = true; // I'm not sending this data, so why bother + } + if (session.info.out.v !== totalVideo) { + session.info.out.v = totalAudio; + //changed = true; // I'm not sending this data, so why bother + } + if (session.info.out.c !== totalCon) { + if (session.info.out.c) { + changed = true; // update if I'm not the first one + } + session.info.out.c = totalCon; + } + if (session.info.out.s !== totalScenes) { + if (session.info.out.s) { + changed = true; // update if I'm not the first one + } + session.info.out.s = totalScenes; + } + } + } catch (e) { } + //session.info.out = {}; + + var uploadQuality = nackRate / totalStreams || 0; + if (totalStreams === 0) { + uploadQuality = "title='Connection seems good' style='color: transparent; text-shadow: 0 0 0 #4d9bff;'"; + } else if (uploadQuality === 0) { + uploadQuality = "title='Connection seems good' style='color: transparent; text-shadow: 0 0 0 #0F0;'"; + } else if (uploadQuality <= 1) { + uploadQuality = "title='Mild connection issues' style='color: transparent; text-shadow: 0 0 0 yellow;'"; + } else if (uploadQuality <= 5) { + uploadQuality = "title='Moderate connection issues' style='color: transparent; text-shadow: 0 0 0 orange;'"; + } else { + uploadQuality = "title='Severe connection issues' style='color: transparent; text-shadow: 0 0 0 #F00;'"; + } + + if (Firefox && totalBitrate === 0 && totalBitrate2 === 0) { + // does not support the current stats system + } else if (totalBitrate > totalBitrate2) { + headerStats += ", 🔺 " + Math.round(totalBitrate / 10.24) / 100 + "-Mbps"; + } else if (totalBitrate2 > 1000) { + headerStats += ", 🔺 " + Math.round(totalBitrate2 / 10.24) / 100 + "-Mbps
    "; + } else { + headerStats += ", 🔺 " + totalBitrate2 + "-kbps"; + } + + if (session.director || !session.roomid) { + // show stats if the director or if not in a group room + if (cpuLimited) { + headerStats += ", 🔥 CPU Overloaded"; + } + //if (relayUsed){ + // headerStats += " 💸"; + //} + + directorGraphStats(); + } + + var miniInfo = {}; + if (changed) { + miniInfo.out = {}; + miniInfo.out.c = session.info.out.c; + } + + if (session.cpuLimited !== cpuLimited) { + session.cpuLimited = cpuLimited; + miniInfo.cpu = cpuLimited; + changed = true; + } + + if (changed) { + for (var uuid in session.pcs) { + session.sendMessage({ miniInfo: miniInfo }, uuid); // lets send it to everyone. + } + } + + if (document.getElementById("head5")) { + try { + if (Object.keys(session.pcs).length || meshcastActive) { + document.getElementById("head5").classList.remove("hidden"); + } + } catch (e) { } + document.getElementById("head5").innerHTML = headerStats; + //miniTranslate(document.getElementById("head5")); + document.getElementById("head5").onclick = function () { + var [menu, innerMenu] = statsMenuCreator(); + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + printMyStats(innerMenu); + }; + } +} + +function updateStats(obsvc = false) { + if (document.getElementById("previewWebcam")) { + var ele = document.getElementById("previewWebcam"); + var wcs = "webcamstats"; + } else if (document.getElementById("videosource")) { + var ele = document.getElementById("videosource"); + var wcs = "webcamstats3"; + } else { + return; + } + + try { + getById(wcs).innerHTML = ""; + ele.srcObject.getVideoTracks().forEach(function (track) { + if (obsvc && parseInt(track.getSettings().frameRate) == 30) { + getById(wcs).innerHTML = "Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + " @ up to 60fps"; + } else { + var frameRateFPS = track.getSettings().frameRate; + if (frameRateFPS) { + getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + "@" + parseInt(frameRateFPS * 100) / 100.0 + "fps"; + } else { + getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0); + } + } + }); + } catch (e) { + errorlog(e); + } +} + +function toggleControlBar() { + if (!getById("controlButtons").classList.contains("hidden")) { + getById("controlButtons").dataset.enabled = true; + getById("controlButtons").classList.add("hidden"); + } else if (getById("controlButtons").dataset.enabled) { + getById("controlButtons").classList.remove("hidden"); + delete getById("controlButtons").dataset.enabled; + } +} + +function toggleMute(apply = false, event = false) { + // TODO: I need to have this be MUTE, toggle, with volume not touched. + + var mouseUp = null; + var touchEnd = null; + var timeStart = Date.now(); + if (event) { + mouseUp = document.onmouseup; + touchEnd = document.ontouchend; + document.onmouseup = function () { + document.onmouseup = mouseUp; + document.ontouchend = touchEnd; + if (Date.now() - timeStart < 500) { + return; + } else { + toggleMute(); + } + }; + document.ontouchend = function () { + document.onmouseup = mouseUp; + document.ontouchend = touchEnd; + if (Date.now() - timeStart < 300) { + return; + } else { + toggleMute(); + } + }; + } + + if (session.director) { + if (!session.directorEnabledPPT) { + log("Director doesn't have PPT enabled yet"); + // director has not enabled PTT yet. + return; + } + } + + if (apply) { + session.muted = !session.muted; // we flip here as we are going to flip again in a second. + } + //try{var ptt = getById("press2talk");} catch(e){var ptt=false;} + + if (session.muted == false) { + session.muted = true; + if (session.pendingMicRefreshTimeout) { + clearTimeout(session.pendingMicRefreshTimeout); + session.pendingMicRefreshTimeout = null; + } + getById("mutetoggle").className = "las la-microphone-slash toggleSize"; + if (!session.cleanOutput) { + getById("mutebutton").classList.add("red", "pulsate"); + getById("mutebutton").ariaPressed = "true"; + getById("header").classList.add("red"); + + if (session.localMuteElement) { + session.localMuteElement.style.display = "block"; + } + } + if (session.streamSrc) { + session.streamSrc.getAudioTracks().forEach(track => { + track.enabled = false; + }); + } + if ((window.obsstudio || session.mobile) && session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getAudioTracks().forEach(track => { + track.enabled = false; + }); + } + } else { + session.muted = false; + getById("mutetoggle").className = "las la-microphone toggleSize"; + if (!session.cleanOutput) { + getById("mutebutton").classList.remove("red", "pulsate"); + getById("mutebutton").ariaPressed = "false"; + getById("header").classList.remove("red"); + + if (session.localMuteElement) { + session.localMuteElement.style.display = "none"; + } + } + if (session.streamSrc) { + session.streamSrc.getAudioTracks().forEach(track => { + track.enabled = true; + }); + } + //if (session.mobile) { + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getAudioTracks().forEach(track => { + track.enabled = true; + }); + } + if (!apply && event && (iOS || iPad || SafariVersion)) { + if (session.pendingMicRefreshTimeout) { + clearTimeout(session.pendingMicRefreshTimeout); + } + session.pendingMicRefreshTimeout = setTimeout(() => { + session.pendingMicRefreshTimeout = null; + if (!session.muted) { + refreshMicrophoneDevice(); // refresh mic only if still unmuted + } + }, 150); + } + //} + + // toggleMute(false, event) + + //if (ptt){ + // ptt.innerHTML = "🔴 Push to Mute"; + //} + } + + try { + postMessageIframe(document.getElementById("screensharesource"), { mic: !session.muted }); + } catch (e) { } + + if (!apply) { + // only if they are changing states do we bother to spam. + var data = {}; + data.muteState = session.muted; + session.sendMessage(data); + log("SEND MUTE STATE TO PEERS"); + pokeIframeAPI("mic-mute-state", session.muted); + pokeAPI("muted", session.muted); + } +} + +function postMessageIframe(iFrameEle, message) { + // iframes seem to only have the contentWindow work on the last placed iframe object, so this checks the dom first. + if (iFrameEle && iFrameEle.nodeName == "IFRAME") { + try { + if (iFrameEle.id && document.getElementById(iFrameEle.id)) { + document.getElementById(iFrameEle.id).contentWindow.postMessage(message, "*"); + } else { + iFrameEle.contentWindow.postMessage(message, "*"); + } + } catch (e) { + errorlog(e); + } + } +} + +function toggleSpeakerMute(apply = false) { + if (session.ignoreNextSpeakerToggle) { + session.ignoreNextSpeakerToggle = false; + return; + } + + closeSpeakerVolumePanel(); + + // TODO: I need to have this be MUTE, toggle, with volume not touched. + + if (CtrlPressed) { + resetupAudioOut(); + } + + if (apply) { + session.speakerMuted = !session.speakerMuted; + } + if (session.speakerMuted == false) { + // mute output + session.speakerMuted = true; + getById("mutespeakertoggle").className = "las la-volume-mute toggleSize"; + if (!session.cleanOutput) { + getById("mutespeakerbutton").className = "float red"; + } + var sounds = document.getElementsByTagName("video"); + + if (iOS || iPad) { + for (var i = 0; i < sounds.length; ++i) { + if (sounds[i].id === "keepAlivePlayer") { + // we need to keep this unmuted + continue; + } + sounds[i].muted = !sounds[i].muted; + sounds[i].muted = session.speakerMuted; + } + } else { + for (var i = 0; i < sounds.length; ++i) { + if (sounds[i].id === "keepAlivePlayer") { + // we need to keep this unmuted + continue; + } + sounds[i].muted = session.speakerMuted; + } + } + } else { + session.speakerMuted = false; // unmute output + + getById("mutespeakertoggle").className = "las la-volume-up toggleSize"; + if (!session.cleanOutput) { + getById("mutespeakerbutton").className = "float"; + } + var sounds = document.getElementsByTagName("video"); + + if (iOS || iPad) { + // attempting to fix an iOS bug + for (var i = 0; i < sounds.length; ++i) { + sounds[i].muted = !sounds[i].muted; + if (sounds[i].id === "videosource") { + // don't unmute ourselves. feedback galore if so. + sounds[i].muted = true; + continue; + } else if (sounds[i].id === "previewWebcam") { + sounds[i].muted = true; + continue; + } else if (sounds[i].id === "screensharesource") { + sounds[i].muted = true; + continue; + } else if (sounds[i].id === "screenshare") { + // this is a webcam + sounds[i].muted = true; + continue; + } else { + sounds[i].muted = session.speakerMuted; + } + } + } else { + for (var i = 0; i < sounds.length; ++i) { + if (sounds[i].id === "videosource") { + // don't unmute ourselves. feedback galore if so. + continue; + } else if (sounds[i].id === "screensharesource") { + // don't unmute ourselves. feedback galore if so. + continue; + } else if (sounds[i].id === "previewWebcam") { + continue; + } else if (sounds[i].id === "screenshare") { + // this is a webm + continue; + } else { + sounds[i].muted = session.speakerMuted; + } + } + } + } + + for (var UUID in session.rpcs) { + applyMuteState(UUID); + postMessageIframe(session.rpcs[UUID].iframeEle, { mute: session.speakerMuted }); + } + + pokeIframeAPI("audio-mute-state", session.speakerMuted); + + if (!apply) { + pokeAPI("speakerMuted", session.speakerMuted); + } + + if (iOS || iPad) { + resetupAudioOut(); + } +} + +const SPEAKER_VOLUME_HOLD_DELAY = 400; +const SPEAKER_VOLUME_MIN_PERCENT = 1; +const SPEAKER_VOLUME_MAX_PERCENT = 100; +var speakerVolumeButton = null; +var speakerVolumePanelElement = null; +var speakerVolumeSliderElement = null; +var speakerVolumeValueElement = null; +var speakerVolumeHoldTimer = null; +var speakerVolumePanelVisible = false; +var speakerVolumeDocumentListenersActive = false; + +function clampSpeakerVolumePercent(percent) { + percent = parseInt(percent, 10); + if (isNaN(percent)) { + percent = SPEAKER_VOLUME_MAX_PERCENT; + } + if (percent < SPEAKER_VOLUME_MIN_PERCENT) { + percent = SPEAKER_VOLUME_MIN_PERCENT; + } + if (percent > SPEAKER_VOLUME_MAX_PERCENT) { + percent = SPEAKER_VOLUME_MAX_PERCENT; + } + return percent; +} + +function getCurrentSpeakerVolumePercent() { + if (typeof session.volume === "number" && !isNaN(session.volume)) { + return clampSpeakerVolumePercent(Math.round(session.volume * 100)); + } + return SPEAKER_VOLUME_MAX_PERCENT; +} + +function convertSpeakerPercentToVolume(percent) { + return clampSpeakerVolumePercent(percent) / 100; +} + +function updateSpeakerVolumeSliderUI(volume) { + if (!speakerVolumeSliderElement) { + return; + } + var effectiveVolume = typeof volume === "number" && !isNaN(volume) ? volume : 1; + if (effectiveVolume < SPEAKER_VOLUME_MIN_PERCENT / 100) { + effectiveVolume = SPEAKER_VOLUME_MIN_PERCENT / 100; + } + if (effectiveVolume > 1) { + effectiveVolume = 1; + } + var percent = clampSpeakerVolumePercent(Math.round(effectiveVolume * 100)); + speakerVolumeSliderElement.value = percent; + if (speakerVolumeValueElement) { + speakerVolumeValueElement.textContent = percent + "%"; + } +} + +function setSessionPlaybackVolume(volume, target) { + if (typeof volume !== "number" || isNaN(volume)) { + return; + } + if (volume > 1) { + volume = 1; + } + if (volume < 0) { + volume = 0; + } + + session.volume = volume; + + var applyToAll = !target || target === "*" || typeof target === "undefined"; + + if (applyToAll) { + if (session.videoElement && typeof session.videoElement.volume === "number") { + try { + session.videoElement.volume = volume; + } catch (e) { + errorlog(e); + } + } + try { + var mediaElements = document.querySelectorAll("video, audio"); + for (var i = 0; i < mediaElements.length; i++) { + var media = mediaElements[i]; + if (!media || typeof media.volume !== "number") { + continue; + } + if (media.dataset && media.dataset.keepVolume === "1") { + continue; + } + media.volume = volume; + } + } catch (e) { + errorlog(e); + } + if (session.screenShareElement && typeof session.screenShareElement.volume === "number") { + try { + session.screenShareElement.volume = volume; + } catch (e) { + errorlog(e); + } + } + } + + for (var UUID in session.rpcs) { + if (!Object.prototype.hasOwnProperty.call(session.rpcs, UUID)) { + continue; + } + try { + var peer = session.rpcs[UUID]; + if (!peer || !peer.videoElement) { + continue; + } + if (!applyToAll && target && target !== "*" && peer.streamID && peer.streamID !== target) { + continue; + } + peer.videoElement.volume = volume; + } catch (e) { + errorlog(e); + } + } + + updateSpeakerVolumeSliderUI(volume); +} + +function handleSpeakerVolumeSliderInput(event) { + if (!event || !event.target) { + return; + } + var percent = clampSpeakerVolumePercent(event.target.value); + var volume = convertSpeakerPercentToVolume(percent); + setSessionPlaybackVolume(volume, "*"); +} + +function clearSpeakerVolumeHoldTimer() { + if (speakerVolumeHoldTimer) { + clearTimeout(speakerVolumeHoldTimer); + speakerVolumeHoldTimer = null; + } +} + +function openSpeakerVolumePanel() { + clearSpeakerVolumeHoldTimer(); + if (!speakerVolumePanelElement || speakerVolumePanelVisible) { + return; + } + + updateSpeakerVolumeSliderUI(typeof session.volume === "number" ? session.volume : 1); + + speakerVolumePanelElement.classList.remove("hidden"); + speakerVolumePanelElement.setAttribute("aria-hidden", "false"); + speakerVolumePanelVisible = true; + + if (speakerVolumeSliderElement) { + try { + speakerVolumeSliderElement.focus({ preventScroll: true }); + } catch (e) { + try { + speakerVolumeSliderElement.focus(); + } catch (err) { } + } + } + + if (!speakerVolumeDocumentListenersActive) { + document.addEventListener("pointerdown", handleSpeakerVolumeDocumentPointerDown, true); + document.addEventListener("keydown", handleSpeakerVolumeKeydown, true); + speakerVolumeDocumentListenersActive = true; + } +} + +function closeSpeakerVolumePanel(options) { + clearSpeakerVolumeHoldTimer(); + + if (!speakerVolumePanelElement || !speakerVolumePanelVisible) { + if (speakerVolumePanelElement) { + speakerVolumePanelElement.setAttribute("aria-hidden", "true"); + } + if (speakerVolumeDocumentListenersActive) { + document.removeEventListener("pointerdown", handleSpeakerVolumeDocumentPointerDown, true); + document.removeEventListener("keydown", handleSpeakerVolumeKeydown, true); + speakerVolumeDocumentListenersActive = false; + } + if (!options || options.preserveToggleGuard !== true) { + session.ignoreNextSpeakerToggle = false; + } + speakerVolumePanelVisible = false; + return; + } + + speakerVolumePanelElement.classList.add("hidden"); + speakerVolumePanelElement.setAttribute("aria-hidden", "true"); + speakerVolumePanelVisible = false; + + if (speakerVolumeDocumentListenersActive) { + document.removeEventListener("pointerdown", handleSpeakerVolumeDocumentPointerDown, true); + document.removeEventListener("keydown", handleSpeakerVolumeKeydown, true); + speakerVolumeDocumentListenersActive = false; + } + + if (!options || options.preserveToggleGuard !== true) { + session.ignoreNextSpeakerToggle = false; + } +} + +function handleSpeakerVolumeDocumentPointerDown(event) { + if (!speakerVolumePanelVisible) { + return; + } + if (speakerVolumePanelElement && speakerVolumePanelElement.contains(event.target)) { + return; + } + if (speakerVolumeButton && speakerVolumeButton.contains(event.target)) { + return; + } + closeSpeakerVolumePanel(); +} + +function handleSpeakerVolumeKeydown(event) { + if (!speakerVolumePanelVisible) { + return; + } + if (event.key === "Escape" || event.key === "Esc") { + closeSpeakerVolumePanel(); + } +} + +function handleSpeakerButtonMouseDown(event) { + if (!event) { + return; + } + var target = event.target; + var panel = speakerVolumePanelElement; + if (!panel) { + panel = getById("speakerVolumePanel"); + } + if (panel && panel.contains(target)) { + event.stopPropagation(); + return; + } + event.preventDefault(); + event.stopPropagation(); +} + +function speakerButtonPointerDown(event) { + if (!speakerVolumeButton) { + return; + } + if (event && typeof event.stopPropagation === "function") { + event.stopPropagation(); + } + if (speakerVolumePanelVisible) { + session.ignoreNextSpeakerToggle = true; + closeSpeakerVolumePanel({ preserveToggleGuard: true }); + return; + } + if (event && event.pointerType === "mouse" && typeof event.button === "number" && event.button !== 0) { + return; + } + clearSpeakerVolumeHoldTimer(); + speakerVolumeHoldTimer = setTimeout(function () { + session.ignoreNextSpeakerToggle = true; + openSpeakerVolumePanel(); + }, SPEAKER_VOLUME_HOLD_DELAY); +} + +function speakerButtonPointerUp(event) { + if (event && typeof event.stopPropagation === "function") { + event.stopPropagation(); + } + clearSpeakerVolumeHoldTimer(); + if (speakerVolumePanelVisible) { + session.ignoreNextSpeakerToggle = true; + } +} + +function speakerButtonPointerLeave() { + clearSpeakerVolumeHoldTimer(); +} + +function initSpeakerVolumeControl() { + speakerVolumeButton = getById("mutespeakerbutton"); + speakerVolumePanelElement = getById("speakerVolumePanel"); + speakerVolumeSliderElement = getById("speakerVolumeSlider"); + speakerVolumeValueElement = getById("speakerVolumeValue"); + + if (!speakerVolumeButton || !speakerVolumePanelElement || !speakerVolumeSliderElement) { + return; + } + + var initialPercent = getCurrentSpeakerVolumePercent(); + speakerVolumeSliderElement.value = initialPercent; + if (speakerVolumeValueElement) { + speakerVolumeValueElement.textContent = initialPercent + "%"; + } + speakerVolumePanelElement.classList.add("hidden"); + speakerVolumePanelElement.setAttribute("aria-hidden", "true"); + speakerVolumePanelVisible = false; + + speakerVolumeButton.addEventListener("pointerdown", speakerButtonPointerDown); + speakerVolumeButton.addEventListener("pointerup", speakerButtonPointerUp); + speakerVolumeButton.addEventListener("pointerleave", speakerButtonPointerLeave); + speakerVolumeButton.addEventListener("pointercancel", speakerButtonPointerLeave); + + speakerVolumePanelElement.addEventListener("pointerdown", function (event) { + event.stopPropagation(); + }); + speakerVolumePanelElement.addEventListener("mousedown", function (event) { + event.stopPropagation(); + }); + speakerVolumePanelElement.addEventListener("touchstart", function (event) { + event.stopPropagation(); + }); + + speakerVolumeSliderElement.addEventListener("mousedown", function (event) { + event.stopPropagation(); + }); + speakerVolumeSliderElement.addEventListener("touchstart", function (event) { + event.stopPropagation(); + }); + + speakerVolumeSliderElement.addEventListener("input", handleSpeakerVolumeSliderInput); + speakerVolumeSliderElement.addEventListener("change", handleSpeakerVolumeSliderInput); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initSpeakerVolumeControl); +} else { + initSpeakerVolumeControl(); +} + +function getPeerDisplayName(UUID, fallback = "Someone", preferLabel = true) { + if (!UUID || !session.rpcs || !(UUID in session.rpcs)) { + return fallback; + } + + let name = preferLabel + ? (session.rpcs[UUID].label || session.rpcs[UUID].streamID || fallback) + : (session.rpcs[UUID].streamID || session.rpcs[UUID].label || fallback); + if (!name) { + return fallback; + } + + if (typeof name === "string") { + name = sanitizeLabel(name); + } + + return name || fallback; +} + +const fileTransfers = {}; + +function toggleFileshare(UUID = false, event = null) { + if (session.cleanOputput) { return; } + + let string = ''; + if (UUID === false) { + string = 'Share a file with the group
    '; + } else if (session.directorList.indexOf(UUID) >= 0) { + string = `The director requested you share a file with them.
    `; + } else { + const requester = getPeerDisplayName(UUID); + string = `${requester} has requested you share a file with them.
    `; + } + + warnUser(string, false, false); + + updateFileShare(); +} + +const enhancedFileTransfers = { + transfers: {}, + + initTransfer(fileId, total) { + if (!this.transfers[fileId]) { + this.transfers[fileId] = { + progress: 0, + total, + speed: 0, + lastUpdate: Date.now(), + bytesLastSecond: 0, + downloaders: new Map() + }; + } + }, + + updateProgress(fileId, transferred, UUID) { + const transfer = this.transfers[fileId]; + if (!transfer) return; + + const now = Date.now(); + const timeDiff = (now - transfer.lastUpdate) / 1000; + + if (UUID && transfer.downloaders.get(UUID)) { + transfer.downloaders.set(UUID, { + progress: (transferred / transfer.total) * 100, + speed: timeDiff >= 1 ? Math.round((transferred - (transfer.downloaders.get(UUID).bytesTransferred || 0)) / timeDiff) : 0, + bytesTransferred: transferred, + lastUpdate: now + }); + } + + if (timeDiff >= 1) { + transfer.speed = Math.round((transferred - transfer.bytesLastSecond) / timeDiff); + transfer.lastUpdate = now; + transfer.bytesLastSecond = transferred; + } + + transfer.progress = (transferred / transfer.total) * 100; + updateFileShare(); + }, + + removeDownloader(fileId, UUID) { + const transfer = this.transfers[fileId]; + if (transfer) { + transfer.downloaders.delete(UUID); + updateFileShare(); + } + } +}; +let isFileManagerMinimized = false; + +function toggleFileManagerSize() { + isFileManagerMinimized = !isFileManagerMinimized; + updateFileShare(); +} +function updateFileShare() { + if (session.cleanOutput) return; + + const activeSharesDiv = document.getElementById('activeShares'); + activeSharesDiv.innerHTML = ''; + + const container = document.createElement('div'); + container.className = `file-manager${isFileManagerMinimized ? ' minimized' : ''}`; + + // Header + const header = document.createElement('div'); + header.className = 'file-manager-header'; + const headerControls = document.createElement('div'); + headerControls.className = 'header-controls'; + + header.innerHTML = `

    File Sharing

    `; + headerControls.innerHTML = ` + + + `; + header.appendChild(headerControls); + container.appendChild(header); + + if (!isFileManagerMinimized) { + // Hidden file input + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.id = 'fileInput'; + fileInput.style.display = 'none'; + fileInput.multiple = true; + fileInput.onchange = (e) => session.shareFile(e.target, false, e); + container.appendChild(fileInput); + + // File list + const fileList = document.createElement('div'); + fileList.className = 'file-list'; + + if (!(session.hostedFiles && session.hostedFiles.length)) { + fileList.innerHTML = '
    No files being shared
    '; + } else { + session.hostedFiles.forEach(file => { + const transfer = enhancedFileTransfers.transfers[file.id]; + + const fileItem = document.createElement('div'); + fileItem.className = 'file-item'; + + const fileInfo = document.createElement('div'); + fileInfo.className = 'file-info'; + fileInfo.innerHTML = ` + ${file.name} + ${formatFileSize(file.size)} + `; + + if (transfer && transfer.downloaders.size > 0) { + const downloadList = document.createElement('div'); + downloadList.className = 'transfer-info'; + + transfer.downloaders.forEach((data, UUID) => { + const transferItem = document.createElement('div'); + transferItem.className = 'transfer-item'; + transferItem.innerHTML = ` +
    + Downloading by: ${getLabelForUUID(UUID)} +
    +
    +
    + ${Math.round(data.progress)}% + ${formatSpeed(data.speed)} +
    + `; + downloadList.appendChild(transferItem); + }); + + fileInfo.appendChild(downloadList); + } + + const actions = document.createElement('div'); + actions.innerHTML = ` + + `; + + fileItem.appendChild(fileInfo); + fileItem.appendChild(actions); + fileList.appendChild(fileItem); + }); + } + + container.appendChild(fileList); + } + + activeSharesDiv.appendChild(container); +} +function getLabelForUUID(UUID) { + if (!UUID) return 'Unknown'; + + let label = UUID; + if (session.rpcs[UUID] && session.rpcs[UUID].label) { + label = sanitizeLabel(session.rpcs[UUID].label); + } else if (session.pcs[UUID] && session.pcs[UUID].label) { + label = sanitizeLabel(session.pcs[UUID].label); + } + + if (session.directorList.indexOf(UUID) >= 0) { + label = 'Director: ' + label; + } + + return label; +} +function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} + +function formatSpeed(bytesPerSecond) { + return bytesPerSecond ? formatFileSize(bytesPerSecond) + '/s' : ''; +} + +function cancelFileTransfer(fileId) { + console.log(fileId); + + if (fileTransfers[fileId]) { + fileTransfers[fileId].channel.close(); + delete fileTransfers[fileId]; + } + // Remove the file from hostedFiles array + session.hostedFiles = session.hostedFiles.filter(file => file.id !== fileId); + updateFileShare(); // Refresh the file share display +} + +function truncateUrl(url) { + try { + const urlObj = new URL(url); + const path = urlObj.pathname.length > 15 ? + urlObj.pathname.substring(0, 15) + '...' : + urlObj.pathname; + return urlObj.hostname + path; + } catch (e) { + return url.length > 30 ? url.substring(0, 30) + '...' : url; + } +} + +session.sendFile = function (UUID, fileid) { + log("SENDING FILE: " + fileid + " " + UUID); + var fr = new FileReader(); + var fid = session.hostedFiles.findIndex(file => file.id === fileid); + + if (fid === -1) { + warnlog("requested file was not found"); + return; + } else if (session.hostedFiles[fid].state == 0) { + warnlog("requested file has been removed."); + return; + } else if (session.hostedFiles[fid].restricted && session.hostedFiles[fid].restricted !== UUID) { + warnlog("user didn't have access for this file."); + return; + } + + var chunksize = 16384; + var cid = 0; + var channelName = fileid; + if (channelName === "sendChannel") { + channelName = "sendChannel_" + session.generateStreamID(5); + } + + var transferchannel; + if (UUID in session.pcs) { + transferchannel = session.pcs[UUID].createDataChannel(channelName); + } else if (UUID in session.rpcs) { + transferchannel = session.rpcs[UUID].createDataChannel(channelName); + } else { + warnlog("UUID does not exist"); + return; + } + + transferchannel.binaryType = "arraybuffer"; + var chunk = session.hostedFiles[fid].file.slice(0, chunksize); // Use the stored File object + + fileTransfers[fileid] = { + channel: transferchannel, + progress: 0 + }; + + transferchannel.onopen = () => { + transferchannel.send(JSON.stringify({ type: "filetransfer", size: session.hostedFiles[fid].size, filename: session.hostedFiles[fid].name, id: session.hostedFiles[fid].id })); + fr.readAsArrayBuffer(chunk); + }; + + transferchannel.onclose = () => { + try { + var index = session.hostedTransfers.indexOf(transferchannel); + if (index > -1) { + session.hostedTransfers.splice(index, 1); + } + } catch (e) { + errorlog(e); + } + log("Transfer ended"); + delete fileTransfers[fileid]; + updateFileShare(); // Refresh the file share display + transferchannel = null; + enhancedFileTransfers.removeDownloader(fileid, UUID); + }; + + transferchannel.onmessage = event => { + // console.log(event.data); + }; + + session.hostedTransfers.push(transferchannel); + + fr.onload = function () { + if (session.hostedFiles[fid].state == 0) { + return; + } + var arrayBuffer = fr.result; + try { + transferchannel.send(arrayBuffer); + } catch (e) { + try { + transferchannel.close(); + } catch (e) { } + warnlog(e); + return; + } + cid += 1; + const progress = Math.min(100, Math.round((cid * chunksize / session.hostedFiles[fid].size) * 100)); + fileTransfers[fileid].progress = progress; + updateFileShare(); // Update progress display + + enhancedFileTransfers.updateProgress(fileid, cid * chunksize, UUID); + + if (cid * chunksize < session.hostedFiles[fid].size) { + try { + chunk = session.hostedFiles[fid].file.slice(cid * chunksize, (cid + 1) * chunksize); // Use the stored File object + fr.readAsArrayBuffer(chunk); + } catch (e) { + errorlog(e); + } + } else { + transferchannel.send("EOF1"); + transferchannel.close(); + } + }; +}; + +function toggleChat(event = null) { + const chatModule = document.getElementById('chatModule'); + + if (!chatModule.configured) { + setupAdjustableChat(); + } + + if (session.chat === false) { + session.chat = true; + chatModule.classList.remove("hidden"); + getById("chatInput").focus(); + getById("chatNotification").classList.remove("notification", "red"); + getById("chattoggle").classList.remove("pulsate"); + } else { + session.chat = false; + chatModule.classList.add("hidden"); + } + updateMessages(); +} + +function toggleDirectFeedback(event = null) { + const unmuteSelf = document.getElementById('unmuteSelf'); + unmuteSelf.classList.remove("hidden"); + + if (session.videoElement) { + session.videoElement.muted = session.videoElement.muted ? false : true; + + if (session.selfVolume) { + session.selfVolume = parseFloat(session.selfVolume); + if (session.selfVolume >= 1) { + session.videoElement.volume = Math.min(100, Math.max(1, session.selfVolume)) / 100; + session.selfVolume = null; + } else { + session.videoElement.volume = Math.min(1, Math.max(0, session.selfVolume)); + session.selfVolume = null; + } + } + } + + if (session.videoElement.muted) { + unmuteSelf.classList.remove("red", "pulsate"); + unmuteSelf.ariaPressed = "false"; + + } else { + unmuteSelf.classList.add("red", "pulsate"); + unmuteSelf.ariaPressed = "true"; + } +} + +function setupAdjustableChat() { + const chatModule = document.getElementById('chatModule'); + chatModule.configured = true; + + const chatBody = getById('chatBody'); + let isResizing = false; + let isDragging = false; + let startY, startHeight, startX, startTop, startLeft; + + function initResize(e) { + isResizing = true; + document.body.style.userSelect = 'none'; + startY = e.clientY; + startHeight = parseInt(window.getComputedStyle(chatBody).height, 10); + document.addEventListener('mousemove', resize); + document.addEventListener('mouseup', stopResize); + } + + function resize(e) { + if (isResizing) { + const maxHeight = window.innerHeight - chatModule.offsetTop - 20; // 20px buffer + const newHeight = Math.min(startHeight + (e.clientY - startY), maxHeight); + chatBody.style.height = `${Math.max(newHeight, 50)}px`; // Minimum height of 50px + keepInBounds(); + } + } + + function stopResize() { + isResizing = false; + document.removeEventListener('mousemove', resize); + document.removeEventListener('mouseup', stopResize); + document.body.style.userSelect = ''; + } + + function initDrag(e) { + isDragging = true; + document.body.style.userSelect = 'none'; + startX = e.clientX - chatModule.offsetLeft; + startY = e.clientY - chatModule.offsetTop; + document.addEventListener('mousemove', drag); + document.addEventListener('mouseup', stopDrag); + } + + function drag(e) { + if (isDragging) { + const newLeft = Math.min(Math.max(e.clientX - startX, 0), window.innerWidth - chatModule.offsetWidth); + const newTop = Math.min(Math.max(e.clientY - startY, 0), window.innerHeight - chatModule.offsetHeight); + chatModule.style.left = `${newLeft}px`; + chatModule.style.top = `${newTop}px`; + chatModule.style.bottom = 'auto'; + chatModule.style.right = 'auto'; + keepInBounds(); + } + } + + function stopDrag() { + isDragging = false; + document.removeEventListener('mousemove', drag); + document.removeEventListener('mouseup', stopDrag); + document.body.style.userSelect = ''; + } + + function keepInBounds() { + const rect = chatModule.getBoundingClientRect(); + + if (rect.right > window.innerWidth) { + chatModule.style.left = `${window.innerWidth - rect.width}px`; + } + if (rect.bottom > window.innerHeight) { + chatModule.style.top = `${window.innerHeight - rect.height}px`; + } + if (rect.left < 0) { + chatModule.style.left = '0px'; + } + if (rect.top < 0) { + chatModule.style.top = '0px'; + chatModule.style.bottom = 'auto'; + } + } + + chatModule.querySelector('.resizer').addEventListener('mousedown', initResize); + chatModule.querySelector('.chat-header').addEventListener('mousedown', initDrag); + + // Keep chat window in bounds when window is resized + window.addEventListener('resize', keepInBounds); + + // Initial positioning + keepInBounds(); +} + +function directorAdvanced(ele) { + var target = document.createElement("div"); + target.style = "position:absolute;float:left;width:270px;height:222px;background-color:#7E7E7E;"; + + var closeButton = document.createElement("button"); + closeButton.innerHTML = " close"; + closeButton.style.left = "5px"; + closeButton.style.position = "relative"; + closeButton.onclick = function () { + target.parentNode.removeChild(target); + }; + target.appendChild(closeButton); + + var someButton = document.createElement("button"); + someButton.innerHTML = " some action "; + someButton.style.left = "5px"; + someButton.style.position = "relative"; + someButton.onclick = function () { + var actionMsg = {}; + session.sendRequest(actionMsg, ele.dataset.UUID); + }; + target.appendChild(someButton); + + ele.parentNode.appendChild(target); +} + +function directorSendMessage(ele) { + var UUID = ele.dataset.UUID; + var target = document.querySelector("[data--u-u-i-d='" + UUID + "'][data-action-type='messaging-box']"); + if (!target) { + return; + } + + if (target.classList.contains("hidden")) { + target.classList.remove("hidden"); + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } else { + target.classList.add("hidden"); + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + return; + } + + var inputField = target.querySelector("[data-action-type='messaging-box-text']"); + if (inputField) { + inputField.focus(); + inputField.select(); + } + if ("overlay" in target) { + return; + } + + target.overlay = true; + + if (inputField) { + inputField.addEventListener("keydown", function (e) { + if (e.keyCode == 13) { + e.preventDefault(); + sendButton.click(); + } else if (e.keyCode == 27) { + e.preventDefault(); + inputField.value = ""; + target.parentNode.removeChild(target); + } + }); + } + + var sendButton = target.querySelector("[data-action-type='messaging-box-send']"); + if (sendButton) { + sendButton.onclick = function () { + var chatMsg = {}; + chatMsg.chat = inputField.value; + if (sendButton.parentNode.overlay) { + chatMsg.overlay = sendButton.parentNode.overlay; + } + session.sendRequest(chatMsg, ele.dataset.UUID); + inputField.value = ""; + //target.parentNode.removeChild(target); + }; + } + + var closeButton = target.querySelector("[data-action-type='messaging-box-close']"); + if (closeButton) { + closeButton.onclick = function () { + inputField.value = ""; + target.classList.add("hidden"); + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + }; + } + + var overlayMsg = target.querySelector("[data-action-type='messaging-box-toggle']"); + if (overlayMsg) { + overlayMsg.onclick = function (e) { + log(e.target.parentNode.parentNode); + if (e.target.parentNode.parentNode.overlay === true) { + e.target.parentNode.parentNode.overlay = false; + e.target.parentNode.innerHTML = ""; + } else { + e.target.parentNode.parentNode.overlay = true; + e.target.parentNode.innerHTML = ""; + } + }; + } +} + +function toggleAutoVideoMute() { + // for iOS devices, that tab out. + // document.visibilityState + if (!session.videoMuted && session.permaid !== false) { + var msg = {}; + msg.videoMuted = document.visibilityState === "hidden" || false; + //try { + session.sendMessage(msg); + //} catch(e){errorlog(e);} + pokeIframeAPI("video-mute-state", document.visibilityState); + } +} + +function toggleVideoMute(apply = false) { + // TODO: I need to have this be MUTE, toggle, with volume not touched. + if (apply) { + session.videoMuted = !session.videoMuted; + } + + if (!session.remoteVideoMuted) { + getById("head8").classList.add("hidden"); + } + + if (session.videoMuted == false) { + session.videoMuted = true; + getById("mutevideotoggle").className = "las la-video-slash toggleSize"; + if (!session.cleanOutput) { + getById("mutevideobutton").classList.add("red"); + getById("mutevideobutton").ariaPressed = "true"; + getById("header").classList.add("red"); + if (session.remoteVideoMuted) { + getById("head8").classList.remove("hidden"); + } + } + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(track => { + track.enabled = false; + }); + } + } else if (session.remoteVideoMuted) { + // the director has muted this guest's video feed + session.videoMuted = false; // just setting it back to the pre-toggled state + getById("mutevideotoggle").className = "las la-video toggleSize"; + if (!session.cleanOutput) { + getById("head8").classList.remove("hidden"); + getById("header").classList.add("red"); + getById("mutevideobutton").classList.remove("red"); + getById("mutevideobutton").ariaPressed = "false"; + } + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(track => { + track.enabled = false; + }); + } + } else { + session.videoMuted = false; + + getById("mutevideotoggle").className = "las la-video toggleSize"; + if (!session.cleanOutput) { + getById("mutevideobutton").classList.remove("red"); + getById("mutevideobutton").ariaPressed = "false"; + getById("header").classList.remove("red"); + } + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(track => { + track.enabled = true; + }); + } + } + + if (session.avatar && session.avatar.ready && !apply) { + updateRenderOutpipe(); + if (session.videoMuted) { + var msg = {}; + msg.videoMuted = false; // doesn't matter the actual mute state; this is the avatar + session.sendMessage(msg); + } + } else if (!apply) { + var msg = {}; + msg.videoMuted = session.videoMuted; + session.sendMessage(msg); + } + + pokeIframeAPI("video-mute-state", session.videoMuted || session.remoteVideoMuted); + + if (!apply) { + pokeAPI("videoMuted", session.videoMuted || session.remoteVideoMuted); + } + + if (session.style && session.style == 1) { + if (!session.videoElement || session.videoElement.id !== "previewWebcam") { + updateMixer(); + } + } +} +var toggleSettingsState = false; +let settingsClickHandler = null; + +async function toggleSettings(forceShow = false) { + if (session.nosettings) return; + + const multiselectTrigger = getById("multiselect-trigger3"); + multiselectTrigger.dataset.state = "0"; + multiselectTrigger.classList.add("closed"); + multiselectTrigger.classList.remove("open"); + getById("chevarrow2").classList.add("bottom"); + + const popupSelector = getById("popupSelector"); + + // For forceShow, if already open just update devices + if (toggleSettingsState && forceShow) { + await enumerateDevices().then(gotDevices2); + return; + } + + if (popupSelector.style.display === "none") { + await showSettings(); + } else { + hideSettings(); + } + + pokeIframeAPI("settings-menu-state", toggleSettingsState); +} + +async function showSettings() { + const popupSelector = getById("popupSelector"); + const settingsButton = getById("settingsbutton"); + + updateConstraintSliders(); + + // Handler only closes the menu when clicking outside + settingsClickHandler = (e) => { + if (!popupSelector.contains(e.target) && !e.target.closest('#settingsbutton')) { + hideSettings(); + pokeIframeAPI("settings-menu-state", false); + } + }; + + setTimeout(() => { + document.addEventListener("click", settingsClickHandler); + }, 10); + + if (navigator.userAgent.indexOf("Chrome") !== -1) { + try { + const permissionResult = await navigator.permissions.query({ name: "camera" }); + if (permissionResult.state === "prompt") { + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); + await enumerateDevices().then(gotDevices2); + stream.getTracks().forEach(track => track.stop()); + } catch { + await enumerateDevices().then(gotDevices2); + } + } else { + await enumerateDevices().then(gotDevices2); + } + } catch { + await enumerateDevices().then(gotDevices2); + } + } else { + await enumerateDevices().then(gotDevices2); + } + + popupSelector.style.display = "inline-block"; + settingsButton.classList.add("brown"); + settingsButton.ariaPressed = "true"; + + loadContentEffectsImages(); + + setTimeout(() => { + popupSelector.style.right = "0px"; + }, 1); + + toggleSettingsState = true; +} + +function hideSettings() { + const popupSelector = getById("popupSelector"); + const settingsButton = getById("settingsbutton"); + + if (settingsClickHandler) { + document.removeEventListener("click", settingsClickHandler); + settingsClickHandler = null; + } + + popupSelector.style.right = "-500px"; + settingsButton.classList.remove("brown"); + settingsButton.ariaPressed = "false"; + + setTimeout(() => { + popupSelector.style.display = "none"; + }, 200); + + toggleSettingsState = false; + getById("videoSettings3").style.display = "none"; +} + +function toggleFullscreenButtonSetting() { + var btn = getById("toggleFullscreenButton"); + var fullscreenPage = getById("fullscreenPage"); + + if (iOS || iPad) { + warnUser("Fullscreen is not supported on iOS/iPadOS"); + return; + } + + if (session.fullscreenButton) { + // Disable + session.fullscreenButton = false; + fullscreenPage.classList.add("hidden"); + document.documentElement.style.removeProperty("--full-screen-button"); + btn.innerText = "Enable"; + btn.classList.remove("selected"); + + // Exit fullscreen if currently in it + if (document.fullscreenElement) { + try { document.exitFullscreen(); } catch(e) {} + } + } else { + // Enable + session.fullscreenButton = true; + fullscreenPage.classList.remove("hidden"); + document.documentElement.style.setProperty("--full-screen-button", "none"); + btn.innerText = "Disable"; + btn.classList.add("selected"); + } + + try { + setStorage("fullscreenButtonSetting", session.fullscreenButton); + } catch(e) {} +} + +function togglePIPButtonSetting() { + var btn = getById("togglePIPButton"); + var pipPage = getById("PictureInPicturePage"); + + if (typeof documentPictureInPicture === "undefined") { + warnUser("Picture-in-Picture is not supported in this browser"); + return; + } + + if (pipPage.classList.contains("hidden")) { + // Enable + pipPage.classList.remove("hidden"); + btn.innerText = "Disable"; + btn.classList.add("selected"); + try { + setStorage("pipButtonSetting", true); + } catch(e) {} + } else { + // Disable + pipPage.classList.add("hidden"); + btn.innerText = "Enable"; + btn.classList.remove("selected"); + try { + setStorage("pipButtonSetting", false); + } catch(e) {} + } +} + +function initButtonToggleSettings() { + // Hide toggles on unsupported platforms + if (iOS || iPad) { + var fsContainer = getById("fullscreenToggleContainer"); + if (fsContainer) fsContainer.style.display = "none"; + } + + if (typeof documentPictureInPicture === "undefined") { + var pipContainer = getById("pipToggleContainer"); + if (pipContainer) pipContainer.style.display = "none"; + } + + // Restore saved settings (only if URL params didn't already set them) + try { + var savedFullscreen = getStorage("fullscreenButtonSetting"); + if (savedFullscreen === true && !session.fullscreenButton && !(iOS || iPad)) { + toggleFullscreenButtonSetting(); + } + + var savedPIP = getStorage("pipButtonSetting"); + var pipPage = getById("PictureInPicturePage"); + if (savedPIP === true && pipPage && pipPage.classList.contains("hidden") && typeof documentPictureInPicture !== "undefined") { + togglePIPButtonSetting(); + } + } catch(e) {} + + // Update button states to reflect current state + updateButtonToggleStates(); +} + +function updateButtonToggleStates() { + var fullscreenBtn = getById("toggleFullscreenButton"); + var pipBtn = getById("togglePIPButton"); + var fullscreenPage = getById("fullscreenPage"); + var pipPage = getById("PictureInPicturePage"); + + if (fullscreenBtn && fullscreenPage) { + if (!fullscreenPage.classList.contains("hidden")) { + fullscreenBtn.innerText = "Disable"; + fullscreenBtn.classList.add("selected"); + } else { + fullscreenBtn.innerText = "Enable"; + fullscreenBtn.classList.remove("selected"); + } + } + + if (pipBtn && pipPage) { + if (!pipPage.classList.contains("hidden")) { + pipBtn.innerText = "Disable"; + pipBtn.classList.add("selected"); + } else { + pipBtn.innerText = "Enable"; + pipBtn.classList.remove("selected"); + } + } +} + +let wakeLockObject = null; +let wakeLockReleaseHandler = null; +let wakeLockInteractionArmed = false; + +function armWakeLockOnInteraction() { + if (wakeLockInteractionArmed || !("wakeLock" in navigator)) { + return; + } + + wakeLockInteractionArmed = true; + const handler = () => { + document.removeEventListener("pointerdown", handler); + document.removeEventListener("keydown", handler); + wakeLockInteractionArmed = false; + acquireWakeLock(true); + }; + + document.addEventListener("pointerdown", handler, { once: true }); + document.addEventListener("keydown", handler, { once: true }); +} + +async function acquireWakeLock(fromUserGesture = false) { + if (!("wakeLock" in navigator)) { + warnlog("Wake Lock API is not supported in this browser"); + if (typeof session !== "undefined") { + session.forceLegacyWakeLock = true; + } + startLegacyKeepAliveLoop(); + return; + } + + try { + if (wakeLockObject && wakeLockReleaseHandler) { + try { + wakeLockObject.removeEventListener("release", wakeLockReleaseHandler); + } catch (e) { + errorlog(e); + } + } + + const lock = await navigator.wakeLock.request("screen"); + wakeLockObject = lock; + if (typeof session !== "undefined") { + session.wakeLockActive = true; + session.forceLegacyWakeLock = false; + } + + wakeLockReleaseHandler = () => { + wakeLockObject = null; + wakeLockReleaseHandler = null; + if (typeof session !== "undefined") { + session.wakeLockActive = false; + session.forceLegacyWakeLock = true; + } + startLegacyKeepAliveLoop(); + armWakeLockOnInteraction(); + }; + + wakeLockObject.addEventListener("release", wakeLockReleaseHandler); + removeLegacyKeepAlivePlayer(); + log("Wake Lock is active"); + } catch (err) { + if (typeof session !== "undefined") { + session.wakeLockActive = false; + session.forceLegacyWakeLock = true; + } + startLegacyKeepAliveLoop(); + if (!fromUserGesture) { + armWakeLockOnInteraction(); + } + errorlog(err); + } +} + +function handleVisibilityChangeWakeLock() { + if (document.visibilityState === "visible") { + acquireWakeLock(); + armWakeLockOnInteraction(); + } +} + +function releaseWakeLock() { + if (wakeLockObject) { + wakeLockObject + .release() + .then(() => { + wakeLockObject = null; + if (typeof session !== "undefined") { + session.wakeLockActive = false; + session.forceLegacyWakeLock = true; + } + wakeLockReleaseHandler = null; + startLegacyKeepAliveLoop(); + log("Wake Lock is released"); + }) + .catch(err => { + errorlog(err); + }); + } +} + +// QoS Report - sends anonymous connection quality data on hangup +function sendQosReport() { + if (!session.qosEnabled || !session.qosData || session.qosData.sent) return; + session.qosData.sent = true; // Prevent duplicate sends + + try { + var qd = session.qosData; + + // Helper functions + var avg = function(arr) { + if (!arr || !arr.length) return null; + return arr.reduce(function(a, b) { return a + b; }, 0) / arr.length; + }; + var max = function(arr) { + if (!arr || !arr.length) return null; + return Math.max.apply(null, arr); + }; + + // Determine browser (using existing detection) + var browser = "Unknown"; + var browserVersion = 0; + if (typeof Safari !== "undefined" && Safari) { + browser = "Safari"; + browserVersion = SafariVersion || 0; + } else if (typeof Firefox !== "undefined" && Firefox) { + browser = "Firefox"; + browserVersion = Firefox; + } else if (typeof ChromiumVersion !== "undefined" && ChromiumVersion) { + browser = "Chrome"; + browserVersion = ChromiumVersion; + } + + // Determine platform + var platform = "desktop"; + if (typeof iOS !== "undefined" && iOS) platform = "mobile"; + else if (typeof iPad !== "undefined" && iPad) platform = "tablet"; + else if (/Android/i.test(navigator.userAgent)) platform = "mobile"; + + // Determine connection type + var connectionType = "viewer"; + if (session.director) connectionType = "director"; + else if (session.streamSrc || session.videoElement) connectionType = "publisher"; + + // Get TURN server (already filtered to hostnames by client-side allowlist) + var turnServer = qd.turnServersUsed && qd.turnServersUsed.length ? qd.turnServersUsed[0] : null; + // Trim to 30 chars max (DB limit) + if (turnServer && turnServer.length > 30) turnServer = turnServer.substring(0, 30); + + // Get meshcast server if used (only if &meshcast enabled) + var meshcastServer = null; + if (session.meshcast && qd.meshcastServersUsed && qd.meshcastServersUsed.length > 0) { + meshcastServer = qd.meshcastServersUsed[0]; + } + + // For publishers: also collect stats from outbound connections (session.pcs) + // This supplements the inbound stats from processStats + if (session.pcs) { + for (var uuid in session.pcs) { + try { + var pcStats = session.pcs[uuid].stats; + if (!pcStats) continue; + + // Get transport type from publisher connection + if (pcStats.candidateType_local) { + if (pcStats.candidateType_local === "relay") { + qd.transportType = "turn"; + // TURN hostname already tracked in processPcsQosStats via allowlist + } else if (!qd.transportType || qd.transportType === "unknown") { + qd.transportType = "p2p"; + } + if (!qd.candidateTypesLocal.includes(pcStats.candidateType_local)) { + qd.candidateTypesLocal.push(pcStats.candidateType_local); + } + } + if (pcStats.candidateType_remote && !qd.candidateTypesRemote.includes(pcStats.candidateType_remote)) { + qd.candidateTypesRemote.push(pcStats.candidateType_remote); + } + + // Get RTT from publisher stats + if (pcStats.average_roundTripTime_ms) { + qd.rttSamples.push(pcStats.average_roundTripTime_ms); + } + + // Get resolution/codec from publisher stats + if (pcStats.resolution && !qd.lastResolution) { + qd.lastResolution = pcStats.resolution; + } + if (pcStats.encoder) { + qd.lastVideoCodec = pcStats.encoder; + } + } catch (e) { } + } + } + + // Set transport type for WHIP/WHEP if not already set + if (!qd.transportType || qd.transportType === "unknown") { + if (session.whipOut) qd.transportType = "whip"; + else if (session.whepIn || session.whepInput) qd.transportType = "whep"; + } + + // For non-meshcast WHIP/WHEP, mark as "private" (don't log actual endpoint) + if ((qd.transportType === "whip" || qd.transportType === "whep") && + (!qd.meshcastServersUsed || qd.meshcastServersUsed.length === 0)) { + meshcastServer = "private"; + } + + // Build payload - NO room IDs, stream IDs, or passwords + var payload = { + // Session + sessionDuration: Math.round((Date.now() - qd.startTime) / 1000), + connectionType: connectionType, + + // Client (privacy-safe) + browser: browser, + browserVersion: browserVersion, + platform: platform, + + // Transport + transportType: qd.transportType || "unknown", + turnServer: turnServer, + meshcastServer: meshcastServer, + wssSuccess: qd.wssSuccess, + candidateLocal: qd.candidateTypesLocal.length > 0 ? qd.candidateTypesLocal[0] : null, + candidateRemote: qd.candidateTypesRemote.length > 0 ? qd.candidateTypesRemote[0] : null, + + // Quality + connectionSuccess: qd.connectionSuccesses > 0 || qd.rttSamples.length > 0, + connectionFailures: qd.connectionFailures, + iceRestarts: qd.iceRestarts, + + // Packet loss + avgPacketLossVideo: avg(qd.packetLossVideoSamples) !== null ? Math.round(avg(qd.packetLossVideoSamples) * 100) / 100 : null, + avgPacketLossAudio: avg(qd.packetLossAudioSamples) !== null ? Math.round(avg(qd.packetLossAudioSamples) * 100) / 100 : null, + maxPacketLossVideo: max(qd.packetLossVideoSamples) !== null ? Math.round(max(qd.packetLossVideoSamples) * 100) / 100 : null, + + // Latency + avgRtt: avg(qd.rttSamples) !== null ? Math.round(avg(qd.rttSamples)) : null, + maxRtt: max(qd.rttSamples) !== null ? Math.round(max(qd.rttSamples)) : null, + avgJitter: avg(qd.jitterSamples) !== null ? Math.round(avg(qd.jitterSamples)) : null, + + // Media + videoCodec: qd.lastVideoCodec ? qd.lastVideoCodec.replace("video/", "") : null, + audioCodec: qd.lastAudioCodec ? qd.lastAudioCodec.replace("audio/", "") : null, + avgVideoBitrate: avg(qd.bitrateSamples) !== null ? Math.round(avg(qd.bitrateSamples)) : null, + maxResolution: qd.lastResolution, + + // Errors (last 10, sanitized to remove private data) + errors: (typeof errorReport !== "undefined" && errorReport && errorReport.length > 0) ? + errorReport.slice(-10).map(function(e) { + var msg = String(e.error || e || ""); + // Strip private/personal data from error messages + msg = msg + // Remove full URLs and URL parameters + .replace(/https?:\/\/[^\s"'<>)]+/gi, "[URL]") + .replace(/wss?:\/\/[^\s"'<>)]+/gi, "[WSS]") + // Remove UUIDs (various formats) + .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]") + .replace(/[0-9a-f]{32,}/gi, "[HASH]") + // Remove stream IDs, view IDs, push IDs + .replace(/(streamID|stream_id|streamid|sid|push|view|scene)[=:]["']?[a-zA-Z0-9_-]{3,30}["']?/gi, "$1=[REDACTED]") + // Remove room IDs + .replace(/(room|roomid|room_id)[=:]["']?[a-zA-Z0-9_-]{3,30}["']?/gi, "$1=[REDACTED]") + // Remove passwords and hashes + .replace(/(password|pass|pwd|hash|salt|key|token|auth)[=:]["']?[^\s"'&]{1,50}["']?/gi, "$1=[REDACTED]") + // Remove IP addresses (IPv4 and IPv6) + .replace(/\b(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?\b/g, "[IP]") + .replace(/\b([0-9a-f]{1,4}:){2,7}[0-9a-f]{1,4}\b/gi, "[IPv6]") + // Remove base64-ish strings that might be tokens/credentials + .replace(/[A-Za-z0-9+/=]{40,}/g, "[TOKEN]") + // Remove email addresses + .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, "[EMAIL]") + // Remove query string parameters + .replace(/\?[^\s"'<>]+/g, "?[PARAMS]"); + return { + msg: msg.substring(0, 200), + line: e.line || 0, + time: e.time ? parseInt(e.time) : 0 + }; + }) : null + }; + + // Send using sendBeacon for reliability during page unload + var blob = new Blob([JSON.stringify(payload)], { type: "application/json" }); + navigator.sendBeacon("https://qos.vdo.ninja/v1/report", blob); + log("QoS report sent"); + } catch (e) { + warnlog("QoS report error: " + e); + } +} + +// For view/scene links without explicit hangup - send QoS on page unload +// Only triggers if there are no active push connections (publisher links have explicit hangup) +if (typeof window !== "undefined") { + window.addEventListener("beforeunload", function() { + // Only send for viewers (no push) and if we haven't already sent + if (session && session.qosEnabled && session.qosData && !session.qosData.sent) { + // Check that we're not a publisher (publishers use explicit hangup) + if (!session.streamSrc && !session.videoElement) { + sendQosReport(); + } + } + }); + + // Also trigger on visibility change to hidden (tab close, navigate away) + document.addEventListener("visibilitychange", function() { + if (document.visibilityState === "hidden") { + if (session && session.qosEnabled && session.qosData && !session.qosData.sent) { + // Only for viewers + if (!session.streamSrc && !session.videoElement) { + sendQosReport(); + } + } + } + }); +} + +session.hangup = function (reload = false, estop = false) { + // Send QoS report on hangup + try { + sendQosReport(); + } catch (e) { warnlog(e); } + + try { + window.removeEventListener("beforeunload", confirmUnload); + } catch (e) { } + + // Clean up auto-end timer if it exists + if (session.autoEndTimer) { + clearTimeout(session.autoEndTimer); + session.autoEndTimer = null; + } + if (session.autoEndInterval) { + clearInterval(session.autoEndInterval); + session.autoEndInterval = null; + } + try { + const countdown = document.getElementById("autoEndCountdown"); + if (countdown) { + countdown.remove(); + } + } catch (e) { } + + try { + if (estop) { + recordLocalVideo("estop"); + } + } catch (e) { } + try { + if (estop) { + recordLocalVideo("estop", false, false, true); // screen share + } + } catch (e) { } + session.taintedSession = true; + warnlog("hanging up"); + + try { + recordLocalVideo("stop"); + } catch (e) { } + try { + recordLocalVideo("stop", false, false, true); // screen share + } catch (e) { } + + try { + transferList.forEach(file => { + if (file.writer) { + file.writer.close(); + } + if (file.videoElement && file.videoElement.stopWriter) { + file.videoElement.stopWriter(true); // estop + } + }); + } catch (e) { + errorlog(e); + } + + try { + var msg = {}; + msg.videoMuted = true; // might not trigger + msg.bye = true; + session.sendMessage(msg); // make sure the remote video goes black. + } catch (e) { } + + try { + session.ws.close(); + } catch (e) { } + + try { + if (session.canvasSource && session.canvasSource.srcObject) { + session.canvasSource.srcObject.getTracks().forEach(function (track) { + session.canvasSource.srcObject.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getTracks().forEach(function (track) { + session.videoElement.srcObject.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } + if (session.streamSrc) { + session.streamSrc.getTracks().forEach(function (track) { + session.streamSrc.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } + if (session.streamSrcClone) { + session.streamSrcClone.getTracks().forEach(function (track) { + session.streamSrcClone.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } + if (session.screenStream) { + session.screenStream.getTracks().forEach(function (track) { + session.screenStream.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } + } catch (e) { + errorlog(e); + } + try { + for (i in session.rpcs) { + try { + if (session.rpcs[i].videoElement) { + if (session.rpcs[i].videoElement.recording) { + recordLocalVideo("stop", null, session.rpcs[i].videoElement); + } + } + } catch (e) { } + log("closing rpc due to hangup event"); + session.closeRPC(i, true); + } + + for (i in session.pcs) { + log("closing 5"); + session.closePC(i); + } + } catch (e) { + errorlog(e); + } + + for (var sid in session.watchTimeoutList) { + clearTimeout(session.watchTimeoutList[sid]); + } + + if (session.whipOut && session.whipOut.deleteme) { + session.whipOut.deleteme(); + } + + if (DebugLog && errorReport) { + downloadLogs(); + } + + if (session.popupChat) { + if (!session.popupChat.closed) { + session.popupChat.close(); + session.popupChat = null; + } + } + + releaseWakeLock(); + + if (reload) { + reloadRequested(); + warnlog("Reloading? uh oh. Why didn't it?"); + return; + } else { + setTimeout(function () { + for (i in session) { + try { + delete session[i]; + } catch (e) { } + } + delete session; + }, 1200); + + hangupComplete(); + log("HANG UP COMPLETE"); + } +}; + +function hangup(showhangup = true) { + // TODO: I need to have this be MUTE, toggle, with volume not touched. + if (session.hostedTransfers.length) { + confirmAlt("There are still file transfer in progress\nAre you sure you wish to exit?").then(res => { + if (res) { + try { + if (showhangup) { + document.getElementById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML; + } else { + document.getElementById("main").innerHTML = ""; + document.getElementById("hangupTemplate").innerHTML = ""; + } + } catch (e) { } + + setTimeout(function () { + session.hangup(); + }, 0); + } + }); + } else { + try { + if (showhangup) { + document.getElementById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML; + } else { + document.getElementById("main").innerHTML = ""; + document.getElementById("hangupTemplate").innerHTML = ""; + } + } catch (e) { } + + setTimeout(function () { + session.hangup(); + }, 0); + } +} + +function hangup2() { + session.hangupDirector(); + getById("miniPerformer").innerHTML = ""; + getById("press2talk").dataset.enabled = false; + getById("screensharebutton").classList.add("hidden"); + getById("screenshare2button").classList.add("hidden"); + getById("screenshare3button").classList.add("hidden"); + getById("settingsbutton").classList.add("hidden"); + getById("mutebutton").classList.add("hidden"); + getById("hangupbutton2").classList.add("hidden"); + //getById("chatbutton").classList.remove("hidden"); + getById("controlButtons").classList.remove("hidden"); + //getById("mutespeakerbutton").classList.add("hidden"); + getById("mutevideobutton").classList.add("hidden"); + + getById("screensharebutton").classList.remove("green"); + getById("screensharebutton").ariaPressed = "false"; + + if (!session.showDirector) { + getById("miniPerformer").innerHTML = ''; + miniTranslate(getById("miniPerformer")); + } else { + getById("miniPerformer").innerHTML = ''; + } + getById("miniPerformer").className = ""; +} + +function hangupComplete() { + try { + getById("main").innerHTML = document.getElementById("hangupTemplate").innerHTML; + + } catch (e) { } + + updateMixerRun = function () { }; + + pokeIframeAPI("hungup", true); // don't use Hangup, as that's an action. + pokeAPI("hangup", true); + + if (session.redirectHangup) { + setTimeout(function (href) { + window.location.href = href; + }, session.redirectHangupTimer || 0, session.redirectHangup); + } +} + +function reloadRequested() { + pokeIframeAPI("reloading", true); + window.removeEventListener("beforeunload", confirmUnload); // clear the confirm on reload + location.reload(); // the main reload function call +} +function confirmUnload(event) { + if (!session.noExitPrompt && !session.cleanOutput && session.scene === false && (session.seeding || session.roomid !== false || session.permaid !== false || session.director)) { + (event || window.event).returnValue = "Are you sure you want to exit?"; //Gecko + IE + return "Are you sure you want to exit?"; + } else { + return undefined; // ADDED OCT 29th; get rid of popup. Just close the socket connection if the user is refreshing the page. It's one or the other. + } +} + +function gobackSlide() { + var data = {}; + data.data = [176, 110, 10]; + sendRawMIDI(data); + try { + pokeIframeAPI("back-slide", true); + } catch (e) { } +} + +function nextSlide() { + var data = {}; + data.data = [176, 110, 11]; + sendRawMIDI(data); + + try { + pokeIframeAPI("next-slide", true); + } catch (e) { } +} + +function raisehand() { + if (session.raisehands !== 2 && session.directorUUID == false) { + // fine + log("no director in room yet"); + return false; + } + + var data = {}; + var handstate = false; + + log(data); + if (getById("raisehandbutton").dataset.raised == "0") { + getById("raisehandbutton").dataset.raised = "1"; + getById("raisehandbutton").classList.add("raisedHand"); + data.chat = "Raised hand"; + handstate = true; + log("hand raised"); + } else { + log("hand lowered"); + getById("raisehandbutton").dataset.raised = "0"; + getById("raisehandbutton").classList.remove("raisedHand"); + data.chat = "Lowered hand"; + handstate = false; + } + + if (session.raisehands == 2) { + session.sendMessage(data); + } else { + for (var i = 0; i < session.directorList.length; i++) { + data.UUID = session.directorList[i]; + session.sendMessage(data, data.UUID); + } + } + + try { + pokeIframeAPI("hand", handstate); + } catch (e) { } + + return handstate; +} + +function lowerhand() { + log("hand lowered"); + getById("raisehandbutton").dataset.raised = "0"; + getById("raisehandbutton").classList.remove("raisedHand"); + pokeIframeAPI("hand", false); + return false; +} + +var previousRoom = ""; +var stillNeedRoom = true; +var transferCancelled = false; +var armedTransfer = false; +var transferSettings = {}; + +async function directMigrate(ele, event, room = false) { + // everyone in the room will hangup this guest also? I like that idea. What about the STREAM ID? I suppose we don't kick out if the viewID matches. + log("directMigrate"); + if (room) { + var migrateRoom = room; + } else if (event === false) { + if (previousRoom === null) { + // user cancelled in previous callback + ele.innerHTML = ' transfer'; + miniTranslate(ele); + //ele.style.backgroundColor = null; + ele.classList.remove("armed"); + return false; + } + if (transferCancelled === true) { + ele.innerHTML = ' transfer'; + miniTranslate(ele); + //ele.style.backgroundColor = null; + ele.classList.remove("armed"); + return false; + } + var migrateRoom = previousRoom; + } else if (event.ctrlKey || event.metaKey) { + ele.innerHTML = ' armed'; + miniTranslate(ele); + ele.classList.add("armed"); + //ele.style.backgroundColor = "#BF3F3F"; + transferCancelled = false; + //armedTransfer=true; + Callbacks.push([directMigrate, ele, stillNeedRoom]); + stillNeedRoom = false; + log("Migrate queued"); + return true; + // } else if (armedTransfer){ + //migrateRoom = sanitizeRoomName(previousRoom); + } else { + if (armedTransfer !== false && previousRoom !== "") { + var migrateRoom = sanitizeRoomName(previousRoom); + } else { + var broadcastMode = null; + if ("broadcast" in transferSettings) { + broadcastMode = transferSettings.broadcast; + } else if (session.rpcs[ele.dataset.UUID] && session.rpcs[ele.dataset.UUID].stats.info && "broadcast_mode" in session.rpcs[ele.dataset.UUID].stats.info) { + broadcastMode = session.rpcs[ele.dataset.UUID].stats.info.broadcast_mode; + } else if (session.broadcastTransfer !== null) { + broadcastMode = session.broadcastTransfer; + } + + var queuedMode = null; + if ("queue" in transferSettings) { + queuedMode = transferSettings.queue; + } else if (session.queueTransfer) { + queuedMode = session.queueTransfer; + } + + var updateurl = null; + if ("updateurl" in transferSettings) { + updateurl = transferSettings.updateurl; + } + window.focus(); + + var response = await promptTransfer(previousRoom, broadcastMode, updateurl, queuedMode); + var migrateRoom = response.roomid; + if (migrateRoom !== null) { + transferSettings = response; + } + } + stillNeedRoom = true; + if (migrateRoom === null) { + // user cancelled + ele.innerHTML = ' transfer'; + miniTranslate(ele); + //ele.style.backgroundColor = null; + ele.classList.remove("armed"); + transferCancelled = true; + return false; + } + try { + migrateRoom = sanitizeRoomName(migrateRoom); + previousRoom = migrateRoom; + } catch (e) { } + } + ele.innerHTML = ' transfer'; + miniTranslate(ele); + //ele.style.backgroundColor = null; + ele.classList.remove("armed"); + + if (migrateRoom) { + previousRoom = migrateRoom; + session.directMigrateIssue(migrateRoom, transferSettings, ele.dataset.UUID); + return true; + } +} + +var stillNeedHangupTarget = 1; +async function directHangup(ele, event) { + // everyone in the room will hangup this guest? I like that idea. + var confirmHangup = false; + var blockUser = false; + + if (event == false) { + // Multi-user armed hangup mode + if (stillNeedHangupTarget === 1) { + window.focus(); + confirmHangup = confirm(getTranslation("confirm-disconnect-users")); + stillNeedHangupTarget = confirmHangup; + } else { + confirmHangup = stillNeedHangupTarget; + } + } else if (event === true) { + confirmHangup = true; + } else if (event.ctrlKey || event.metaKey) { + ele.innerHTML = ' ARMED'; + miniTranslate(ele); + ele.classList.add("armed"); + //ele.style.backgroundColor = "#BF3F3F"; + stillNeedHangupTarget = 1; + Callbacks.push([directHangup, ele, false]); + log("Hangup queued"); + return; + } else { + // Single user hangup - show dialog with block option + window.focus(); + var result = await confirmHangupWithBlock(getTranslation("confirm-disconnect-user")); + confirmHangup = result.confirmed; + blockUser = result.block; + } + + if (confirmHangup) { + var msg = {}; + msg.hangup = true; + + // If director chose to block, just set the flag - guest will use their own room info + if (blockUser) { + msg.block = true; + } + + log(msg); + log(ele.dataset.UUID); + var targetUUID = ele.dataset.UUID; + session.sendRequest(msg, targetUUID); + pokeIframeAPI("hungup", "directing", targetUUID); + //session.anysend(msg); // send to everyone in the room, so they know if they are on air or not. + + // Delayed fallback: if the hangup message never arrives (dead connection), + // clean up locally after 4 seconds to prevent stuck control boxes for co-directors + if (session.rpcs[targetUUID]) { + var sessionAtHangup = session.rpcs[targetUUID].session; // Capture session ID to verify it's the same connection + var fallbackTimeout = setTimeout(function(uuid, origSession) { + try { + // Only close if it's still the same session (not a reconnect or re-request) + if (!(uuid in session.rpcs) || session.rpcs[uuid].session !== origSession) { + return; + } + // Skip if connection AND data channel are healthy - message should have been delivered + var rpc = session.rpcs[uuid]; + var connState = rpc.connectionState || "unknown"; + var channelOpen = rpc.receiveChannel && rpc.receiveChannel.readyState === "open"; + if (connState === "connected" && channelOpen) { + warnlog("Hangup fallback: skipping - connection and channel healthy"); + return; + } + warnlog("Hangup fallback: cleaning up (conn: " + connState + ", channel: " + (channelOpen ? "open" : "closed") + ")"); + session.closeRPC(uuid, true); + } catch (e) { + warnlog(e); + } + }, 4000, targetUUID, sessionAtHangup); + // Store timeout so closeRPC can cancel it if cleanup happens normally + session.rpcs[targetUUID].hangupFallbackTimeout = fallbackTimeout; + } + + return true; + } else { + ele.innerHTML = ' Hangup'; + miniTranslate(ele); + //ele.style.backgroundColor = null; + ele.classList.remove("armed"); + return false; + } +} + +function getAutoAssignChannel() { + // Returns channel number (1-8) to assign based on session.autochannels config + if (!session.autochannels || !session.autochannels.length) return false; + + // Build usage map: channel -> count of guests using it + var usage = {}; + session.autochannels.forEach(function(ch) { usage[ch] = 0; }); + + // Count current assignments from sceneAudioChannel buttons in guest containers + var buttons = document.querySelectorAll('#guestFeeds [data-action-type="sceneAudioChannel"][data-state="1"]'); + buttons.forEach(function(btn) { + var ch = parseInt(btn.dataset.channel); + if (ch in usage) { + usage[ch]++; + } + }); + + if (session.autochannelmode === "roundrobin") { + // Pick next in sequence, wrap around + var ch = session.autochannels[session.autochannelIndex % session.autochannels.length]; + session.autochannelIndex++; + return ch; + } else { + // "leastused" mode: pick channel with fewest guests (enables stacking) + var minCount = Infinity; + var bestChannel = session.autochannels[0]; + for (var i = 0; i < session.autochannels.length; i++) { + var ch = session.autochannels[i]; + if (usage[ch] < minCount) { + minCount = usage[ch]; + bestChannel = ch; + } + } + return bestChannel; + } +} + +function autoAssignAudioChannel(UUID) { + // Auto-assign a newly joined guest to an audio channel based on session.autochannels config + if (!session.autochannels) return; + + var channel = false; + + // Check if guest has a preferred channel and it's in the allowed list + if (session.rpcs[UUID] && session.rpcs[UUID].preferChannel) { + var preferred = session.rpcs[UUID].preferChannel; + if (session.autochannels.includes(preferred)) { + channel = preferred; + log("Using guest's preferred channel C" + channel); + } + } + + // Fall back to auto-assignment if no valid preferred channel + if (!channel) { + channel = getAutoAssignChannel(); + } + if (!channel) return; + + // Find the guest's container + var container = getById("container_" + UUID); + if (!container) return; + + // Find the channel button in the guest's container + var btn = container.querySelector('[data-action-type="sceneAudioChannel"][data-channel="' + channel + '"]'); + if (!btn) return; + + // Set button state (skip C4 warning dialog since user configured allowed channels) + btn.dataset.state = "1"; + btn.classList.add("pressed"); + btn.ariaPressed = "true"; + + // Build and send message to scene viewers + var msg = {}; + msg.audioOutputChannel = channel; + msg.sid = session.rpcs[UUID].streamID; + + for (var uuid in session.pcs) { + if (session.pcs[uuid].scene !== false) { + session.sendMessage(msg, uuid); + } + } + + // Sync to co-directors + syncDirectorState(btn); + + log("Auto-assigned " + session.rpcs[UUID].streamID + " to channel C" + channel); +} + +async function directAudioChannel(ele, event, director = false) { + var UUID = ele.dataset.UUID; + var channel = parseInt(ele.dataset.channel); + var added = false; + + if (!(event.ctrlKey || event.metaKey)) { + if (ele.dataset.state == "1") { + ele.dataset.state = "0"; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } else { + if (channel == 4) { + if (event) { + let ret = await confirmAlt("⚠️Warning! Channel 4 should be avoided.\n\nChannel 4 in OBS is used for low-frequency audio and may distort the audio if you record to it.\n\nDo you wish to proceed?", false); + if (!ret) { + return; + } + } + } + ele.dataset.state = "1" + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + added = true; + } + } else if (ele.dataset.state == "1") { + added = true; + } + + if (director) { + + if (added) { + document.querySelectorAll('#controls_director [data-action-type="sceneAudioChannel"][data-state="1"]:not([data-channel="' + channel + '"])').forEach(el => { + el.dataset.state = "0"; + el.classList.remove("pressed"); + el.ariaPressed = "false"; + }); + } else { + document.querySelectorAll('#controls_director [data-action-type="sceneAudioChannel"][data-state="1"][data-channel]').forEach(el => { + el.dataset.state = "0"; + el.classList.remove("pressed"); + el.ariaPressed = "false"; + }); + channel = false; + } + + } else { + + if (added) { + document.querySelectorAll('[data-sid][data--u-u-i-d="' + UUID + '"][data-action-type="sceneAudioChannel"][data-state="1"]:not([data-channel="' + channel + '"])').forEach(el => { + el.dataset.state = "0"; + el.classList.remove("pressed"); + el.ariaPressed = "false"; + }); + } else { + document.querySelectorAll('[data-sid][data--u-u-i-d="' + UUID + '"][data-channel][data-action-type="sceneAudioChannel"][data-state="1"]').forEach(el => { + el.dataset.state = "0"; + el.classList.remove("pressed"); + el.ariaPressed = "false"; + }); + channel = false; + } + } + + var msg = {}; + + msg.audioOutputChannel = channel; + if (director) { + msg.sid = session.streamID; + } else { + msg.sid = ele.dataset.sid; + } + + for (var uuid in session.pcs) { + if (session.pcs[uuid].scene !== false) { + session.sendMessage(msg, uuid); + } + } + syncDirectorState(ele); + + if (channel) { + return true; + } else { + return false; + } +} + +function directEnable(ele, event, director = false) { + // A directing room only is controlled by the Director, with the exception of MUTE. + var scene = ele.dataset.scene; + if (!(event.ctrlKey || event.metaKey)) { + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + if (ele.children[1]) { + ele.children[1].innerHTML = "Add to Scene " + scene; + } + if (director) { + var cc = 0; + getById("container_director") + .querySelectorAll('[data-action-type="addToScene"]') + .forEach(ge => { + if (ge.value == 1) { + cc += 1; + } + }); + if (!cc) { + getById("container_director").style.backgroundColor = null; + getById("container_director").classList.remove("containerGreen"); + } + } else { + var cc = 0; + getById("container_" + ele.dataset.UUID) + .querySelectorAll('[data-action-type="addToScene"]') + .forEach(ge => { + if (ge.value == 1) { + cc += 1; + log("ge.value: '" + ge.value + "'"); + } else { + log("ge.value:--'" + ge.value + "'"); + } + }); + log(cc + " " + "container_" + ele.dataset.UUID); + if (!cc) { + getById("container_" + ele.dataset.UUID).style.backgroundColor = null; + getById("container_" + ele.dataset.UUID).classList.remove("containerGreen"); + } + } + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + if (ele.children[1]) { + ele.children[1].innerHTML = "Remove"; + } + if (director) { + getById("container_director").classList.add("containerGreen"); + } else { + getById("container_" + ele.dataset.UUID).classList.add("containerGreen"); + } + } + } + + var msg = {}; + + scene = scene + ""; + + msg.scene = scene; + msg.action = "display"; + msg.value = ele.value; + msg.target = ele.dataset.sid; + + try { + if (msg.value == 1) { + pokeIframeAPI("add-to-scene", scene, ele.dataset.UUID); + } else { + pokeIframeAPI("remove-from-scene", scene, ele.dataset.UUID); + } + } catch (e) { } + + //for (var uuid in session.pcs){ // removing this since it's obsolete at this point. + // if (session.pcs[uuid].stats.info && ("version" in session.pcs[uuid].stats.info) && (session.pcs[uuid].stats.info.version < 17.2)){ + //// msg.request = "sendroom"; + // session.sendMsg(msg); + // return; + // } + //} + for (var uuid in session.pcs) { + if (session.pcs[uuid].scene === scene) { + session.sendMessage(msg, uuid); + } + } + syncDirectorState(ele); + + if (msg.value) { + return true; + } else { + return false; + } +} + +function syncDirectorState(ele) { + //if (session.director){ // assumed director, since this is a directEnable sub-function + var msg = {}; + msg.directorState = getDetailedState(ele.dataset.sid); + + for (var uuid in session.pcs) { + if (session.pcs[uuid].coDirector) { + session.sendMessage(msg, uuid); + } + } + for (var i in session.directorList) { + var uuid = session.directorList[i]; + if (session.rpcs[uuid]) { + session.sendRequest(msg, uuid); + } + } + + pokeAPI("details", msg.directorState); +} + +function getQuickStats(sid = false) { + var stats = {}; + try { + stats.inbound = {}; + stats.outbound = {}; + + stats.streamID = session.streamID; + + if (session.whipOut && session.whipOut.stats) { + myStats.whip_outbound = session.whipOut.stats; + } + if (session.whepIn && session.whepIn.stats) { + myStats.whep_inbound = session.whepIn.stats; + } + + for (var i in session.rpcs) { + if (session.rpcs[i].streamID) { + stats.inbound[session.rpcs[i].streamID] = session.rpcs[i].stats; + } + } + for (var i in session.pcs) { + stats.outbound[i] = session.pcs[i].stats; + } + } catch (e) { } + if (sid) { + if (sid in stats.inbound) { + return stats.inbound[sid]; + } else { + return null; + } + } + return stats; +} + +function getDetailedState(sid = false) { + var streamList = {}; + var guestFeeds = document.getElementById("guestFeeds"); + + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].streamID) { + if (sid && sid !== session.rpcs[UUID].streamID) { + continue; + } + let item = {}; + item.streamID = session.rpcs[UUID].streamID; + item.label = session.rpcs[UUID].label; + item.group = session.rpcs[UUID].group; + + if (session.rpcs[UUID].stats && session.rpcs[UUID].stats.info) { + item.miscellaneous = session.rpcs[UUID].stats.info; + } + + try { + item.layout = session.rpcs[UUID].layout; + + if (session.director && session.slotmode) { + item.slot = getSlotState(UUID); + } else if (session.currentSlots) { + item.slot = Object.keys(session.currentSlots).find(key => session.currentSlots[key] === session.rpcs[UUID].streamID) || false; + } + + if (item.slot) { + item.slot = parseInt(item.slot); + } + + if (session.director) { + let featured = query("[data--u-u-i-d='" + UUID + "'][data-action-type='solo-video']"); + if (featured && parseInt(featured.value)) { + item.featured = true; + } else { + item.featured = false; + } + } else if (session.infocus && session.infocus === UUID) { + item.featured = true; + } else { + item.featured = false; + } + } catch (e) { + errorlog(e); + } + + item.iframeSrc = session.rpcs[UUID].iframeSrc; + item.localStream = false; + item.muted = session.rpcs[UUID].remoteMuteState; + item.videoMuted = session.rpcs[UUID].videoMuted; + try { + item.activeSpeaker = session.rpcs[UUID].activelySpeaking; + item.defaultSpeaker = session.rpcs[UUID].defaultSpeaker; + } catch (e) { + errorlog(e); + } + + item.videoVisible = session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.checkVisibility(); + if (session.rpcs[UUID].videoElement) { + item.videoVolume = session.rpcs[UUID].videoElement.volume; + } + item.iframeVisible = session.rpcs[UUID].iframeVisible && session.rpcs[UUID].iframeVisible.checkVisibility(); + + if (session.directorList.indexOf(UUID) >= 0) { + item.director = true; + } else { + item.director = false; + } + try { + if (session.director) { + if (guestFeeds) { + var lock = parseInt(document.getElementById("position_" + UUID).dataset.locked); + if (lock) { + item.position = lock; // probably should make a universal function to do this, for all lock requesting + } else { + var child = document.getElementById("container_" + UUID); + if (child) { + var parent = child.parentNode; + if (parent.id == "guestFeeds") { + item.position = Array.prototype.indexOf.call(parent.children, child) + 1; + } + } + } + } + var scenes = getById("container_" + UUID).querySelectorAll('[data-action-type="addToScene"][data-scene][data--u-u-i-d="' + UUID + '"]'); + var sceneState = {}; + for (var i = 0; i < scenes.length; i++) { + if (scenes[i].value == 1) { + sceneState[scenes[i].dataset.scene] = true; + } else { + sceneState[scenes[i].dataset.scene] = false; + } + } + item.scenes = sceneState; + + var others = getById("container_" + UUID).querySelectorAll('[data-action-type][data--u-u-i-d="' + UUID + '"]'); + var otherState = {}; + for (var i = 0; i < others.length; i++) { + if (!others[i] || !others[i].dataset) { + continue; + } + if (others[i].dataset.actionType === "solo-video" && + others[i].classList && + others[i].classList.contains("altpress")) { + otherState[others[i].dataset.actionType] = "alt"; + continue; + } else if (others[i].dataset.actionType === "remove-queue") { + if (others[i].classList.contains("hidden")) { + otherState[others[i].dataset.actionType] = false; + } else { + otherState[others[i].dataset.actionType] = true; + } + continue; + } else if (others[i].dataset.actionType === "hand-raised") { + if (others[i].classList.contains("hidden")) { + otherState[others[i].dataset.actionType] = false; + } else { + otherState[others[i].dataset.actionType] = true; + } + continue; + } + if ("scene" in others[i].dataset) { + continue; + } else if ("toggle-group" == others[i].dataset.actionType) { + continue; + } else if ("value" in others[i]) { + if (others[i].value !== "") { + otherState[others[i].dataset.actionType] = others[i].value; + } + } else { + try { + if (others[i].querySelector(".altpress")) { + otherState[others[i].dataset.actionType] = "alt"; + } else if (others[i].querySelector(".pressed")) { + otherState[others[i].dataset.actionType] = true; + } else if (others[i].dataset.actionType == "soloChat") { + otherState[others[i].dataset.actionType] = false; + } + } catch (e) { + errorlog(e); + } + } + } + item.others = otherState; + } + } catch (e) { } + + streamList[session.rpcs[UUID].streamID] = item; + } + } + + if (sid && sid !== session.streamID) { + return streamList; + } + + streamList[session.streamID] = {}; + + try { + if (session.director) { + var sceneState = {}; + var scenes = getById("container_director").querySelectorAll('[data-action-type="addToScene"][data-scene]'); + for (var i = 0; i < scenes.length; i++) { + if (scenes[i].value == 1) { + sceneState[scenes[i].dataset.scene] = true; + } else { + sceneState[scenes[i].dataset.scene] = false; + } + } + streamList[session.streamID].scenes = sceneState; + } + } catch (e) { } + + if (session.director) { + let featured = document.querySelector("#highlightDirector[data-action-type='solo-video'], #container_director [data-action-type='solo-video']"); + if (featured && parseInt(featured.value)) { + streamList[session.streamID].featured = true; + } else { + streamList[session.streamID].featured = false; + } + } else if (session.infocus && session.infocus === true) { + streamList[session.streamID].featured = true; + } else { + streamList[session.streamID].featured = false; + } + + streamList[session.streamID].label = session.label; + streamList[session.streamID].meta = session.meta; + streamList[session.streamID].group = session.group; + streamList[session.streamID].groupView = session.groupView; + streamList[session.streamID].scene = session.scene; + streamList[session.streamID].streamID = session.streamID; + streamList[session.streamID].iframeSrc = session.iframeSrc; + streamList[session.streamID].director = session.directorState; //session.director is what you want to be; session.directorState is what you are + streamList[session.streamID].localstream = true; // deprecated. + streamList[session.streamID].localStream = true; + streamList[session.streamID].seeding = session.seeding; + streamList[session.streamID].muted = session.muted; + streamList[session.streamID].videoMuted = session.videoMuted; + streamList[session.streamID].videoVisible = session.videoElement && session.videoElement.checkVisibility(); + streamList[session.streamID].speakerMuted = session.speakerMuted; + streamList[session.streamID].position = null; + streamList[session.streamID].meshcast = session.meshcast; + streamList[session.streamID].layout = session.layout; + + try { + if (session.streamID && session.slotmode) { + // Properly check for director's slots by looking directly at currentSlots + let directorSlot = false; + + // Look for the director's main stream in currentSlots + Object.entries(session.currentSlots).forEach(([slot, sid]) => { + if (sid === session.streamID) { + directorSlot = parseInt(slot); + } + }); + + // If the director has a slot assigned, use it + if (directorSlot) { + streamList[session.streamID].slot = directorSlot; + } else { + // No slot found for director + streamList[session.streamID].slot = false; + } + } + } catch (e) { + errorlog(e); + } + + if (session.info && session.info.out) { + streamList[session.streamID].outbound = session.info.out; + } + + if (session.showDirector && session.director) { + var child = document.getElementById("container_director"); + if (child) { + var parent = child.parentNode; + if (parent.id == "guestFeeds") { + streamList[session.streamID].position = Array.prototype.indexOf.call(parent.children, child) + 1; + } + } + } + + if (session.notifyScreenShare) { + streamList[session.streamID].screenSharing = session.screenShareState; + } else { + streamList[session.streamID].screenSharing = false; + } + + if (session.streamSrc) { + streamList[session.streamID].audioTrack = session.streamSrc.getAudioTracks().length !== 0; + streamList[session.streamID].videoTrack = session.streamSrc.getVideoTracks().length !== 0; + } else { + streamList[session.streamID].audioTrack = false; + streamList[session.streamID].videoTrack = false; + } + + return streamList; +} + +function getGuestList() { + var guestFeeds = document.getElementById("guestFeeds"); + if (!guestFeeds) { + return {}; + } + var streamList = {}; + for (var i = 0; i < guestFeeds.children.length; i++) { + try { + if (session.rpcs[guestFeeds.children[i].dataset.UUID]) { + streamList[i + 1 + ""] = { streamID: session.rpcs[guestFeeds.children[i].dataset.UUID].streamID, label: session.rpcs[guestFeeds.children[i].dataset.UUID].label || "" }; + } else if (guestFeeds.children[i].id == "container_director") { + streamList[i + 1 + ""] = { streamID: session.streamID, label: session.label || "" }; + } else if (guestFeeds.children[i].id == "container_screen_director") { + streamList[i + 1 + ""] = { streamID: session.streamID + ":s", label: session.screenShareLabel || "" }; + } + } catch (e) { + errorlog(e); + } + } + + return streamList; +} + +function syncOtherState(sid) { + if (!session.syncState) { + return; + } + if (!session.syncState[sid]) { + return; + } + + /* if (session.rpcs[ele.dataset.UUID].directorMutedState==1){ + pokeIframeAPI("director-mute-state", true, ele.dataset.UUID); + pokeAPI("directorMuted", true, session.rpcs[ele.dataset.UUID].streamID); + } else { + pokeIframeAPI("director-mute-state", false, ele.dataset.UUID); + pokeAPI("directorMuted", false, session.rpcs[ele.dataset.UUID].streamID); + } */ + + var others = session.syncState[sid].others; + + log(others); + + for (var other in others) { + if (other == "toggle-group") { + continue; + } + var ele = document.querySelector('[data-sid="' + sid + '"][data-action-type="' + other + '"]'); + if (ele) { + var state = others[other]; + if (state === "alt" && other === "solo-video") { + if ("value" in ele) { + ele.value = 1; + } + if (ele.nodeName && ele.nodeName.toLowerCase() == "input") { + try { + ele.checked = true; + } catch (e) { } + } + ele.classList.add("altpress"); + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + continue; + } else if (other === "remove-queue") { + if (state) { + ele.classList.remove("hidden"); + } else { + ele.classList.add("hidden"); + } + continue; + } else if (other === "hand-raised") { + if (state) { + ele.classList.remove("hidden"); + } else { + ele.classList.add("hidden"); + } + continue; + } else { + ele.classList.remove("altpress"); + } + if (state) { + if (!("value" in ele)) { + errorlog("NO DEFAULT VALUE IN SPECIFIED ELEMENT; guessing default: " + other); + ele.value = 0; + } + var changed = true; + if (ele.value == state) { + changed = false; + } + if (other == "mute-guest") { + if (changed) { + remoteMute(ele, false, true); + } + } else if (other == "hide-guest") { + if (changed) { + remoteHideVideo(ele, true, true); + } + } else if (other == "mute-video-guest") { + if (changed) { + remoteMuteVideo(ele, true, true); + } + } else { + ele.value = state; + + if (ele.nodeName.toLowerCase() == "input") { + ele.value = parseInt(state); + } else if (parseInt(state)) { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } else { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } + } + } + } + } + + var UUID = document.querySelector('[data-sid="' + sid + '"][data--u-u-i-d'); + if (UUID && UUID.dataset.UUID) { + UUID = UUID.dataset.UUID; + if (session.syncState[sid].group && session.rpcs[UUID]) { + session.rpcs[UUID].group = session.syncState[sid].group; + syncGroup(session.rpcs[UUID].group, UUID); + } + } +} + +function htmlToElement(html) { + var template = document.createElement("template"); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + return template.content.firstChild; +} + +function syncGroup(groups, UUID) { + if (!groups || typeof groups !== "object") { + errorlog("Group isn't an object"); + return; + } + groups.forEach(group => { + var ele = getById("container_" + UUID).querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]'); + if (!ele) { + var newGroup = htmlToElement('"); + + var added = false; + getById("container_" + UUID) + .querySelectorAll(".customGroup>[data-group]") + .forEach(ele => { + log(ele); + if (!added && ele.dataset.group > group + "") { + ele.parentNode.insertBefore(newGroup, ele); + added = true; + } + }); + if (!added) { + var newGroupCon = getById("container_" + UUID).querySelector(".customGroup"); + if (!newGroupCon) { + newGroupCon = document.createElement("div"); + newGroupCon.classList.add("customGroup"); + getById("container_" + UUID).appendChild(newGroupCon); + } + newGroupCon.appendChild(newGroup); + } + } + }); + + var elements = document.querySelectorAll('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group]'); + if (elements.length) { + for (var i = 0; i < elements.length; i++) { + if (session.rpcs[UUID].group.includes(elements[i].dataset.group)) { + elements[i].classList.add("pressed"); + elements[i].ariaPressed = "true"; + } else { + elements[i].classList.remove("pressed"); + elements[i].ariaPressed = "false"; + } + } + log("synced group"); + } else { + log("not syncing group buttons; don't exist"); + } +} + +function syncLabelState(sid) { + if (!session.syncState || !session.syncState[sid]) { + return; + } + var newLabel = session.syncState[sid].label; + // Find the UUID for this streamID + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].streamID === sid) { + // Update the RPC label + if (session.rpcs[UUID].label !== newLabel) { + session.rpcs[UUID].label = newLabel; + // Label came from director sync - mark it to prevent info message from overwriting + if (newLabel) { + session.rpcs[UUID].labelSetByDirector = true; + } else { + session.rpcs[UUID].labelSetByDirector = false; + } + // Update the UI label element + var labelEle = document.getElementById("label_" + UUID); + if (labelEle) { + if (newLabel) { + labelEle.innerText = newLabel; + labelEle.classList.remove("addALabel"); + } else if (session.directorUUID === UUID) { + miniTranslate(labelEle, "main-director"); + labelEle.classList.remove("addALabel"); + } else if (session.directorList.indexOf(UUID) >= 0) { + miniTranslate(labelEle, "co-director"); + labelEle.classList.remove("addALabel"); + } else { + miniTranslate(labelEle, "add-a-label"); + labelEle.classList.add("addALabel"); + } + } + log("synced label for " + sid + ": " + newLabel); + } + break; + } + } +} + +function syncSceneState(sid) { + if (!session.syncState) { + return; + } + if (!session.syncState[sid]) { + return; + } + var scenes = session.syncState[sid].scenes || []; + for (var scene in scenes) { + try { + var ele = document.querySelector('[data-sid="' + sid + '"][data-action-type="addToScene"][data-scene="' + scene + '"]'); + if (ele) { + if (scenes[scene]) { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + getById("container_" + ele.dataset.UUID).classList.add("containerGreen"); + if (ele.children[1]) { + ele.children[1].innerHTML = "Remove"; + } + } else { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + if (ele.children[1]) { + ele.children[1].innerHTML = "Add to Scene " + scene; + } + } + } + } catch (e) { } + } +} + +function issueLayout(scene = false, UUID = false) { + // A directing room only is controlled by the Director, with the exception of MUTE. + log("issueLayout() called"); + + var msg = {}; + msg.layout = session.layout; + msg.layout_array = session.layout_array; + + //try { + // pokeIframeAPI("layout", {layout:layout, scene:scene}); + //} catch(e){} + + /* session.layout = { + "stevetestA": { + x:0, + y:0, + w:40, + h:40, + z:0, + c:false + + }, + "stevetestB": { + x:50, + y:50, + w:40, + h:40, + z:1, + c:true + } + }; */ + + if (UUID) { + if (session.pcs[UUID] && scene !== false && session.pcs[UUID].scene === scene + "" && !session.pcs[UUID].solo && session.pcs[UUID].layout) { + // scene specified + session.sendMessage(msg, UUID); + session.pcs[UUID].layoutState = normalizeLayoutStateValue(session.layout); + } else if (session.pcs[UUID] && session.pcs[UUID].layout && !session.pcs[UUID].solo) { + // no scene targetted + session.sendMessage(msg, UUID); + session.pcs[UUID].layoutState = normalizeLayoutStateValue(session.layout); + log("broadcast"); + } + } else { + for (var uuid in session.pcs) { + if (scene !== false && session.pcs[uuid].scene === scene + "" && !session.pcs[uuid].solo && session.pcs[uuid].layout) { + session.sendMessage(msg, uuid); + session.pcs[uuid].layoutState = normalizeLayoutStateValue(session.layout); + } else if (session.pcs[uuid].layout && !session.pcs[uuid].solo) { + session.sendMessage(msg, uuid); + session.pcs[uuid].layoutState = normalizeLayoutStateValue(session.layout); + log("broadcast"); + } + } + } +} + +async function issueLayoutOBS(data) { + // A directing room only is controlled by the Director, with the exception of MUTE. + + var layout = data.layout || false; + var scene = data.scene || false; + var UUID = data.UUID || false; + var obsCommand = data.obsCommand || false; + const normalizedLayoutState = normalizeLayoutStateValue(layout); + + log("issueLayoutOBS() called"); + var msg = {}; + msg.layout = layout; + msg.obsCommand = obsCommand; + + if (data.remote) { + msg.remote = data.remote; + } else { + msg.remote = session.remote || true; + } + msg = await session.encodeRemote(msg); + + if (UUID) { + try { + log("CONTROL STATE" + session.pcs[UUID].obsState.details.controlLevel); + } catch (e) { } + + if (session.pcs[UUID] && scene !== false && session.pcs[UUID].scene === scene + "") { + if (!session.pcs[UUID].solo) { + session.sendMessage(msg, UUID); + session.pcs[UUID].layoutState = normalizedLayoutState; + } + } else if (session.pcs[UUID] && session.pcs[UUID].layout) { + session.sendMessage(msg, UUID); + session.pcs[UUID].layoutState = normalizedLayoutState; + log("broadcast"); + } + } else { + for (var uuid in session.pcs) { + try { + log("CONTROL STATE" + session.pcs[UUID].obsState.details.controlLevel); + } catch (e) { } + + if (scene !== false && session.pcs[uuid].scene === scene + "") { + if (!session.pcs[uuid].solo) { + session.sendMessage(msg, uuid); + session.pcs[uuid].layoutState = normalizedLayoutState; + } + } else if (session.pcs[uuid].layout) { + session.sendMessage(msg, uuid); + session.pcs[uuid].layoutState = normalizedLayoutState; + log("broadcast"); + } + } + } +} + +var previousURL = ""; +var stillNeedURL = true; +var reloadCancelled = false; +var armedReload = false; + +async function directPageReload(ele, event) { + log("URL Page reload"); + if (event === false) { + if (previousURL === null) { + // user cancelled in previous callback + ele.innerHTML = ' change URL'; + miniTranslate(ele); + ele.classList.remove("armed"); // ele.style.backgroundColor = null; + return; + } + if (reloadCancelled === true) { + ele.innerHTML = ' change URL'; + miniTranslate(ele); + ele.classList.remove("armed"); + //ele.style.backgroundColor = null; + return; + } + reloadURL = previousURL; + } else if (event.ctrlKey || event.metaKey) { + ele.innerHTML = ' armed'; + miniTranslate(ele); + ele.classList.add("armed"); + //ele.style.backgroundColor = "#BF3F3F"; + reloadCancelled = false; + armedReload = true; + Callbacks.push([directPageReload, ele, stillNeedURL]); + stillNeedURL = false; + log("URL update queued"); + return; + } else if (armedReload) { + reloadURL = previousURL; + } else { + window.focus(); + var reloadURL = await promptAlt(getTranslation("transfer-guest-to-url"), false, false, previousURL); + stillNeedURL = true; + if (reloadURL === null) { + // user cancelled + ele.innerHTML = ' change URL'; + miniTranslate(ele); + ele.classList.remove("armed"); + //ele.style.backgroundColor = null; + reloadCancelled = true; + return; + } + try { + previousURL = reloadURL; + } catch (e) { } + } + ele.innerHTML = ' change URL'; + miniTranslate(ele); + ele.classList.remove("armed"); //ele.style.backgroundColor = null; + + if (reloadURL) { + previousURL = reloadURL; + + var msg = {}; + msg.changeURL = reloadURL; + if (ele.dataset.UUID in session.rpcs) { + session.rpcs[ele.dataset.UUID].receiveChannel.send(JSON.stringify(msg)); + } + } +} + +async function directTimer(ele, event = false, manualSetTime = false) { + // A directing room only is controlled by the Director, with the exception of MUTE. + log("directTimer"); + var msg = {}; + ele.classList.remove("blue"); + ele.classList.remove("red"); + if (!event || !(event.ctrlKey || event.metaKey)) { + if (ele.value == 0 || ele.value == 2) { + if (manualSetTime !== false) { + var getTime = parseFloat(manualSetTime) || 0; + } else { + var getTime = await promptAlt("Time to set count down timer", false, false, parseInt(getById("overlayClockContainer").dataset.initial), true); + } + if (getTime === null) { + return; + } + getById("overlayClockContainer").dataset.initial = parseInt(getTime); + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.classList.remove("red"); + msg.setClock = getTime; + msg.showClock = true; + msg.startClock = true; + + ele.innerHTML = ' Remove Timer'; + } else if (ele.value == 3) { + ele.value = 1; + msg.resumeClock = true; + ele.classList.add("red"); + } else { + ele.value = 2; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + msg.stopClock = true; + msg.hideClock = true; + ele.innerHTML = ' Create Timer'; + } + //miniTranslate(ele); + } else if (event.ctrlKey || event.metaKey) { + if (ele.value == 1) { + ele.value = 3; + msg.pauseClock = true; + ele.classList.add("blue"); + } else if (ele.value == 3) { + ele.value = 1; + msg.resumeClock = true; + ele.classList.add("red"); + } + } + + if (!session.director) { + return; + } + + if (ele.dataset.UUID) { + if (session.sendRequest(msg, ele.dataset.UUID)) { + return true; + } + } else { + if (session.sendRequest(msg)) { + return true; + } + } + return false; +} +function formatTime24h(date, clock24 = true) { + let hours = date.getHours(); + let minutes = date.getMinutes(); + + // Ensure hours are always 0-23 + hours = hours % 24; + + // Pad single digit hours and minutes with leading zeros + hours = hours.toString().padStart(2, '0'); + minutes = minutes.toString().padStart(2, '0'); + + if (clock24) { + // 24-hour format + return `${hours}:${minutes}`; + } else { + // 12-hour format + let period = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12 || 12; // Convert 0 to 12 for midnight + return `${hours}:${minutes} ${period}`; + } +} + +function toggleClock(clock24 = session.clock24) { + if (session.showTime === false) { + return; + } + if (session.showTime) { + clearInterval(session.showTime); + session.showTime = null; + var clock = getById("overlayClock2"); + clock.ctx = null; + clock.canvas = null; + + if (document.pictureInPictureElement && clock.video) { + if (document.pictureInPictureElement == clock.video) { + document.exitPictureInPicture(); + pokeIframeAPI("picture-in-picture", false); + } + clock.video.remove; + } + clock.video = null; + clock.innerHTML = ""; + getById("overlayClockContainer2").classList.add("hidden"); + } else { + var time = new Date(); + + var clock = getById("overlayClock2"); + if (clock.ctx) { + clock.ctx.beginPath(); + clock.ctx.rect(0, 0, 230, 40); + clock.ctx.fillStyle = "#000"; + clock.ctx.fill(); + clock.ctx.fillStyle = "#FFF"; + clock.ctx.font = "50px monospace"; + clock.ctx.textAlign = "center"; + clock.ctx.fillText(formatTime24h(time, clock24), 115, 37); + } else { + clock.innerHTML = formatTime24h(time, clock24); + } + + session.showTime = setInterval(function () { + var time = new Date(); + + var clock = getById("overlayClock2"); + if (clock.ctx) { + clock.ctx.beginPath(); + clock.ctx.rect(0, 0, 230, 40); + clock.ctx.fillStyle = "#000"; + clock.ctx.fill(); + clock.ctx.fillStyle = "#FFF"; + clock.ctx.font = "50px monospace"; + clock.ctx.textAlign = "center"; + clock.ctx.fillText(formatTime24h(time, clock24), 115, 37); + } else { + getById("overlayClock2").innerHTML = formatTime24h(time, clock24); + } + }, 2000); + getById("overlayClockContainer2").classList.remove("hidden"); + } + return; +} + +async function directRoomClock(ele, event = false) { + if (ele.active) { + ele.active = false; + session.showRoomTime = false; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } else { + ele.active = true; + session.showRoomTime = true; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } + + if (session.showTime !== false) { + if (ele.active && !session.showTime) { + toggleClock(); + } else if (!ele.active && session.showTime) { + toggleClock(); + } + } + var msg = {}; + if (session.clock24 !== null) { + msg.clock24 = session.clock24; + } + msg.showTime = session.showRoomTime; + session.sendRequest(msg); +} +async function directRoomTimer(ele, event = false, preSetTime = false) { + // A directing room only is controlled by the Director, with the exception of MUTE. + log("directGlobalRoomTimer"); + var msg = {}; + ele.classList.remove("blue"); + ele.classList.remove("red"); + + getById("overlayClockContainer").style.fontSize = "50px"; + + if (!event || !(event.ctrlKey || event.metaKey || event.altKey)) { + if (ele.value == 0 || ele.value == 2) { + if (preSetTime !== false) { + var getTime = preSetTime; + } else { + var getTime = await promptAlt("Time to set count down timer", false, false, parseInt(getById("overlayClockContainer").dataset.initial), true); + } + if (getTime === null) { + return; + } + getTime = parseInt(getTime); + getById("overlayClockContainer").dataset.initial = getTime; + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.classList.remove("red"); + + session.roomTimer = Date.now() / 1000 + getTime; + session.roomTimerGlobal = false; + + msg.setClock = getTime; + setClock(getTime); + msg.showClock = true; + showClock(); + msg.startClock = true; + startClock(); + ele.innerHTML = ' Remove Timer'; + } else if (ele.value == 3) { + ele.value = 1; + msg.resumeClock = true; + resumeClock(); // this needed to be removed, right? + if (!session.roomTimer) { + session.roomTimer = false; + } else if (session.roomTimer > 0) { + session.roomTimer = false; + } else { + session.roomTimer = Date.now() / 1000 - session.roomTimer; + } + ele.innerHTML = ' Remove Timer'; + ele.classList.add("red"); + } else { + ele.value = 2; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + session.roomTimer = false; + msg.stopClock = true; + stopClock(); + msg.hideClock = true; + hideClock(); + ele.innerHTML = ' Create Timer'; + } + //miniTranslate(ele); + } else if (event.ctrlKey || event.metaKey || event.altKey) { + if (ele.value == 1) { + ele.value = 3; + msg.pauseClock = true; + pauseClock(); + if (!session.roomTimer) { + session.roomTimer = false; + } else if (session.roomTimer < Date.now() / 1000) { + session.roomTimer = false; + } else { + session.roomTimer = Date.now() / 1000 - session.roomTimer; + } + ele.innerHTML = ' Resume Timer'; + ele.classList.add("blue"); + } else if (ele.value == 3) { + ele.value = 1; + msg.resumeClock = true; + resumeClock(); + if (!session.roomTimer) { + session.roomTimer = false; + } else if (session.roomTimer > 0) { + session.roomTimer = false; + } else { + session.roomTimer = Date.now() / 1000 - session.roomTimer; + } + ele.innerHTML = ' Remove Timer'; + ele.classList.add("red"); + + } else if (event.altKey && ele.dataset.actionType && !ele.dataset.UUID && (ele.dataset.actionType == "create-timer-global")) { + if (preSetTime !== false) { + var getTime = preSetTime; + } else { + var getTime = await promptAlt("Time to set count down timer .\n(This alt-timer will show in scenes-also)", false, false, parseInt(getById("overlayClockContainer").dataset.initial), true); + } + if (getTime === null) { + return; + } + getTime = parseInt(getTime); + getById("overlayClockContainer").dataset.initial = getTime; + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.classList.remove("red"); + + session.roomTimer = Date.now() / 1000 + getTime; + session.roomTimerGlobal = true; + + msg.setClock = getTime; + setClock(getTime); + msg.showClock = true; + showClock(); + msg.startClock = true; + startClock(); + ele.innerHTML = ' Remove Global Timer'; + } + } + if (!session.director) { + return; + } + if (ele.dataset.UUID) { + session.sendRequest(msg, ele.dataset.UUID); + } else if (session.roomTimerGlobal) { + session.sendPeers(msg); + } else { + session.sendRequest(msg); + } +} + +function updateRemoteTimerButton(UUID, currentTime) { + var elements = document.querySelectorAll('[data-action-type="create-timer"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + if (elements[0].value != 2) { + var time = parseInt(currentTime) || 0; + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + elements[0].value = 1; + if (time < 0) { + time = time * -1; + var minutes = Math.floor(time / 60); + var seconds = time - minutes * 60; + elements[0].classList.add("red"); + elements[0].innerHTML = ' -' + minutes + "m : " + zpadTime(seconds) + "s"; + } else { + var minutes = Math.floor(time / 60); + var seconds = time - minutes * 60; + elements[0].classList.remove("red"); + elements[0].innerHTML = ' ' + minutes + "m : " + zpadTime(seconds) + "s"; + } + } else { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + elements[0].classList.remove("red"); + elements[0].innerHTML = ' Create Timer'; + } + } +} + +function directMute(ele, event = false) { + // A directing room only is controlled by the Director, with the exception of MUTE. + log("mute 2"); + + if (!event || !(event.ctrlKey || event.metaKey)) { + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + miniTranslate(ele, "mute-scene"); + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + miniTranslate(ele, "unmute"); + } + } + var msg = {}; + msg.scene = true; + msg.action = "mute"; + msg.value = ele.value; + msg.target = ele.dataset.sid; + + log(msg); + log("ele:"); + log(ele); + //for (var uuid in session.pcs){ // obsolete at this point; v22 + // if (session.pcs[uuid].stats.info && ("version" in session.pcs[uuid].stats.info) && (session.pcs[uuid].stats.info.version < 17.2)){ + // msg.request = "sendroom"; + // session.sendMsg(msg); + // return; + // } + //} + + for (var uuid in session.pcs) { + if (session.pcs[uuid].scene !== false) { + // send to all scenes (but scene = 0) + session.sendMessage(msg, uuid); + } + } + + syncDirectorState(ele); + + if (msg.value) { + return true; + } else { + return false; + } +} + +function requestFileUpload(ele) { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.disabled = true; + ele.innerHTML = ' Requesting..'; + setTimeout( + function (ele) { + try { + ele.innerHTML = ' Request File'; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + ele.disabled = false; + } catch (e) { } + }, + 15000, + ele + ); + var msg = { + requestUpload: true, + UUID: ele.dataset.UUID + }; + session.sendRequest(msg, ele.dataset.UUID); +} + +function remoteSpeakerMute(ele, event = false) { + log("speaker mute"); + if (!event || !(event.ctrlKey || event.metaKey)) { + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + ele.innerHTML = ' Deafen'; + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.innerHTML = ' Undeafen'; + } + miniTranslate(ele); + } + + var msg = {}; + if (ele.value == 0) { + msg.speakerMute = false; + } else { + msg.speakerMute = true; + } + msg.UUID = ele.dataset.UUID; + session.sendRequest(msg, ele.dataset.UUID); + syncDirectorState(ele); + + errorlog(msg); + + return msg.speakerMute; +} + +function updateRemoteSpeakerMute(UUID) { + var ele = document.querySelectorAll('[data-action-type="toggle-remote-speaker"][data--u-u-i-d="' + UUID + '"]'); + if (ele[0]) { + ele[0].classList.add("pressed"); + ele[0].ariaPressed = "true"; + ele[0].value = 1; + ele[0].innerHTML = ' undeafen'; + miniTranslate(ele[0]); + } + return true; +} + +function updateRemoteDisplayMute(UUID, blind = true) { + var ele = document.querySelectorAll('[data-action-type="toggle-remote-display"][data--u-u-i-d="' + UUID + '"]'); + if (ele[0]) { + if (blind) { + ele[0].classList.add("pressed"); + ele[0].ariaPressed = "true"; + ele[0].value = 1; + ele[0].innerHTML = ' unblind'; + miniTranslate(ele[0]); + return true; + } else { + ele[0].classList.remove("pressed"); + ele[0].ariaPressed = "false"; + ele[0].value = 0; + ele[0].innerHTML = ' blind'; + miniTranslate(ele[0]); + return false; + } + } + return false; +} + +function blindAllGuests(ele, event = false) { + if (!session.director) { + if (!session.cleanOutput) { + warnUser("Only a director can mute other guests"); + } + return; + } // only a director can use this button. + + log("blind all display mute"); + if (!event || !(event.ctrlKey || event.metaKey)) { + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + ele.classList.remove("red"); + ele.innerHTML = ''; + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.classList.add("red"); + ele.innerHTML = ''; + } + } + + var msg = {}; + if (ele.value == 0) { + msg.displayMute = false; + session.directorBlindAllGuests = false; + } else { + msg.displayMute = true; + session.directorBlindAllGuests = true; + } + for (var UUID in session.rpcs) { + // doesn't include scenes, as they don't publiish and this is rpcs + if (session.directorList.indexOf(UUID) >= 0) { + continue; + } // don't try to mute other directors + try { + session.sendRequest(msg, UUID); + updateRemoteDisplayMute(UUID, msg.displayMute); + } catch (e) { + errorlog(e); + } + } + syncDirectorState(ele); + return msg.displayMute; +} + +function remoteDisplayMute(ele, event = false) { + log("display mute"); + if (!event || !(event.ctrlKey || event.metaKey)) { + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + ele.innerHTML = ' Blind'; + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.innerHTML = ' Unblind'; + } + miniTranslate(ele); + } + + var msg = {}; + if (ele.value == 0) { + msg.displayMute = false; + } else { + msg.displayMute = true; + } + msg.UUID = ele.dataset.UUID; + session.sendRequest(msg, ele.dataset.UUID); + syncDirectorState(ele); + return msg.displayMute; +} + +function remoteLowerhands(UUID) { + var msg = {}; + msg.lowerhand = true; + msg.UUID = UUID; + session.sendRequest(msg, UUID); + + try { + getById("hands_" + UUID).classList.add("hidden"); + session.rpcs[UUID].remoteRaisedHandElement.classList.add("hidden"); + } catch (e) { } + + // Sync hand-lowered state to co-directors (only main director syncs) + if (session.directorState !== false) { + try { + if (session.rpcs[UUID] && session.rpcs[UUID].streamID) { + var ele = { dataset: { sid: session.rpcs[UUID].streamID } }; + syncDirectorState(ele); + } + } catch (e) { errorlog(e); } + } + + return true; +} + +function remoteMute(ele, event = false, skipSend = false) { + log("mute"); + var val = parseInt(ele.value) || 0; + if (!event || !(event.ctrlKey || event.metaKey)) { + if (val == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + //ele.innerHTML = ' mute'; + miniTranslate(ele, "mute"); + //ele.innerHTML += getTranslation("mute"); + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + //ele.innerHTML = ' unmute'; + miniTranslate(ele, "unmute"); + } + } + + try { + session.rpcs[ele.dataset.UUID].directorMutedState = ele.value; + var volume = session.rpcs[ele.dataset.UUID].directorVolumeState; + } catch (e) { + errorlog(e); + var volume = 100; + } + + if (!skipSend) { + var msg = {}; + if (val == 1) { + msg.volume = volume; + } else { + msg.volume = 0; + } + msg.UUID = ele.dataset.UUID; + session.sendRequest(msg, ele.dataset.UUID); + syncDirectorState(ele); + log(msg); + } + + if (session.rpcs[ele.dataset.UUID].directorMutedState == 1) { + pokeIframeAPI("director-mute-state", true, ele.dataset.UUID); + pokeAPI("directorMuted", true, session.rpcs[ele.dataset.UUID].streamID); + } else { + pokeIframeAPI("director-mute-state", false, ele.dataset.UUID); + pokeAPI("directorMuted", false, session.rpcs[ele.dataset.UUID].streamID); + } + + if (val) { + return true; + } else { + return false; + } +} + +function toggleQualityGear3() { + toggle(document.getElementById("videoSettings3"), (inline = false)); + if (getById("gear_webcam3").style.display === "inline-block") { + var videoSelect = document.querySelector("select#videoSource3").options; + var obscam = false; + log(videoSelect[videoSelect.selectedIndex].text); + if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) { + // OBS Virtualcam + obscam = true; + } else if (videoSelect[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) { + // OBS Virtualcam + obscam = true; + } + + updateStats(obscam); + } +} + +function remoteHideVideo(ele, event = false, skipSend = false) { + log("video hide"); + + if (!event || event.ctrlKey || event.metaKey) { + //ele.children[1].innerHTML = getTranslation("armed"); + miniTranslate(ele.children[1], "armed"); + //ele.style.backgroundColor = "#BF3F3F"; + ele.classList.add("armed"); + Callbacks.push([remoteHideVideo, ele, false]); + log("video queued"); + return; + } else { + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + ele.innerHTML = ' Hide'; + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.innerHTML = ' Unhide'; + } + miniTranslate(ele); + ele.classList.remove("armed"); //ele.style.backgroundColor = null; + } + + var msg = {}; + if (ele.value == 0) { + msg.directVideoMuted = false; + } else { + msg.directVideoMuted = true; + } + + if (!skipSend) { + for (var i in session.pcs) { + msg.target = ele.dataset.UUID; + + if (i === msg.target) { + msg.target = true; + } + try { + session.pcs[i].sendChannel.send(JSON.stringify(msg)); + } catch (e) { } + } + syncDirectorState(ele); + } + + pokeIframeAPI("director-video-hide-state", msg.directVideoMuted, ele.dataset.UUID); + pokeAPI("directorVideoHide", msg.directVideoMuted, session.rpcs[ele.dataset.UUID].streamID); + + return msg.directVideoMuted; +} + +function remoteMuteVideo(ele, event = false, skipSend = false) { + log("video mute"); + + if (!event || event.ctrlKey || event.metaKey) { + //ele.children[1].innerHTML = getTranslation("armed"); + miniTranslate(ele.children[1], "armed"); + ele.classList.add("armed"); //ele.style.backgroundColor = "#BF3F3F"; + Callbacks.push([remoteMuteVideo, ele, false]); + log("video queued"); + return; + } else { + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + ele.innerHTML = ' Video off'; + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.innerHTML = ' Video on'; + } + miniTranslate(ele); + ele.classList.remove("armed"); + } + + var msg = {}; + if (ele.value == 0) { + msg.remoteVideoMuted = false; + } else { + msg.remoteVideoMuted = true; + } + + if (!skipSend) { + session.sendRequest(msg, ele.dataset.UUID); + syncDirectorState(ele); + } + + pokeIframeAPI("remote-video-mute-state", msg.remoteVideoMuted, ele.dataset.UUID); + pokeAPI("remoteVideoMuted", msg.remoteVideoMuted, session.rpcs[ele.dataset.UUID].streamID); + + return msg.remoteVideoMuted; +} +function updateDirectorVideoHide(UUID) { + var ele = document.querySelectorAll('[data-action-type="hide-guest"][data--u-u-i-d="' + UUID + '"]'); + if (ele[0]) { + ele[0].value = 1; + ele[0].classList.add("pressed"); + ele[0].ariaPressed = "true"; + ele[0].innerHTML = ' Unhide'; + miniTranslate(ele[0]); + } + return true; +} +function updateDirectorVideoMute(UUID) { + var ele = document.querySelectorAll('[data-action-type="mute-video-guest"][data--u-u-i-d="' + UUID + '"]'); + if (ele[0]) { + ele[0].value = 1; + ele[0].classList.add("pressed"); + ele[0].ariaPressed = "true"; + ele[0].innerHTML = ' Video on'; + miniTranslate(ele[0]); + } + return true; +} + +function directVolume(ele) { + // NOT USED ANYMORE + log("volume"); + var msg = {}; + msg.scene = true; + msg.action = "volume"; + msg.target = ele.dataset.sid; // i want to focus on the STREAM ID, not the UUID... + msg.value = ele.value; + + //for (var uuid in session.pcs){ + // if (session.pcs[uuid].stats.info && ("version" in session.pcs[uuid].stats.info) && (session.pcs[uuid].stats.info.version < 17.2)){ + // msg.request = "sendroom"; + // session.sendMsg(msg); + // return; + // } + //} + + for (var uuid in session.pcs) { + if (session.pcs[uuid].scene !== false) { + // send to all scenes (but scene = 0) + session.sendMessage(msg, uuid); + } + } + + syncDirectorState(ele); + return msg.value; +} + +function applyMuteState(UUID) { + // this is the mute state of PLAYBACK audio; not the microphone or outbound. + if (!(UUID in session.rpcs)) { + return "UUID not found"; + } + + var muteOutcome = session.rpcs[UUID].mutedState || session.rpcs[UUID].mutedStateMixer || session.rpcs[UUID].mutedStateScene || session.speakerMuted || session.rpcs[UUID].bandwidthMuted; + + if (session.pauseInvisible) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.isInvisible) { + muteOutcome = true; + } + } + + if (!muteOutcome && session.noaudio !== false) { + if (session.noaudio === true) { + muteOutcome = true; + } else if (session.noaudio.length) { + if (("streamID" in session.rpcs[UUID]) && session.rpcs[UUID].streamID && !session.noaudio.includes(session.rpcs[UUID].streamID)) { + muteOutcome = true; + } + } else { + muteOutcome = true; + } + } else if (!muteOutcome && session.excludeaudio) { + if (("streamID" in session.rpcs[UUID]) && session.rpcs[UUID].streamID && session.excludeaudio.includes(session.rpcs[UUID].streamID)) { + muteOutcome = true; + } + } + + if (session.rpcs[UUID].videoElement) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.usermuted === 1) { + return "usermuted 1"; + } + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.usermuted === 2) { + return "usermuted 2"; + } + session.rpcs[UUID].videoElement.muted = muteOutcome; + } + + // session.scene + return muteOutcome; +} + +function checkMuteState(UUID) { + // this is the mute state of PLAYBACK audio; not the microphone or outbound. + if (!(UUID in session.rpcs)) { + return false; + } + if (session.pauseInvisible) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.isInvisible) { + return true; + } + } + return session.rpcs[UUID].mutedState || session.rpcs[UUID].mutedStateMixer || session.rpcs[UUID].mutedStateScene || session.speakerMuted || session.rpcs[UUID].bandwidthMuted; +} + +var volumeLUT = [0, 1, 2, 2.4, 2.7, 3, 3.4, 3.7, 4, 4.5, 4.8, 5, 5.6, 6, 6.4, 6.8, 7, 7.7, 8, 8.6, 9, 9.5, 10, 10.4, 10.9, 11, 12, 12.5, 13, 13.6, 14, 14.7, 15, 15.6, 16, 17, 17.7, 18, 19, 19.7, 20, 21, 21.8, 22, 23, 24, 24.7, 25, 26, 27, 28, 28.5, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 48, 49, 51, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 67, 68, 70, 72, 74, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 100, 102, 104, 107, 109, 112, 114, 117, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 156, 159, 163, 166, 170, 174, 177, 181, 185, 189, 193, 198, 202, 206, 211, 215, 220, 225, 230, 234, 240, 245, 250, 255, 261, 266, 272, 278, 284, 290, 296, 302, 308, 315, 322, 328, 335, 342, 350, 357, 364, 372, 380, 388, 396, 404, 413, 421, 430, 439, 448, 458, 467, 477, 487, 497, 507, 518, 528, 539, 551, 562, 574, 586, 598, 610, 623, 635, 649, 662, 676, 690, 704, 718, 733, 748, 764, 779, 795, 812, 828]; + +function initAudioButtons(audioGain, UUID) { + if (audioGain === 0) { + var ele = document.querySelector('[data-action-type="mute-guest"][data--u-u-i-d="' + UUID + '"]'); + if (ele) { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + miniTranslate(ele.children[1], "unmute"); + session.rpcs[UUID].directorMutedState = 1; + } + pokeIframeAPI("director-mute-state", true, UUID); + } else { + var ele = document.querySelector('[data-action-type="volume"][data--u-u-i-d="' + UUID + '"]'); + if (ele) { + if (audioGain == 100) { + ele.value = audioGain; + } else { + audioGain = parseInt(audioGain) || 0; + ele.value = 200; + for (var i = 1; i <= 200; i++) { + if (volumeLUT[i] >= audioGain) { + ele.value = i; + break; + } + } + } + session.rpcs[UUID].directorVolumeState = audioGain; + remoteVolumeUI(ele); + } + } +} +function remoteVolumeUI(ele) { + var value = ele.value; + value = volumeLUT[parseInt(value)]; + + ele.nextElementSibling.innerHTML = value + "%"; + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + remoteVolume(ele); + } + //setVolumeColor(ele); + return value; +} + +function remoteVolume(ele) { + // A directing room only is controlled by the Director, with the exception of MUTE. + log("volume: " + session.rpcs[ele.dataset.UUID].directorMutedState); + var msg = {}; + var muted = session.rpcs[ele.dataset.UUID].directorMutedState; + + var value = ele.value; + value = volumeLUT[parseInt(value)]; + + //log(ele); + if (muted == true) { + // 1 is a string, not an int, so == and not ===. this happens in a few places :/ + session.rpcs[ele.dataset.UUID].directorVolumeState = value; + } else { + session.rpcs[ele.dataset.UUID].directorVolumeState = value; + msg.volume = value; + msg.UUID = ele.dataset.UUID; + session.sendRequest(msg, ele.dataset.UUID); + } + + //const minLog = Math.log10(0.01); // Log10 of minimum gain + // const maxLog = Math.log10(10); // Log10 of maximum gain + // const normalizedValue = (msg.volume / 75) * (maxLog - minLog) + minLog; + // const gainValue = Math.pow(10, normalizedValue); + //console.log(gainValue*100); + + pokeIframeAPI("director-volume-state", value, ele.dataset.UUID); + + syncDirectorState(ele); + return value; +} + +/* function setVolumeColor(ele){ + var vol1 = 200-parseInt(ele.value); + if (vol1<0){vol1=0}; + ele.style.backgroundColor = "hsl("+vol1+", 100%, 50%)"; +} */ + +function clearDirectorSettings() { + // make sure to wipe the director's room settings if creating a new room. + //console.warn("Clearing"); + removeStorage("directorCustomize"); + removeStorage("directorWebsiteShare"); +} + +function saveDirectorSettings() { + //console.warn("Saving"); + var settings = {}; + + if (getById("customizeLinks").classList.contains("hidden")) { + settings.customizeLinks = true; + } + + var customizeLinks1 = getById("customizeLinks1").querySelectorAll("input"); + settings.customizeLinks1 = {}; + for (var i = 0; i < customizeLinks1.length; i++) { + settings.customizeLinks1[customizeLinks1[i].dataset.param] = customizeLinks1[i].checked; + } + + var customizeLinks3 = getById("customizeLinks3").querySelectorAll("input"); + settings.customizeLinks3 = {}; + for (var i = 0; i < customizeLinks3.length; i++) { + settings.customizeLinks3[customizeLinks3[i].dataset.param] = customizeLinks3[i].checked; + } + + var directorLinks1 = getById("directorLinks1").querySelectorAll("input"); + settings.directorLinks1 = {}; + for (var i = 0; i < directorLinks1.length; i++) { + settings.directorLinks1[directorLinks1[i].dataset.param] = directorLinks1[i].checked; + } + + var directorLinks2 = getById("directorLinks2").querySelectorAll("input"); + settings.directorLinks2 = {}; + for (var i = 0; i < directorLinks2.length; i++) { + settings.directorLinks2[directorLinks2[i].dataset.param] = directorLinks2[i].checked; + } + setStorage("directorCustomize", settings); +} + +function loadDirectorSettings() { + //console.warn("LOAD DIRECTOR SETTING"); + var settings = getStorage("directorCustomize"); + if (!settings || typeof settings !== "object") { + return; + } + + if (settings.customizeLinks && !session.cleanDirector && !session.cleanOutput) { + try { + hideDirectorinvites(getById("directorLinksButton"), false); + } catch (e) { + errorlog(e); + } + } + + if (settings.customizeLinks1) { + var customizeLinks1 = getById("customizeLinks1"); + Object.keys(settings.customizeLinks1).forEach((key, index) => { + try { + if (settings.customizeLinks1[key]) { + customizeLinks1.querySelector('[data-param="' + key + '"]').checked = settings.customizeLinks1[key]; + customizeLinks1.querySelector('[data-param="' + key + '"]').onchange(); + } + } catch (e) { + errorlog(e); + } + }); + } + + if (settings.customizeLinks3) { + var customizeLinks3 = getById("customizeLinks3"); + Object.keys(settings.customizeLinks3).forEach((key, index) => { + try { + if (settings.customizeLinks3[key]) { + customizeLinks3.querySelector('[data-param="' + key + '"]').checked = settings.customizeLinks3[key]; + customizeLinks3.querySelector('[data-param="' + key + '"]').onchange(); + } + } catch (e) { + errorlog(e); + } + }); + } + + if (settings.directorLinks1) { + var directorLinks1 = getById("directorLinks1"); + Object.keys(settings.directorLinks1).forEach((key, index) => { + try { + if (key in settings.directorLinks1) { + directorLinks1.querySelector('[data-param="' + key + '"]').checked = settings.directorLinks1[key]; + directorLinks1.querySelector('[data-param="' + key + '"]').onchange(); + } + } catch (e) { + errorlog("key :" + key); + errorlog(e); + } + }); + } + + if (settings.directorLinks2) { + var directorLinks2 = getById("directorLinks2"); + Object.keys(settings.directorLinks2).forEach((key, index) => { + try { + if (key in settings.directorLinks2) { + directorLinks2.querySelector('[data-param="' + key + '"]').checked = settings.directorLinks2[key]; + directorLinks2.querySelector('[data-param="' + key + '"]').onchange(); + } + } catch (e) { + errorlog("key :" + key); + errorlog(e); + } + }); + } +} + +function sendChat(chatmessage = "hi", UUID = false, overlay = false) { + // A directing room only is controlled by the Director, with the exception of MUTE. + log("Chat message"); + var msg = {}; + msg.chat = chatmessage; + msg.overlay = overlay; + session.sendPeers(msg, UUID); + return true; +} + +// ===================== +// TIPPING FUNCTIONALITY +// ===================== + +// Initialize default tip settings +if (typeof session.receiveTips === 'undefined') session.receiveTips = false; +if (typeof session.tipId === 'undefined') session.tipId = null; +if (typeof session.tipsId === 'undefined') session.tipsId = null; // Overlay token for SSE +if (typeof session.tipServer === 'undefined') session.tipServer = "https://ninjabacker.com"; +if (typeof session.tipAmounts === 'undefined') session.tipAmounts = [5, 10, 25, 50, 100]; +if (typeof session.tipCurrency === 'undefined') session.tipCurrency = "USD"; +if (typeof session.tipEventSource === 'undefined') session.tipEventSource = null; +if (typeof session.tipStripe === 'undefined') session.tipStripe = null; + +// Cache for performer validation results +var tipPerformerCache = {}; + +// Validate that a performer has completed Stripe setup and can receive tips +async function validateTipPerformer(tipId, tipServer) { + if (!tipId) return false; + + tipServer = tipServer || session.tipServer || "https://ninjabacker.com"; + var cacheKey = tipServer + "/" + tipId; + + // Return cached result if available + if (cacheKey in tipPerformerCache) { + return tipPerformerCache[cacheKey]; + } + + try { + var response = await fetch(tipServer + "/v1/performer/" + tipId); + var isValid = response.ok; // 200 = performer exists and has charges_enabled + tipPerformerCache[cacheKey] = isValid; + return isValid; + } catch(e) { + // Network error - assume not valid, don't cache to allow retry + return false; + } +} + +// Add tip icon overlay to video container (two-way opt-in system) +async function addTipIconToVideo(UUID) { + if (!session.showTips || session.cleanOutput) return; + + var peer = session.rpcs[UUID] || session.pcs[UUID]; + if (!peer || !peer.acceptsTips) return; + + // Build tip page URL for QR code - only if performer has a registered tipId + var tipServer = peer.tipServer || session.tipServer || "https://ninjabacker.com"; + var tipId = peer.tipId; // Must be explicitly set - don't fall back to UUID + + // If no tipId, performer hasn't set up tipping properly - don't show icon + if (!tipId) return; + + // Validate performer has completed Stripe setup before showing tip icon + var isValidPerformer = await validateTipPerformer(tipId, tipServer); + if (!isValidPerformer) return; + + // Find video container - try different naming patterns + var videoContainer = document.getElementById("videoContainer_" + UUID); + var videoElement = document.getElementById("videosource_" + UUID); + if (!videoContainer) { + // Try finding the video element's parent + if (videoElement && videoElement.parentElement) { + videoContainer = videoElement.parentElement; + } + } + + // If container doesn't exist yet, try again later + if (!videoContainer) { + setTimeout(function() { + addTipIconToVideo(UUID); + }, 1000); + return; + } + + // Don't add duplicate icons + if (videoContainer.querySelector(".tipIconOverlay")) return; + + // Determine if we're in OBS or a scene/view link (not a guest/publisher) + var isOBS = !!window.obsstudio; + var isSceneOrView = session.scene !== false || session.view; // scene link or view parameter + + // QR code shown only if: + // 1. User explicitly set &tipqrsize, OR + // 2. In OBS studio, OR + // 3. It's a scene/view link (not a guest publisher page) + var showQRCode = false; + if (session.tipQRSize && session.tipQRSize !== 150) { // User explicitly set size + showQRCode = true; + } else if (isOBS || isSceneOrView) { + showQRCode = !session.noTipQR; // Can be disabled with ¬ipqr + } + + // Check video display size - only show QR if video is large enough (min 640x360) + if (showQRCode) { + var videoWidth = videoContainer.offsetWidth || (videoElement ? videoElement.offsetWidth : 0); + var videoHeight = videoContainer.offsetHeight || (videoElement ? videoElement.offsetHeight : 0); + var minWidthForQR = 640; + var minHeightForQR = 360; + if (videoWidth < minWidthForQR || videoHeight < minHeightForQR) { + showQRCode = false; + } + } + + var tipPageUrl = tipServer + "/" + (tipId || ""); + var qrSize = Math.max(session.tipQRSize || 150, 100); // Minimum 100px for scanability + + // Create container for heart + label + QR + var tipOverlay = document.createElement("div"); + tipOverlay.className = "tipIconOverlay"; + if (!showQRCode) { + tipOverlay.classList.add("noQR"); + } + if (isOBS) { + tipOverlay.classList.add("obsMode"); + } + tipOverlay.title = "Send a tip"; + tipOverlay.dataset.UUID = UUID; + + // Heart icon with dollar sign + var heartIcon = document.createElement("div"); + heartIcon.className = "tipHeart"; + heartIcon.innerHTML = '$'; + tipOverlay.appendChild(heartIcon); + + // "Send a Tip" label + var tipLabel = document.createElement("div"); + tipLabel.className = "tipLabel"; + tipLabel.textContent = "Send a Tip"; + tipOverlay.appendChild(tipLabel); + + // Only add QR code if appropriate + if (showQRCode) { + var qrContainer = document.createElement("div"); + qrContainer.className = "tipQR"; + qrContainer.style.width = qrSize + "px"; + qrContainer.style.height = qrSize + "px"; + + // Generate styled QR code + generateStyledTipQR(qrContainer, tipPageUrl, qrSize); + tipOverlay.appendChild(qrContainer); + } + + // Click handler + tipOverlay.onclick = function(e) { + e.stopPropagation(); + if (typeof openTipModal === 'function') { + openTipModal(this.dataset.UUID); + } + }; + + videoContainer.appendChild(tipOverlay); + + // Start animation based on mode + if (showQRCode && isOBS) { + // OBS mode: QR code with occasional "Send a Tip" text + startTipQRAnimation(tipOverlay, true); + } else if (showQRCode) { + // Scene/view mode: QR code with occasional "Send a Tip" text + startTipQRAnimation(tipOverlay, false); + } else { + // Guest mode: Heart with occasional "Send a Tip" label + startTipLabelAnimation(tipOverlay); + } +} + +// Animate between heart/label and QR code +// OBS mode: Show QR most of the time, occasionally show "Send a Tip" label +// Scene mode: Show QR periodically (every 45 seconds for 8 seconds) +function startTipQRAnimation(tipOverlay, isOBS) { + if (isOBS) { + // OBS: Start with QR showing, periodically show "Send a Tip" label + tipOverlay.classList.add("showQR"); + + function showLabel() { + tipOverlay.classList.remove("showQR"); + tipOverlay.classList.add("showLabel"); + // Show label for 6 seconds + setTimeout(function() { + if (tipOverlay && tipOverlay.parentElement) { + tipOverlay.classList.remove("showLabel"); + tipOverlay.classList.add("showQR"); + } + }, 6000); + } + + // Show label every 60 seconds + tipOverlay.qrInterval = setInterval(function() { + if (tipOverlay && tipOverlay.parentElement) { + showLabel(); + } + }, 60000); + + // First label after 20 seconds + setTimeout(function() { + if (tipOverlay && tipOverlay.parentElement) { + showLabel(); + } + }, 20000); + } else { + // Scene/view mode: Show heart normally, QR periodically + function showQR() { + tipOverlay.classList.add("showQR"); + // Show QR for 8 seconds + setTimeout(function() { + if (tipOverlay && tipOverlay.parentElement) { + tipOverlay.classList.remove("showQR"); + } + }, 8000); + } + + // Show QR every 45 seconds + tipOverlay.qrInterval = setInterval(function() { + if (tipOverlay && tipOverlay.parentElement) { + showQR(); + } + }, 45000); + + // First QR after 15 seconds + setTimeout(function() { + if (tipOverlay && tipOverlay.parentElement) { + showQR(); + } + }, 15000); + } +} + +// Animate "Send a Tip" label for guest/publisher mode (no QR) +function startTipLabelAnimation(tipOverlay) { + function showLabel() { + tipOverlay.classList.add("showLabel"); + // Show label for 5 seconds + setTimeout(function() { + if (tipOverlay && tipOverlay.parentElement) { + tipOverlay.classList.remove("showLabel"); + } + }, 5000); + } + + // Show label every 45 seconds + tipOverlay.labelInterval = setInterval(function() { + if (tipOverlay && tipOverlay.parentElement) { + showLabel(); + } + }, 45000); + + // First label after 10 seconds + setTimeout(function() { + if (tipOverlay && tipOverlay.parentElement) { + showLabel(); + } + }, 10000); +} + +// Clean up tip icon animation when video removed +function removeTipIconFromVideo(UUID) { + var tipOverlay = document.querySelector('.tipIconOverlay[data-uuid="' + UUID + '"]'); + if (tipOverlay) { + if (tipOverlay.qrInterval) { + clearInterval(tipOverlay.qrInterval); + } + if (tipOverlay.labelInterval) { + clearInterval(tipOverlay.labelInterval); + } + tipOverlay.remove(); + } +} + +// Generate QR code using built-in thirdparty/qrcode.min.js library +var tipQRPendingContainers = []; // Queue for containers waiting for library + +function generateStyledTipQR(container, url, size) { + // Use existing QRCode library from thirdparty/qrcode.min.js + if (window.QRCode) { + createTipQR(container, url, size); + } else { + // Queue this container and load library + tipQRPendingContainers.push({ container: container, url: url, size: size }); + loadQR(function() { + // Process all pending containers + tipQRPendingContainers.forEach(function(item) { + createTipQR(item.container, item.url, item.size); + }); + tipQRPendingContainers = []; + }); + } +} + +function createTipQR(container, url, size) { + try { + // Create inner div for QR code + var qrDiv = document.createElement("div"); + qrDiv.style.cssText = "width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#fff;border-radius:4px;"; + container.appendChild(qrDiv); + + var qrcode = new QRCode(qrDiv, { + width: size - 8, + height: size - 8, + colorDark: "#e53935", // Red color to match heart theme + colorLight: "#FFFFFF", + correctLevel: QRCode.CorrectLevel.H // High error correction for video compression + }); + qrcode.makeCode(url); + + // Remove default title + qrDiv.title = ""; + setTimeout(function() { + qrDiv.title = ""; + // Style the generated image + var imgs = qrDiv.getElementsByTagName("img"); + if (imgs.length) { + imgs[0].style.cursor = "pointer"; + imgs[0].style.margin = "auto"; + imgs[0].style.borderRadius = "4px"; + } + var canvas = qrDiv.getElementsByTagName("canvas"); + if (canvas.length) { + canvas[0].style.borderRadius = "4px"; + } + }, 100); + } catch (e) { + errorlog("QR code error:", e); + } +} + +// Show onboarding modal for first-time tip setup +function showTipOnboardingModal() { + // Check if already seen + if (getStorage("tipOnboardingSeen")) return; + + var tipServer = session.tipServer || "https://ninjabacker.com"; + + // If user already has tipsId set, they're fully configured - skip onboarding + if (session.tipsId) { + setStorage("tipOnboardingSeen", "true", 9999); + return; + } + + var step2Content = + '

    2. Enter Your Username

    ' + + '

    After registering, enter your username below:

    ' + + '
    ' + + '' + + '' + + '
    ' + + '

    Enter your registered username to enable tipping.

    '; + + var modalHTML = + '
    ' + + '
    ' + + '×' + + '

    Tipping Setup

    ' + + '

    To receive tips, follow these steps:

    ' + + + '

    1. Register Your Account

    ' + + '

    Create a username and connect your Stripe account:

    ' + + '' + + 'Register & Create Username' + + '' + + + step2Content + + + '

    3. Viewer Setup

    ' + + '

    Viewers need &showtips in their URL to see tip buttons.

    ' + + + '

    4. QR Code Feature

    ' + + '

    A scannable QR code will appear on your video periodically. Use &notipqr to disable, or &tipqrsize=200 to resize.

    ' + + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + + document.body.insertAdjacentHTML("beforeend", modalHTML); +} + +function closeTipOnboarding(permanent) { + if (permanent) { + setStorage("tipOnboardingSeen", "true", 9999); // Never show again + } else { + setStorage("tipOnboardingSeen", "true", 1); // Show again in 1 day + } + var modal = document.getElementById("tipOnboardingModal"); + var backdrop = document.getElementById("tipOnboardingBackdrop"); + if (modal) modal.remove(); + if (backdrop) backdrop.remove(); +} + +async function applyTipUsername() { + var input = document.getElementById("tipUsernameInput"); + if (!input) return; + + var username = input.value.trim().toLowerCase(); + if (!username) { + warnUser("Please enter a username"); + return; + } + + // Validate username format (alphanumeric, underscore, hyphen, 3-30 chars) + if (!/^[a-z0-9_-]{3,30}$/.test(username)) { + warnUser("Username must be 3-30 characters (letters, numbers, _ or -)"); + return; + } + + // Fetch performer info from API to get overlay token + var tipServer = session.tipServer || "https://ninjabacker.com"; + try { + var response = await fetch(tipServer + "/v1/performer/" + username); + if (!response.ok) { + warnUser("Username not found or not registered for tips"); + return; + } + var data = await response.json(); + if (!data.overlay_token) { + warnUser("Performer account not fully set up"); + return; + } + + // Build new URL with tipsid parameter (overlay token) + var url = new URL(window.location.href); + url.searchParams.delete("tip"); + url.searchParams.delete("tips"); + url.searchParams.delete("tipid"); + url.searchParams.set("tipsid", data.overlay_token); + + // Reload with new parameter + window.location.href = url.toString(); + } catch(e) { + errorlog("Failed to lookup performer:", e); + warnUser("Failed to verify username. Please try again."); + } +} + +// Get currency symbol helper +function getTipCurrencySymbol(currency) { + var symbols = { USD: "$", EUR: "\u20AC", GBP: "\u00A3", CAD: "C$", AUD: "A$", JPY: "\u00A5" }; + return symbols[currency] || currency + " "; +} + +// Show on-screen tip banner (performer only) +function showTipBanner(tipData) { + var currencySymbol = getTipCurrencySymbol(tipData.currency || "USD"); + var fromLabel = sanitizeLabel(tipData.fromLabel || tipData.from || "Anonymous"); + var amount = tipData.amount; + + // Create banner element + var banner = document.createElement("div"); + banner.className = "tipBanner"; + banner.innerHTML = currencySymbol + amount + " tip from " + fromLabel; + if (tipData.message) { + banner.innerHTML += '
    "' + sanitizeChat(tipData.message) + '"
    '; + } + + document.body.appendChild(banner); + + // Trigger animation + setTimeout(function() { + banner.classList.add("tipBannerShow"); + }, 10); + + // Remove after 5 seconds + setTimeout(function() { + banner.classList.remove("tipBannerShow"); + banner.classList.add("tipBannerHide"); + setTimeout(function() { + banner.remove(); + }, 500); // Wait for fade out animation + }, 5000); +} + +// Process incoming tip message +function processTipMessage(tipData, UUID) { + log("Tip received:", tipData); + + var currencySymbol = getTipCurrencySymbol(tipData.currency || "USD"); + var fromLabel = sanitizeLabel(tipData.fromLabel || tipData.from || "Anonymous"); + var message = tipData.message ? sanitizeChat(tipData.message) : ""; + + // Plain text version for notification + var notifyMsg = currencySymbol + tipData.amount + " tip from " + fromLabel; + if (message) { + notifyMsg += ': "' + message + '"'; + } + + var data = { + time: Date.now(), + type: "tip", + msg: notifyMsg, + label: "Tip" + }; + + messageList.push(data); + messageList = messageList.slice(-100); + + // Play notification sound + if (session.beepToNotify) { + playtone(); + showNotification("Tip received", notifyMsg); + } + + updateMessages(); + + // Show on-screen banner only for SSE-received tips (performer's own tips) + if (UUID === null) { + showTipBanner(tipData); + } + + // Chat notification (red dot) when chat is closed + if (session.chat == false) { + getById("chattoggle").className = "las la-comments toggleSize pulsate"; + getById("chatbutton").className = "float"; + + if (getById("chatNotification").value) { + getById("chatNotification").value = getById("chatNotification").value + 1; + } else { + getById("chatNotification").value = 1; + } + getById("chatNotification").classList.add("notification", "red"); + } + + // Broadcast to popout chat window + if (session.broadcastChannel !== false) { + session.broadcastChannel.postMessage(data); + } + + // Browser notification + if (Notification.permission === "granted") { + try { + new Notification("Tip Received!", { + body: notifyMsg, + icon: "./media/logo.png" + }); + } catch(e) {} + } + + // API callback + if (typeof pokeAPI === 'function') { + pokeAPI("tip", tipData); + } + + // Iframe postMessage + if (isIFrame && session.iframetarget) { + try { + parent.postMessage({ action: "tip", value: tipData }, session.iframetarget); + } catch(e) {} + } +} + +// Initialize SSE connection for tip notifications +function initTipNotifications() { + if (!session.receiveTips) return; + if (session.tipEventSource) return; // Already connected + + // Use tipsId (overlay token) for SSE subscription, fallback to streamID for legacy + var subscribeId = session.tipsId || session.streamID; + if (!subscribeId) return; + + var tipServer = session.tipServer || "https://ninjabacker.com"; + var sseURL = tipServer + "/v1/subscribe/" + subscribeId; + + try { + session.tipEventSource = new EventSource(sseURL); + + session.tipEventSource.onmessage = function(event) { + try { + var tipData = JSON.parse(event.data); + if (tipData.type === "tip") { + processTipMessage(tipData, null); + // Broadcast to room peers + broadcastTipReceived(tipData); + } + } catch(e) { + errorlog("Tip SSE parse error:", e); + } + }; + + session.tipEventSource.onerror = function(err) { + warnlog("Tip SSE connection error, will retry..."); + }; + + log("Tip notifications initialized for: " + subscribeId); + } catch(e) { + errorlog("Failed to initialize tip notifications:", e); + } +} + +// Fetch performer info (username) from tipsId token and set session.tipId +async function fetchPerformerFromToken() { + if (!session.tipsId) return; + if (session.tipId) return; // Already have username + + var tipServer = session.tipServer || "https://ninjabacker.com"; + try { + var response = await fetch(tipServer + "/v1/performer/" + session.tipsId); + if (response.ok) { + var data = await response.json(); + if (data.username) { + session.tipId = data.username; + log("Performer username from token: " + data.username); + } + } + } catch(e) { + errorlog("Failed to fetch performer from token:", e); + } +} + +// Close SSE connection +function closeTipNotifications() { + if (session.tipEventSource) { + session.tipEventSource.close(); + session.tipEventSource = null; + } +} + +// Broadcast tip received to all peers (so viewers see it too) +function broadcastTipReceived(tipData) { + var msg = { tip: tipData }; + session.sendPeers(msg); +} + +// Open tip modal for a peer +function openTipModal(UUID) { + var peer = session.rpcs[UUID] || session.pcs[UUID]; + if (!peer || !peer.acceptsTips) { + warnUser("This user does not accept tips"); + return; + } + + var peerLabel = sanitizeLabel(peer.tipId || peer.label || peer.streamID || "Performer"); + var amounts = peer.tipAmounts || session.tipAmounts || [5, 10, 25, 50, 100]; + var currency = peer.tipCurrency || session.tipCurrency || "USD"; + var currencySymbol = getTipCurrencySymbol(currency); + + var modalID = "tipModal_" + UUID; + var zindex = 32 + document.querySelectorAll(".promptModal").length + document.querySelectorAll(".alertModal").length; + + var amountButtons = amounts.map(function(amt) { + return ''; + }).join(''); + + var modalTemplate = + '
    ' + + '
    ' + + '' + + '
    ' + + '

    \uD83D\uDCB0 Send a tip to ' + peerLabel + '

    ' + + '
    ' + + '
    ' + amountButtons + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '
    ' + + 'Powered by' + + '' + + '
    ' + + '
    ' + + 'Selected: ' + currencySymbol + '0' + + '
    ' + + '' + + '' + + '
    ' + + '
    '; + + document.body.insertAdjacentHTML("beforeend", modalTemplate); + + // Initialize Stripe Elements + initTipStripeElements(UUID, peer); +} + +// Load Stripe.js dynamically if not already loaded +function loadStripeJS() { + return new Promise(function(resolve, reject) { + if (typeof Stripe !== 'undefined') { + resolve(); + return; + } + var script = document.createElement('script'); + script.src = 'https://js.stripe.com/v3/'; + script.onload = resolve; + script.onerror = function() { reject(new Error('Failed to load Stripe.js')); }; + document.head.appendChild(script); + }); +} + +// Initialize Stripe Elements for tip modal +async function initTipStripeElements(UUID, peer) { + peer = peer || {}; + var tipServer = peer.tipServer || session.tipServer || "https://ninjabacker.com"; + + try { + // Load Stripe.js if needed + await loadStripeJS(); + + // Get performer-specific Stripe publishable key (supports test/live mode per account) + var performerId = peer.tipId || peer.tipsId || peer.streamID; + var stripeKey = null; + + if (performerId) { + try { + var perfResponse = await fetch(tipServer + "/v1/performer/" + performerId); + if (perfResponse.ok) { + var perfData = await perfResponse.json(); + stripeKey = perfData.stripePublishableKey; + } + } catch(e) { + // Fall back to config endpoint + } + } + + // Fallback to global config if performer-specific key not available + if (!stripeKey) { + var response = await fetch(tipServer + "/v1/config"); + var config = await response.json(); + stripeKey = config.stripePublishableKey; + } + + if (!stripeKey) { + document.getElementById("tipError_" + UUID).textContent = "Tipping not configured"; + document.getElementById("tipError_" + UUID).classList.remove("hidden"); + return; + } + + // Initialize Stripe with performer-specific key + var stripe = Stripe(stripeKey); + var elements = stripe.elements(); + + var cardElement = elements.create('card', { + hidePostalCode: true, + style: { + base: { + color: '#ffffff', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + fontSize: '16px', + '::placeholder': { color: '#a0a0a0' } + } + } + }); + + cardElement.mount('#tipCardElement_' + UUID); + + // Store for later use + if (!window.tipStripeElements) window.tipStripeElements = {}; + window.tipStripeElements[UUID] = { stripe: stripe, cardElement: cardElement }; + + cardElement.on('change', function(event) { + var amount = window.tipStripeElements[UUID].selectedAmount || 0; + document.getElementById("tipConfirmBtn_" + UUID).disabled = !event.complete || amount <= 0; + }); + + } catch(e) { + errorlog("Failed to init Stripe:", e); + document.getElementById("tipError_" + UUID).textContent = "Failed to load payment form"; + document.getElementById("tipError_" + UUID).classList.remove("hidden"); + } +} + +// Select a predefined tip amount +function selectTipAmount(btn, UUID) { + document.querySelectorAll('#tipModal_' + UUID + ' .tipAmountBtn').forEach(function(b) { + b.classList.remove('selected'); + }); + btn.classList.add('selected'); + + var amount = parseFloat(btn.dataset.amount); + var peer = session.rpcs[UUID] || session.pcs[UUID]; + var currency = peer?.tipCurrency || session.tipCurrency || "USD"; + var currencySymbol = getTipCurrencySymbol(currency); + + document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + amount.toFixed(2); + document.getElementById('tipCustomInput_' + UUID).value = ""; + + if (window.tipStripeElements && window.tipStripeElements[UUID]) { + window.tipStripeElements[UUID].selectedAmount = amount; + } + + // Enable button if card is ready + checkTipButtonState(UUID); +} + +// Handle custom amount input +function customTipAmount(UUID, value) { + var amount = parseFloat(value); + var peer = session.rpcs[UUID] || session.pcs[UUID]; + var currency = peer?.tipCurrency || session.tipCurrency || "USD"; + var currencySymbol = getTipCurrencySymbol(currency); + + // Deselect preset buttons + document.querySelectorAll('#tipModal_' + UUID + ' .tipAmountBtn').forEach(function(b) { + b.classList.remove('selected'); + }); + + if (amount > 0) { + document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + amount.toFixed(2); + if (window.tipStripeElements && window.tipStripeElements[UUID]) { + window.tipStripeElements[UUID].selectedAmount = amount; + } + } else { + document.getElementById('tipSelectedAmount_' + UUID).textContent = currencySymbol + "0"; + if (window.tipStripeElements && window.tipStripeElements[UUID]) { + window.tipStripeElements[UUID].selectedAmount = 0; + } + } + + checkTipButtonState(UUID); +} + +// Check if tip button should be enabled +function checkTipButtonState(UUID) { + if (!window.tipStripeElements || !window.tipStripeElements[UUID]) return; + var amount = window.tipStripeElements[UUID].selectedAmount || 0; + // Button state is also controlled by Stripe card element change event +} + +// Confirm and process tip payment +async function confirmTip(UUID) { + if (!window.tipStripeElements || !window.tipStripeElements[UUID]) return; + + var stripeData = window.tipStripeElements[UUID]; + var peer = session.rpcs[UUID] || session.pcs[UUID]; + + if (!peer || !stripeData.selectedAmount || stripeData.selectedAmount <= 0) { + return; + } + + var tipServer = peer.tipServer || session.tipServer || "https://ninjabacker.com"; + var amount = stripeData.selectedAmount; + var currency = peer.tipCurrency || session.tipCurrency || "USD"; + var tipperName = document.getElementById('tipName_' + UUID)?.value || "Anonymous"; + var message = document.getElementById('tipMessageInput_' + UUID)?.value || ""; + var performerUsername = peer.tipId || peer.streamID; + + var submitBtn = document.getElementById('tipConfirmBtn_' + UUID); + var errorEl = document.getElementById('tipError_' + UUID); + + submitBtn.disabled = true; + submitBtn.textContent = "Processing..."; + errorEl.classList.add('hidden'); + + try { + // Create PaymentIntent + var intentResponse = await fetch(tipServer + "/v1/tip/intent", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + amount: amount, + currency: currency, + performerUsername: performerUsername, + tipperName: tipperName, + message: message + }) + }); + + if (!intentResponse.ok) { + var errData = await intentResponse.json(); + throw new Error(errData.error || 'Failed to create payment'); + } + + var intentData = await intentResponse.json(); + + // Confirm payment with Stripe + var result = await stripeData.stripe.confirmCardPayment(intentData.clientSecret, { + payment_method: { card: stripeData.cardElement } + }); + + if (result.error) { + throw new Error(result.error.message); + } + + if (result.paymentIntent.status === 'succeeded') { + // Confirm with backend + await fetch(tipServer + "/v1/tip/confirm", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tipId: intentData.tipId, + paymentIntentId: result.paymentIntent.id + }) + }); + + // Success! + closeTipModal('tipModal_' + UUID, UUID); + warnUser("Tip sent successfully! Thank you!", 3000); + } + + } catch(e) { + errorlog("Tip payment error:", e); + errorEl.textContent = e.message; + errorEl.classList.remove('hidden'); + submitBtn.disabled = false; + submitBtn.textContent = "Send Tip"; + } +} + +// Close tip modal +function closeTipModal(modalID, UUID) { + var modal = document.getElementById(modalID); + if (modal) modal.remove(); + + // Cleanup Stripe elements + if (window.tipStripeElements && window.tipStripeElements[UUID]) { + if (window.tipStripeElements[UUID].cardElement) { + window.tipStripeElements[UUID].cardElement.destroy(); + } + delete window.tipStripeElements[UUID]; + } +} + +// ===================== +// END TIPPING FUNCTIONALITY +// ===================== + +var activatedStream = false; + +async function publishScreen() { + if (activatedStream == true) { + return; + } + activatedStream = true; + setTimeout(function () { + activatedStream = false; + }, 1000); + + formSubmitting = false; + + var quality = 0; + + if (document.getElementById("webcamquality2")) { + quality = parseInt(document.getElementById("webcamquality2").elements.namedItem("resolution2").value) || 0; + } + + session.quality_ss = quality; + + if (session.quality !== false) { + quality = session.quality; // override the user's setting + } + + if (session.screensharequality !== false) { + quality = session.screensharequality; + } + + var video = {}; + + if (quality == -1) { + // unlocked capture resolution + } else if (quality == -2) { + video.width = { + ideal: 3840 + }; + video.height = { + ideal: 2160 + }; + } else if (quality == -3) { + video.width = { + ideal: 2560 + }; + video.height = { + ideal: 1440 + }; + } else if (quality == 0) { + video.width = { + ideal: 1920 + }; + video.height = { + ideal: 1080 + }; + } else if (quality == 1) { + video.width = { + ideal: 1280 + }; + video.height = { + ideal: 720 + }; + } else if (quality == 2) { + video.width = { + ideal: 640 + }; + video.height = { + ideal: 360 + }; + } else if (quality >= 3) { + // lowest + video.width = { + ideal: 320 + }; + video.height = { + ideal: 180 + }; + } else { + video.width = { + min: 640 + }; + video.height = { + min: 360 + }; + } + + if (session.width) { + video.width = { + ideal: session.width + }; + } + if (session.height) { + video.height = { + ideal: session.height + }; + } + + var constraints = { + audio: { + echoCancellation: false, + autoGainControl: false, + noiseSuppression: false + }, + video: video + }; + + if (session.noiseSuppression === true) { + constraints.audio.noiseSuppression = true; // the defaults for screen publishing should be off. + } + if (session.autoGainControl === true) { + constraints.audio.autoGainControl = true; // the defaults for screen publishing should be off. + } + if (session.echoCancellation === true) { + constraints.audio.echoCancellation = true; // the defaults for screen publishing should be off. + } + if (session.voiceIsolation === true) { + constraint.audio.voiceIsolation = true; + } + try { + let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + if (supportedConstraints.cursor) { + if (session.screensharecursor) { + constraints.video.cursor = ["always", "motion"]; + } else { + constraints.video.cursor = "never"; + } + } + if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) { + constraints.audio.suppressLocalAudioPlayback = true; + } + // + if (session.preferCurrentTab) { + constraints.preferCurrentTab = true; + } + if (session.selfBrowserSurface) { + constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include + } + if (session.surfaceSwitching) { + constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include + } + if (session.systemAudio) { + constraints.systemAudio = session.systemAudio; // exclude or include + } + if (session.displaySurface && supportedConstraints.displaySurface) { + constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser + } + } catch (e) { + warnlog("navigator.mediaDevices.getSupportedConstraints() not supported"); + } + + var overrideFramerate = false; + if (session.screensharefps !== false) { + constraints.video.frameRate = { + ideal: session.screensharefps, + max: session.screensharefps + }; + } else if (session.frameRate !== false && session.maxframeRate != false) { + overrideFramerate = session.frameRate; + constraints.video.frameRate = { + ideal: session.maxframeRate, + max: session.maxframeRate + }; + } else if (session.frameRate !== false) { + constraints.video.frameRate = session.frameRate; + } else if (session.maxframeRate != false) { + constraints.video.frameRate = { + ideal: session.maxframeRate, + max: session.maxframeRate + }; + } else { + constraints.video.frameRate = { + ideal: 60 + }; + } + + var outputSelect = getById("outputSourceScreenshare"); + + try { + session.sink = outputSelect.options[outputSelect.selectedIndex].value; // will probably fail on Safari. + log("Session Sink: " + session.sink); + saveSettings(); + } catch (e) { + warnlog(e); + } + + session.audioDevice = selectedScreenShareAudioDevices; + + return await publishScreen2(constraints, selectedScreenShareAudioDevices, true, overrideFramerate) + .then(res => { + if (res == false) { + return; + } // no screen selected + log("streamID is: " + session.streamID); + if (session.transcript) { + setTimeout(function () { + setupClosedCaptions(); + }, 1000); + } + + if (!session.cleanOutput && !session.cleanViewer) { + getById("mutebutton").classList.remove("hidden"); + getById("mutespeakerbutton").classList.remove("hidden"); + //getById("mutespeakerbutton").className="float"; + getById("chatbutton").className = "float"; + getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. + getById("mutevideobutton").className = "float"; + getById("hangupbutton").className = "float"; + if (session.showSettings) { + getById("settingsbutton").className = "float"; + } + if (session.raisehands) { + getById("raisehandbutton").className = "float"; + } + if (session.pptControls) { + getById("pptbackbutton").classList.remove("hidden"); + getById("pptnextbutton").classList.remove("hidden"); + } + if (session.recordLocal !== false) { + getById("recordLocalbutton").classList.remove("hidden"); + } + if (session.screensharebutton) { + getById("screensharebutton").className = "float"; + } + getById("controlButtons").classList.remove("hidden"); + // getById("legal").classList.remove("hidden"); + //getById("helpbutton").style.display = "inherit"; + //getById("reportbutton").style.display = ""; + } else if (session.cleanish && session.recordLocal !== false) { + getById("recordLocalbutton").classList.remove("hidden"); + getById("mutebutton").classList.add("hidden"); + getById("mutespeakerbutton").classList.add("hidden"); + getById("chatbutton").classList.add("hidden"); + getById("mutevideobutton").classList.add("hidden"); + getById("hangupbutton").classList.add("hidden"); + getById("hangupbutton2").classList.add("hidden"); + getById("controlButtons").classList.remove("hidden"); + getById("settingsbutton").classList.add("hidden"); + getById("screenshare2button").classList.add("hidden"); + getById("screensharebutton").classList.add("hidden"); + getById("screenshare3button").classList.add("hidden"); + getById("queuebutton").classList.add("hidden"); + } else { + getById("controlButtons").classList.add("hidden"); + } + + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("hidden"); + getById("controlButtons").classList.remove("hidden"); + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("hidden"); + } + + if (session.screensharebutton === true) { + getById("controlButtons").classList.remove("hidden"); + getById("screensharebutton").className = "float"; + } + + if (session.hangupbutton === true) { + getById("controlButtons").classList.remove("hidden"); + getById("hangupbutton").className = "float"; + } + + getById("head1").className = "hidden"; + getById("head2").className = "hidden"; + + return res; + }) + .catch(() => { }); +} + +function getWidth() { + return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth, document.documentElement.clientWidth); +} + +function getHeight() { + return Math.max(document.documentElement.clientHeight); +} + +function updateForceRotate(skipLastBit = false) { + var capabilities = { facingMode: "unknown" }; + var FirefoxSucks = false; + + if (session.orientation) { + try { + var track = false; + if (session.streamSrc) { + var tracks = session.streamSrc.getVideoTracks(); + if (tracks.length) { + track = tracks[0]; + } + } + if (!track) { + return; + } + + const settings = track.getSettings(); + session.currentCameraConstraints = settings; + + if (screen && screen.orientation && screen.orientation.type) { + if (!screen.orientation.type.includes("portrait")) { + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + } else if (!window.matchMedia("(orientation: portrait)").matches) { + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + + session.forceRotate = 0; + + if (track.getCapabilities) { + capabilities = track.getCapabilities(); // firefox sucks? + + if ("width" in settings) { + if ("height" in settings) { + if (settings.width < settings.height) { + if (session.orientation == "landscape") { + if (capabilities) { + if (capabilities.facingMode == "environment") { + session.forceRotate = 270; + } else { + session.forceRotate = 90; + } + } + } else { + session.forceRotate = 0; + } + } else if (settings.width > settings.height) { + if (session.orientation == "portrait") { + if (capabilities) { + if (capabilities.facingMode == "environment") { + session.forceRotate = 90; + } else { + session.forceRotate = 270; + } + } + } else { + session.forceRotate = 0; + } + } else { + session.forceRotate = 0; + } + } else { + return; + } + } + } else if (Firefox && session.mobile) { + // firefox sucks... + try { + var vs1 = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3"); + if (vs1) { + vs1 = vs1.options[vs1.selectedIndex].textContent; + if (vs1.includes(" back")) { + capabilities = { facingMode: "environment" }; + FirefoxSucks = 2; + } else if (vs1.includes(" front")) { + capabilities = { facingMode: "user" }; + FirefoxSucks = 1; + } + } + getById("videosource").style.transform = ""; + if (session.orientation == "landscape") { + if (FirefoxSucks === 1) { + session.forceRotate = 90; + // video needs to be flipped + getById("videosource").style.transform = "rotate(90deg)"; + } else if (FirefoxSucks === 2) { + getById("videosource").style.transform = "rotate(270deg)"; + session.forceRotate = 270; + } + } + } catch (e) { + errorlog(e); + } + } else { + return; + } + + var msg = {}; + msg.rotate_video = 0; + if (session.forceRotate !== false) { + if (session.rotate) { + msg.rotate_video = session.forceRotate + parseInt(session.rotate); + } else { + msg.rotate_video = session.forceRotate; + } + } else { + msg.rotate_video = parseInt(session.rotate) || 0; + } + + if (msg.rotate_video && msg.rotate_video >= 360) { + msg.rotate_video -= 360; + } + + for (var UUID in session.pcs) { + try { + if (session.pcs[UUID].rotation != msg.rotate_video) { + // 0 == false will skip I think + session.pcs[UUID].rotation = msg.rotate_video; + session.sendMessage(msg, UUID); + //log("sending updated rotation info"); + } + } catch (e) { + errorlog(e); + if (session.pcs[UUID].startTime + 100000 < Date.now()) { + warnlog("RTC Connection seems to be dead or not yet open? 8"); + } else { + log("RTC Connection seems to be dead or not yet open? 8"); + } + } + } + } catch (e) { + errorlog(e); + } + + if (!(Firefox && session.mobile)) { + updateForceRotatedCSS(); + } + } else if (Firefox && session.mobile) { + try { + var vs1 = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3"); + if (vs1) { + vs1 = vs1.options[vs1.selectedIndex].textContent; + if (vs1.includes(" back")) { + FirefoxSucks = 2; + } else if (vs1.includes(" front")) { + FirefoxSucks = 1; + } + } + + if (screen && screen.orientation && screen.orientation.type) { + if (screen.orientation.type.includes("portrait")) { + session.forceRotate = 0; + } else if (screen.orientation.type.includes("landscape")) { + if (FirefoxSucks === 1) { + session.forceRotate = 90; + } else if (FirefoxSucks === 2) { + session.forceRotate = 270; + } + } + } else if (window.matchMedia("(orientation: portrait)").matches) { + // legacy support; it seems to update late, 100ms or so after screen.orientation, so lets not use it + session.forceRotate = 0; + } else if (window.matchMedia("(orientation: landscape)").matches) { + if (FirefoxSucks === 1) { + session.forceRotate = 90; + } else if (FirefoxSucks === 2) { + session.forceRotate = 270; + } + } + + var msg = {}; + msg.rotate_video = 0; + if (session.forceRotate !== false) { + if (session.rotate) { + msg.rotate_video = session.forceRotate + parseInt(session.rotate); + } else { + msg.rotate_video = session.forceRotate; + } + if (msg.rotate_video && msg.rotate_video >= 360) { + msg.rotate_video -= 360; + } + //warnlog("FIREFOX MOBILE ONLY ROTATE: "+msg.rotate_video); + //session.sendMessage(msg); + } else { + msg.rotate_video = parseInt(session.rotate) || 0; + + if (msg.rotate_video && msg.rotate_video >= 360) { + msg.rotate_video -= 360; + } + //warnlog("FIREFOX MOBILE ONLY ROTATE: "+msg.rotate_video); + } + for (var UUID in session.pcs) { + try { + if (session.pcs[UUID].rotation != msg.rotate_video) { + session.pcs[UUID].rotation = msg.rotate_video; + session.sendMessage(msg, UUID); + //log("sending updated rotation info"); + } + } catch (e) { + errorlog(e); + if (session.pcs[UUID].startTime + 100000 < Date.now()) { + warnlog("RTC Connection seems to be dead or not yet open? 8"); + } else { + log("RTC Connection seems to be dead or not yet open? 8"); + } + } + } + } catch (e) { + errorlog(e); + } + } else { + var msg = {}; + msg.rotate_video = 0; + if (session.forceRotate !== false) { + if (session.rotate) { + msg.rotate_video = session.forceRotate + parseInt(session.rotate); + } else { + msg.rotate_video = session.forceRotate; + } + } else { + msg.rotate_video = parseInt(session.rotate) || 0; + } + + if (msg.rotate_video && msg.rotate_video >= 360) { + msg.rotate_video -= 360; + } + + for (var UUID in session.pcs) { + try { + if (session.pcs[UUID].rotation != msg.rotate_video) { + session.pcs[UUID].rotation = msg.rotate_video; + session.sendMessage(msg, UUID); + //log("sending updated rotation info"); + } + } catch (e) { + errorlog(e); + if (session.pcs[UUID].startTime + 100000 < Date.now()) { + warnlog("RTC Connection seems to be dead or not yet open? 8"); + } else { + log("RTC Connection seems to be dead or not yet open? 8"); + } + } + } + } + if (!skipLastBit) { + applyMirror(session.mirrorExclude); + session.setResolution(); // probably only triggers with mobile devices? + } +} + +function updateForceRotatedCSS(rotateThis = session.forceRotate) { + if (rotateThis == 270) { + document.body.setAttribute("style", "transform: rotate(270deg);position: absolute;top: 100vh;left: 0;height: 100vw;width: 100vh;transform-origin: 0 0;"); + document.body.dataset.rotated = "1"; + } else if (rotateThis == 90) { + document.body.setAttribute("style", "transform: rotate(90deg);position: absolute;top: 0;left: 100vw;height: 100vw;width: 100vh;transform-origin: 0 0;"); + document.body.dataset.rotated = "1"; + } else if (rotateThis == 180) { + document.body.setAttribute("style", "transform: rotate(180deg);position: absolute;top: 100vh;left: 100vw;height: 100vh;width: 100vw;transform-origin: 0 0;"); + document.body.dataset.rotated = ""; + } else { + document.body.setAttribute("style", ""); + document.body.dataset.rotated = ""; + } +} + +async function joinDataMode() { + // join the room, but without publishing anything. + await session.connect(); + if (session.roomid) { + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + joinRoom(session.roomid); + } else if (session.view) { + window.onresize = updateMixer; + play(); + if (session.permaid !== false) { + session.postPublish(); + } + } else if (session.permaid !== false) { + session.postPublish(); + } +} + +function publishWebcam(btn = false, miconly = false) { + if (btn) { + if (btn.dataset.ready == "false") { + warnlog("Clicked too quickly; button not enabled yet"); + return; + } + + if (getById("passwordBasicInput").value.length) { + session.password = getById("passwordBasicInput").value; + session.password = sanitizePassword(session.password); + if (session.password.length == 0) { + session.password = false; + } else { + session.defaultPassword = false; + if (urlParams.has("pass")) { + updateURL("pass=" + session.password); + } else if (urlParams.has("pw")) { + updateURL("pw=" + session.password); + } else if (urlParams.has("p")) { + updateURL("p=" + session.password); + } else { + updateURL("password=" + session.password); + } + } + } + } + + if (activatedStream == true) { + return; + } + activatedStream = true; + log("PRESSED PUBLISH WEBCAM!!"); + + formSubmitting = false; + window.scrollTo(0, 0); // iOS has a nasty habit of overriding the CSS when changing camaera selections, so this addresses that. + + getById("head2").className = "hidden"; + + if (session.mobile && !session.roomid && session.permaid === false) { + if (!getById("rememberStreamID").classList.contains("hidden")) { + if (getById("rememberStreamIDcheck").checked) { + session.streamID = getStorage("permaid") || session.streamID; + setStorage("permaid", session.streamID, 99999); // ~ 13 months? + setStorage("rememberStreamIDmobile", "true", 99999); + } else { + removeStorage("permaid"); + setStorage("rememberStreamIDmobile", "false", 99999); + } + } + } + + if (session.roomid !== false) { + // they are in a room or a faux room + + window.onresize = updateMixer; + window.onorientationchange = function () { + if (Firefox) { + updateForceRotate(true); + } + setTimeout(async function () { + if (session.forceAspectRatio) { + await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + } + if (session.effect && session.effect === "7") { + digitalZoom(); // true needed to restart or start + } + updateForceRotate(); + updateMixer(); + }, 200); + }; + + if (session.roomid === "" && (!session.view || session.view === "")) { + // no room, no viewing, viewing disabled + if (session.manual === null) { + session.manual = session.manual === null ? true : session.manual; + } + if (!session.cleanOutput) { + var showReshare = getStorage("showReshare"); + if (showReshare) { + generateHash(session.streamID + session.salt + "bca321", 4) + .then(function (hash) { + // million to one error. + if (showReshare === hash) { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + } else if (session.permaid === null) { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + } + }) + .catch(errorlog); + } + } + } else { + log("ROOM ID ENABLED"); + log("Update Mixer Event on REsize SET"); + getById("main").style.overflow = "hidden"; + //session.cbr=0; // we're just going to override it + + if (session.stereo == 5) { + if (session.roomid === "") { + session.stereo = 1; + } else { + session.stereo = 3; + } + } + joinRoom(session.roomid); + if (session.roomid !== "") { + if (!session.cleanOutput) { + // Check if user joined via text input (casual user) + if (sessionStorage.getItem("jvi")) { + sessionStorage.removeItem("jvi"); + if (!urlParams.has("push") && !urlParams.has("id") && !urlParams.has("permaid") + && !urlParams.has("perma") && !urlParams.has("sticky")) { + // Show invite header INSTEAD of "You are in room" + var inviteURL = location.protocol + "//" + location.host + location.pathname + "?room=" + session.roomid; + var urlPW = urlParams.get("password") || urlParams.get("pass") || urlParams.get("pw") || urlParams.get("p"); + if (urlPW === "false" || urlPW === "0" || urlPW === "off") { + // Password explicitly disabled + inviteURL += "&password=false"; + getById("inviteLinkURL").href = inviteURL; + getById("inviteLinkURL").innerText = inviteURL; + getById("head9").classList.remove("hidden"); + } else if (urlPW) { + // Actual password in URL - generate hash + generateHash(session.password + session.salt, 4).then(function(hash) { + inviteURL += "&hash=" + hash; + getById("inviteLinkURL").href = inviteURL; + getById("inviteLinkURL").innerText = inviteURL; + getById("head9").classList.remove("hidden"); + }); + } else { + // No password in URL - use default, no param needed + getById("inviteLinkURL").href = inviteURL; + getById("inviteLinkURL").innerText = inviteURL; + getById("head9").classList.remove("hidden"); + } + } else { + getById("head2").className = ""; + } + } else { + getById("head2").className = ""; + } + } + } + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + } + } else { + // they are not in a room or faux room + if (session.manual === null) { + session.manual = session.manual === null ? true : session.manual; + } + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + getById("logoname").style.display = "none"; + generateHash(session.streamID + session.salt + "bca321", 4) + .then(function (hash) { + // million to one error. + setStorage("showReshare", hash, 24 * 30); + }) + .catch(errorlog); + } + + log("streamID is: " + session.streamID); + getById("head1").className = "hidden"; + + if (!session.cleanOutput) { + getById("mutebutton").classList.remove("hidden"); + getById("mutespeakerbutton").classList.remove("hidden"); + //getById("mutespeakerbutton").className="float"; + getById("chatbutton").className = "float"; + getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. + getById("mutevideobutton").className = "float"; + getById("hangupbutton").className = "float"; + if (session.showSettings) { + getById("settingsbutton").className = "float"; + } + if (session.raisehands) { + getById("raisehandbutton").className = "float"; + } + if (session.pptControls) { + getById("pptbackbutton").classList.remove("hidden"); + getById("pptnextbutton").classList.remove("hidden"); + } + if (session.recordLocal !== false) { + getById("recordLocalbutton").classList.remove("hidden"); + } + if (session.screensharebutton) { + if (session.roomid) { + if (session.screenshareType === 3) { + getById("screenshare3button").className = "float"; + getById("screensharebutton").className = "float hidden"; + getById("screenshare2button").className = "float hidden"; + } else if (session.screenshareType === 1) { + getById("screensharebutton").className = "float"; + getById("screenshare3button").className = "float hidden"; + getById("screenshare2button").className = "float hidden"; + } else if (session.screenshareType === 2) { + getById("screenshare2button").className = "float"; + getById("screensharebutton").className = "float hidden"; + getById("screenshare3button").className = "float hidden"; + } else { + getById("screenshare3button").className = "float"; + getById("screensharebutton").className = "float hidden"; + getById("screenshare2button").className = "float hidden"; + } + } else { + getById("screensharebutton").className = "float"; + getById("screenshare2button").className = "float hidden"; + getById("screenshare3button").className = "float hidden"; + } + } + getById("controlButtons").classList.remove("hidden"); + // getById("legal").classList.remove("hidden"); + //getById("helpbutton").style.display = "inherit"; + //getById("reportbutton").style.display = ""; + } else if (session.cleanish && session.recordLocal !== false) { + getById("recordLocalbutton").classList.remove("hidden"); + getById("mutebutton").classList.add("hidden"); + getById("mutespeakerbutton").classList.add("hidden"); + getById("chatbutton").classList.add("hidden"); + getById("mutevideobutton").classList.add("hidden"); + getById("hangupbutton").classList.add("hidden"); + getById("hangupbutton2").classList.add("hidden"); + getById("controlButtons").classList.remove("hidden"); + getById("settingsbutton").classList.add("hidden"); + getById("screenshare2button").classList.add("hidden"); + getById("screensharebutton").classList.add("hidden"); + getById("queuebutton").classList.add("hidden"); + } else { + getById("controlButtons").classList.add("hidden"); + } + + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("hidden"); + getById("controlButtons").classList.remove("hidden"); + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("hidden"); + } + + updatePushId(); + + if (session.dataMode) { + // skip the media stuff. + errorlog("this shoulnd't happen.."); + session.postPublish(); + return; + } + + if (!session.streamSrc) { + checkBasicStreamsExist(); // create srcObject + videoElement + } + + if (session.mobile && session.streamSrc && needsLegacyWakeLock()) { + if (Firefox) { + startLegacyKeepAliveLoop(true); + } else if (!session.avatar) { + try { + if (session.streamSrc.getVideoTracks && !session.streamSrc.getVideoTracks().length) { + startLegacyKeepAliveLoop(); + } + } catch (e) { + errorlog(e); + } + } + } + + session.publishStream(getById("previewWebcam")); // calls session.postPublish at the end. +} + +function createYoutubeLink(vidid) { + return "https://www.youtube.com/embed/" + vidid + "?modestbranding=1&playsinline=1&enablejsapi=1&autoplay=1"; +} +function parseURL4Iframe(iframeURL) { + if (iframeURL == "") { + iframeURL = "./"; + } + if (iframeURL === session.iframeSrc) { + return iframeURL; + } + + if (!iframeURL.startsWith("https://") && !iframeURL.startsWith("http://")) { + if (iframeURL.includes(".") && !iframeURL.startsWith("./") && !iframeURL.startsWith("/")) { + iframeURL = "https://" + iframeURL; + } + } + + if (iframeURL.startsWith("http://") && !electronApi && (location.hostname !== "insecure.vdo.ninja")) { + try { + iframeURL = "https://" + iframeURL.split("http://")[1]; + } catch (e) { + errorlog(e); + } + } + + if (iframeURL.startsWith("https://") || iframeURL.startsWith("http://")) { + var domain; + try { + domain = new URL(iframeURL); + domain = domain.hostname; + } catch (e) { + errorlog(e); + return iframeURL; + } + + if (domain == "youtu.be") { + iframeURL = iframeURL.replace("youtu.be/", "youtube.com/watch?v="); + } + + if (domain == "youtu.be" || domain == "www.youtube.com" || domain == "youtube.com") { + if (iframeURL.includes("/v/")) { + var vidMatch = iframeURL.match(/\/v\/([^\/\?#]+)/); + if (vidMatch && vidMatch[1] && vidMatch[1].length == 11) { + return createYoutubeLink(vidMatch[1]); + } + } + + var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; + var match = iframeURL.match(regExp); + var vidid = match && match[7] && match[7].length == 11 ? match[7] : false; + + if (iframeURL.includes("/live_chat")) { + if (!iframeURL.includes("&embed_domain=")) { + iframeURL += "&embed_domain=" + location.hostname; + } + return iframeURL; + } + + if (vidid) { + iframeURL = createYoutubeLink(vidid); + } else { + iframeURL = iframeURL.replace("playlist?list=", "embed/videoseries?list="); + var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(videoseries\?))\??list?=?([^#&?]*).*/; + var match = iframeURL.match(regExp); + var plid = match && match[7] && match[7].length == 34 ? match[7] : false; + if (plid) { + iframeURL = "https://www.youtube.com/embed/videoseries?list=" + plid + "&autoplay=1&modestbranding=1&playsinline=1&enablejsapi=1"; + } + } + } else if (domain == "twitch.tv" || domain == "www.twitch.tv") { + if (iframeURL.includes("twitch.tv/popout/")) { + iframeURL = iframeURL.replace("/popout/", "/embed/"); + iframeURL = iframeURL.replace("?popout=", "?parent=" + location.hostname); + iframeURL = iframeURL.replace("?popout", "?parent=" + location.hostname); + iframeURL = iframeURL.replace("&popout=", "?parent=" + location.hostname); + iframeURL = iframeURL.replace("&popout", "?parent=" + location.hostname); + if (iframeURL.includes("darkpopout=")) { + iframeURL = iframeURL.replace("?darkpopout=", "?darkpopout=&parent=" + location.hostname); + } else { + iframeURL = iframeURL.replace("?darkpopout", "?darkpopout&parent=" + location.hostname); + } + } else { + var vidid = iframeURL.split("/").pop().split("#")[0].split("?")[0]; + if (vidid) { + iframeURL = "https://player.twitch.tv/?channel=" + vidid + "&parent=" + location.hostname; + } + } + } else if (domain == "www.vimeo.com" || domain == "vimeo.com") { + iframeURL = iframeURL.replace("//vimeo.com/", "//player.vimeo.com/video/"); + iframeURL = iframeURL.replace("//www.vimeo.com/", "//player.vimeo.com/video/"); + } else if (domain.endsWith(".tiktok.com") || domain == "tiktok.com") { + var split = iframeURL.split("/video/"); + if (split.length > 1) { + split = split[1].split("/")[0].split("?")[0].split("#")[0]; + iframeURL = "https://www.tiktok.com/embed/v2/" + split; + } + } + } + + return iframeURL; +} + +function soloLinkGenerator(streamID, scene = true) { + var codecGroupFlag = ""; + if (session.codecGroupFlag) { + codecGroupFlag = session.codecGroupFlag; + } + if (session.bitrateGroupFlag) { + codecGroupFlag += session.bitrateGroupFlag; + } + + var wss = ""; + if (session.wssSetViaUrl) { + if (session.customWSS && (session.customWSS !== true)) { + wss = "&pie=" + session.customWSS; + } else if (session.customWSS == true) { + wss = "&wss=" + session.wss; + } else { + wss = "&wss2=" + session.wss; + } + } + + var passAdd2 = ""; + if (session.password) { + if (session.defaultPassword === false) { + passAdd2 = "&password=" + session.password; + } + } + + if (session.token) { + passAdd2 += "&token=" + session.token; + } + + // Add auth parameters if in auth mode + var authParams = ""; + if (session.authMode) { + // For view links, we need a universal token that bypasses auth + if (session.universalViewToken) { + authParams = "&universaltoken=" + session.universalViewToken; + } else { + // Fallback: include auth flag so viewer knows auth is required + authParams = "&auth=true"; + } + } + + if (scene) { + return "https://" + location.host + location.pathname + "?view=" + streamID + "&solo" + codecGroupFlag + "&room=" + session.roomid + passAdd2 + authParams + wss + soloLinkAppended; + } else { + return "https://" + location.host + location.pathname + "?view=" + streamID + codecGroupFlag + passAdd2 + authParams + wss + soloLinkAppended; + } +} + +function YoutubeAPI(iframe, func, args) { + // playVideo, pauseVideo, stopVideo + if (!(iframe && iframe.contentWindow)) { + return; + } + try { + iframe.contentWindow.postMessage( + JSON.stringify({ + event: "command", + func: func, + args: args || [], + id: iframe.id || "unknown" + }), + "*" + ); + } catch (e) { } +} + +function YoutubeListen(iframe_id) { + var iframe = document.getElementById(iframe_id); + if (!iframe) { + return; + } + if (iframe.loadedYoutubeListen) { + return; + } + try { + iframe.contentWindow.postMessage(JSON.stringify({ event: "listening", id: iframe_id }), "*"); // + } catch (e) { } + setTimeout( + function (iframe_id) { + YoutubeListen(iframe_id); + }, + 1000, + iframe_id + ); +} + +function processYoutubeEvent(e) { + if (!(e.type && e.type === "message")) { + return; + } + try { + var data = JSON.parse(e.data); + if ("id" in data) { + var iframe = document.getElementById(data.id); + if (!iframe) { + return; + } + if (!iframe.loadedYoutubeListen) { + iframe.loadedYoutubeListen = true; + } + } + if (!("mediaReferenceTime" in data.info)) { + return; + } + } catch (e) { + return; + } + + log(e); + + if (iframe.id == "iframe_source") { + if (!session.iframeEle.sendOnNewConnect) { + session.iframeEle.sendOnNewConnect = {}; + session.iframeEle.sendOnNewConnect.ifs = {}; + session.iframeEle.sendOnNewConnect.ifs.t = null; + session.iframeEle.sendOnNewConnect.ifs.v = null; + session.iframeEle.sendOnNewConnect.ifs.s = null; + session.iframeEle.sendOnNewConnect.ifs.r = null; + } + + try { + var msg = {}; + msg.ifs = {}; + + try { + msg.ifs.t = parseFloat(data.info.mediaReferenceTime + 0.01) || 0; + session.iframeEle.sendOnNewConnect.ifs.t = msg.ifs.t; + } catch (e) { + return; + } + + if ("playerState" in data.info) { + msg.ifs.s = parseInt(data.info.playerState); + + if (msg.ifs.s == -1) { + msg.ifs.s = 0; + } + if (msg.ifs.s == 2) { + if (session.iframeEle.sendOnNewConnect.ifs.s == 3) { + delete msg.ifs.s; + } else { + msg.ifs.s = 3; + } + } + if (msg.ifs.s && session.iframeEle.sendOnNewConnect.ifs.s != msg.ifs.s) { + session.iframeEle.sendOnNewConnect.ifs.s = msg.ifs.s; + } else { + //delete(msg.ifs.s); + } + + if ("videoData" in data.info) { + if (session.iframeEle.sendOnNewConnect.ifs.v != data.info.videoData.video_id) { + session.iframeEle.sendOnNewConnect.ifs.v = data.info.videoData.video_id; + msg.ifs.v = data.info.videoData.video_id; + var vidSrc = createYoutubeLink(msg.ifs.v); + if (vidSrc !== session.iframeSrc) { + session.iframeSrc = vidSrc; + var data = {}; + data.iframeSrc = session.iframeSrc; + if (parseInt(msg.ifs.t) > 1) { + data.iframeSrc += "&start=" + parseInt(Math.ceil(msg.ifs.t)) + ""; + } + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowIframe === true) { + session.sendMessage(data, UUID); + } + } + return; + } + } + } + // we will still be sending the msg data if available. + } else if ("videoData" in data.info) { + if (session.iframeEle.sendOnNewConnect.ifs.v != data.info.videoData.video_id) { + msg.ifs.v = data.info.videoData.video_id; + session.iframeEle.sendOnNewConnect.ifs.v = msg.ifs.v; + var vidSrc = createYoutubeLink(msg.ifs.v); + if (vidSrc !== session.iframeSrc) { + session.iframeSrc = vidSrc; + var data = {}; + data.iframeSrc = session.iframeSrc; + if (parseInt(msg.ifs.t) > 1) { + data.iframeSrc += "&start=" + parseInt(Math.ceil(msg.ifs.t)) + ""; + } + if (session.iframeEle.sendOnNewConnect.ifs.s == "1") { + data.iframeSrc += "&autoplay=1"; + } else { + data.iframeSrc += "&autoplay=0"; + } + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowIframe === true) { + session.sendMessage(data, UUID); + } + } + return; + } + } + } else { + if ("playbackRate" in data.info) { + msg.ifs.r = parseFloat(data.info.playbackRate); + if (session.iframeEle.sendOnNewConnect.ifs.r != msg.ifs.r) { + session.iframeEle.sendOnNewConnect.ifs.r = msg.ifs.r; + } else { + delete msg.ifs.r; + } + } + if (session.iframeEle.sendOnNewConnect.ifs.s == 1) { + if ("t" in msg.ifs) { + delete msg.ifs.t; + } + } + } + + if (Object.keys(msg.ifs).length == 0) { + return; + } + + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowIframe) { + session.sendMessage(msg); + } + } + } catch (e) { + return; + } + } else { + try { + var UUID = iframe.dataset.UUID; + var msg = {}; + msg.ifs = {}; + if ("t" in msg.ifs) { + msg.ifs.t = parseFloat(data.info.mediaReferenceTime + 0.01) || 0; + /* if (!iframe.sendOnNewConnect){ + iframe.sendOnNewConnect = msg; + } else { + iframe.sendOnNewConnect.ifs.t = msg.ifs.t; + } */ + } + + if ("playerState" in data.info) { + msg.ifs.s = parseInt(data.info.playerState); + } + if ("videoData" in data.info) { + msg.ifs.v = data.info.videoData.video_id; + } + if ("playbackRate" in data.info && data.info.playbackRate !== 1) { + msg.ifs.r = parseFloat(data.info.playbackRate); + } + // TODO: the viewers don't have a way to tell the director if they reload what the time is at. + session.sendRequest(msg, UUID); // send to the iframe's owner only. let them be the controller for others. + } catch (e) { + return; + } + } +} + +function processIframeSyncFeedback(ifs, UUID) { + // remote iframe feedback from the remote viewers + // YoutubeAPI("iframe_source", "seekTo", [700]); + // YoutubeAPI("iframe_source", "volume", [100]); + + warnlog(ifs); + return; + + if (!session.iframeEle.sendOnNewConnect) { + session.iframeEle.sendOnNewConnect = {}; + session.iframeEle.sendOnNewConnect.ifs = {}; + session.iframeEle.sendOnNewConnect.ifs.t = null; + session.iframeEle.sendOnNewConnect.ifs.v = null; + session.iframeEle.sendOnNewConnect.ifs.s = null; + session.iframeEle.sendOnNewConnect.ifs.r = null; + } + + if ("t" in ifs) { + if (Math.abs(session.iframeEle.sendOnNewConnect.ifs.t - ifs.t) >= 1) { + //session.iframeEle.sendOnNewConnect.ifs.t = ifs.t; + } else { + delete ifs.t; + } + } + if ("v" in ifs) { + if (session.iframeEle.sendOnNewConnect.ifs.v != ifs.v) { + //session.iframeEle.sendOnNewConnect.ifs.v = ifs.v; + } else { + delete ifs.v; + } + } + if ("s" in ifs) { + if (ifs.s == -1) { + ifs.s = 0; + } + if (session.iframeEle.sendOnNewConnect.ifs.s == -1) { + session.iframeEle.sendOnNewConnect.ifs.s = 0; + } + + if (ifs.s == 2) { + ifs.s = 3; + } + if (session.iframeEle.sendOnNewConnect.ifs.s == 2) { + session.iframeEle.sendOnNewConnect.ifs.s = 3; + } + + if (session.iframeEle.sendOnNewConnect.ifs.s != ifs.s) { + //session.iframeEle.sendOnNewConnect.ifs.s = ifs.s; + } else { + delete ifs.s; + } + } + if ("r" in ifs) { + if (session.iframeEle.sendOnNewConnect.ifs.r != ifs.r) { + //session.iframeEle.sendOnNewConnect.ifs.r = ifs.r; + } else { + delete ifs.r; + } + } + + if (session.iframeEle) { + if (ifs.v) { + // I need to have this change videos . + var vidSrc = createYoutubeLink(ifs.v); + if (vidSrc !== session.iframeSrc) { + session.iframeSrc = vidSrc; + session.iframeEle.src = vidSrc; + } + } else if ("t" in ifs) { + YoutubeAPI(session.iframeEle, "seekTo", [parseFloat(ifs.t)]); + } else if (ifs.r) { + /// setPlaybackRate + YoutubeAPI(session.iframeEle, "setPlaybackRate", [parseFloat(ifs.r)]); + } else if ("s" in ifs) { + /// setPlaybackState + if (ifs.s == -1) { + YoutubeAPI(session.iframeEle, "stopVideo"); + } else if (ifs.s == 0) { + YoutubeAPI(session.iframeEle, "stopVideo"); + } // player stops. + else if (ifs.s == 1) { + YoutubeAPI(session.iframeEle, "playVideo"); + } //Video is playing + else if (ifs.s == 2) { + YoutubeAPI(session.iframeEle, "pauseVideo"); + } //Video is paused + else if (ifs.s == 3) { + YoutubeAPI(session.iframeEle, "pauseVideo"); + } //video is buffering + else if (ifs.s == 5) { + } //Video is cued. + } + } else if (session.iframeSrc) { + if (ifs.v) { + var vidSrc = createYoutubeLink(ifs.v); + if (vidSrc !== session.iframeSrc) { + session.iframeSrc = vidSrc; + var data = {}; + data.iframeSrc = session.iframeSrc; + if (ifs.t && parseInt(ifs.t) > 1) { + data.iframeSrc += "&start=" + parseInt(Math.ceil(ifs.t)); + } + if (ifs.s == "1") { + data.iframeSrc += "&autoplay=1"; + } else { + data.iframeSrc += "&autoplay=0"; + } + for (var uuid in session.pcs) { + if (uuid == UUID) { + continue; + } + if (session.pcs[uuid].allowIframe === true) { + session.sendMessage(data, uuid); + } + } + return; + } + } + // we're going to forward the message directly to the other viewers instead + if ("s" in ifs) { + /// setPlaybackState + var msg = {}; + msg.ifs = ifs; + for (var uuid in session.pcs) { + if (uuid == UUID) { + continue; + } + if (session.pcs[uuid].allowIframe) { + session.sendMessage(msg, uuid); + } + } + } + } +} + +function processIframeSyncUpdates(ifs, UUID) { + // playback updates from remote guest. + // YoutubeAPI("iframe_source", "seekTo", [700]); + // YoutubeAPI("iframe_source", "volume", [100]); + if (ifs.v && "s" in ifs) { + // + } else if ("s" in ifs) { + if ("t" in ifs) { + YoutubeAPI(session.rpcs[UUID].iframeEle, "seekTo", [parseFloat(ifs.t)]); + } + YoutubeAPI(session.rpcs[UUID].iframeEle, "playVideo"); + } else if ("t" in ifs) { + YoutubeAPI(session.rpcs[UUID].iframeEle, "seekTo", [parseFloat(ifs.t)]); + } + if (ifs.r) { + /// setPlaybackRate + YoutubeAPI(session.rpcs[UUID].iframeEle, "setPlaybackRate", [parseFloat(ifs.r)]); + } + if ("s" in ifs) { + /// setPlaybackState + if (ifs.s == -1) { + YoutubeAPI(session.rpcs[UUID].iframeEle, "stopVideo"); + } else if (ifs.s == 0) { + YoutubeAPI(session.rpcs[UUID].iframeEle, "stopVideo"); + } // player stops. + else if (ifs.s == 1) { + YoutubeAPI(session.rpcs[UUID].iframeEle, "playVideo"); + } //Video is playing + else if (ifs.s == 2) { + YoutubeAPI(session.rpcs[UUID].iframeEle, "pauseVideo"); + } //Video is paused + else if (ifs.s == 3) { + YoutubeAPI(session.rpcs[UUID].iframeEle, "pauseVideo"); + } //video is buffering + else if (ifs.s == 5) { + } //Video is cued. + } +} + +function updatePushId() { + if (session.doNotSeed) { + return; + } + + if (urlParams.has("push")) { + updateURL("push=" + session.streamID); + } else if (urlParams.has("id")) { + updateURL("id=" + session.streamID); + } else if (urlParams.has("permaid")) { + updateURL("permaid=" + session.streamID); + } else { + updateURL("push=" + session.streamID); + } +} + +session.publishIFrame = function (iframeURL) { + if (!session.cleanOutput) { + getById("websitesharebutton2").classList.remove("hidden"); + } + + if (session.transcript) { + setTimeout(function () { + setupClosedCaptions(); + }, 1000); + } + + session.iframeSrc = parseURL4Iframe(iframeURL); + + if (!session.iFramesAllowed) { errorlog("Can't create iFRAME - security is tainted due to possible CSS injection"); warnUser("Can't create iFRAME - security is tainted due to possible CSS injection"); return; } + + var iframe = document.createElement("iframe"); + iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location + iframe.src = session.iframeSrc; + iframe.id = "iframe_source"; + iframe.setAttribute("allowtransparency", "true"); + iframe.setAttribute("crossorigin", "anonymous"); + iframe.setAttribute("credentialless", "true"); + iframe.loadedYoutubeListen = false; + session.iframeEle = iframe; + + var container = document.createElement("div"); + iframe.container = container; + container.id = "container_iframe"; + container.appendChild(iframe); + getById("gridlayout").appendChild(container); + + if (session.iframeSrc.startsWith("https://www.youtube.com/")) { + // special handler. + setTimeout( + function (iframe_id) { + YoutubeListen(iframe_id); + }, + 1000, + iframe.id + ); + } + + if (session.cover) { + container.style.setProperty("height", "100%", "important"); + } + + if (session.roomid !== false) { + if (session.roomid === "" && (!session.view || session.view === "")) { + } else { + log("ROOMID EANBLED"); + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + joinRoom(session.roomid); + } + } else { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + getById("logoname").style.display = "none"; + } + getById("head1").className = "hidden"; + + updatePushId(); + + getById("head1").className = "hidden"; + getById("head2").className = "hidden"; + + if (!session.cleanOutput) { + getById("chatbutton").className = "float"; + getById("hangupbutton").className = "float"; + getById("controlButtons").classList.remove("hidden"); + // getById("legal").classList.remove("hidden"); + getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. + //getById("helpbutton").style.display = "inherit"; + //getById("reportbutton").style.display = ""; + } else { + getById("controlButtons").classList.add("hidden"); + } + + if (session.chatbutton === false) { + getById("chatbutton").classList.add("hidden"); + } + + if (session.director) { + // + } else if (session.scene !== false) { + updateMixer(); + } else if (session.roomid !== false) { + if (session.roomid === "") { + if (!session.view || session.view === "") { + session.windowed = session.windowed === null ? true : session.windowed; + container.classList.add("vidcon"); + + getById("mutespeakerbutton").classList.add("hidden"); + container.style.width = "100%"; + container.style.height = "100%"; + container.style.alignItems = "center"; + container.style.maxWidth = "100%"; + container.style.maxHeight = "100%"; + container.style.verticalAlign = "middle"; + container.style.margin = "auto"; + container.style.backgroundColor = "#666"; + container.style.border = "2px solid"; + } else { + session.windowed = session.windowed === null ? false : session.windowed; + window.onresize = updateMixer; + updateMixer(); + } + } else { + window.onresize = updateMixer; + session.windowed = session.windowed === null ? false : session.windowed; + updateMixer(); + } + } else { + window.onresize = updateMixer; + container.style.maxHeight = "1280px"; + container.style.maxWidth = "720px"; + container.style.verticalAlign = "middle"; + container.style.height = "100%"; + container.style.width = "100%"; + container.style.margin = "auto"; + container.style.alignItems = "center"; + container.style.backgroundColor = "#666"; + } + + session.seeding = true; + + updateReshareLink(); + pokeIframeAPI("started-iframe-share"); + session.seedStream(); + + return container; +}; // publishIframe + +/* session.publishWhepSrc = function(){ + + if (!session.whepSrc){errorlog("no WHEP Src");return;} + + if (!session.cleanOutput){ + getById("websitesharebutton2").classList.remove('hidden'); + } + + var UUID = whepIn(session.whepSrc); + + var container = document.createElement("div"); + iframe.container = container; + container.id = "container_iframe"; + container.appendChild(iframe); + getById("gridlayout").appendChild(container); + + if (session.iframeSrc.startsWith("https://www.youtube.com/")){ // special handler. + setTimeout(function(iframe_id){YoutubeListen(iframe_id);}, 1000, iframe.id); + } + + if (session.cover){ + container.style.setProperty('height', '100%', 'important'); + } + + if (session.roomid!==false){ + if ((session.roomid==="") && ((!(session.view)) || (session.view===""))){ + + } else { + log("ROOMID EANBLED"); + getById("head3").classList.add('hidden'); + getById("head3a").classList.add('hidden'); + joinRoom(session.roomid); + } + + } else { + getById("head3").classList.remove('hidden'); + getById("head3a").classList.remove('hidden'); + getById("logoname").style.display = 'none'; + } + getById("head1").className = 'hidden'; + + updatePushId() + + getById("head1").className = 'hidden'; + getById("head2").className = 'hidden'; + + if (!(session.cleanOutput)){ + getById("chatbutton").className="float"; + getById("hangupbutton").className="float"; + getById("controlButtons").classList.remove("hidden"); + getById('sharefilebutton').classList.remove("hidden"); // we won't override "display:none", if set, though. + getById("helpbutton").style.display = "inherit"; + getById("reportbutton").style.display = ""; + } else { + getById("controlButtons").classList.add("hidden"); + } + + if (session.chatbutton === false) { + getById("chatbutton").classList.add("hidden"); + } + + if (session.director){ + // + } else if (session.scene!==false){ + updateMixer(); + } else if (session.roomid!==false){ + if (session.roomid===""){ + if (!session.fullscreen && (!(session.view) || (session.view===""))){ + session.windowed = session.windowed === null ? true : session.windowed; + container.classList.add("vidcon"); + + getById("mutespeakerbutton").classList.add("hidden"); + container.style.width="100%"; + container.style.height="100%"; + container.style.alignItems = "center"; + container.style.maxWidth= "100%"; + container.style.maxHeight= "100%"; + container.style.verticalAlign= "middle"; + container.style.margin= "auto"; + container.style.backgroundColor = "#666"; + container.style.border = "2px solid"; + + } else { + session.windowed = session.windowed === null ? false : session.windowed; + window.onresize = updateMixer; + updateMixer(); + } + } else { + window.onresize = updateMixer; + session.windowed = session.windowed === null ? false : session.windowed; + updateMixer(); + } + } else { + window.onresize = updateMixer; + container.style.maxHeight= "1280px"; + container.style.maxWidth= "720px"; + container.style.verticalAlign= "middle"; + container.style.height="100%"; + container.style.width= "100%"; + container.style.margin= "auto"; + container.style.alignItems = "center"; + container.style.backgroundColor = "#666"; + } + + session.seeding=true; + + updateReshareLink(); + pokeIframeAPI('started-iframe-share'); + session.seedStream(); + + return container; +} // publishWhepSrc */ + +function disabledWebAudioPathway() { + log("Executing disabledWebAudioPathway."); + // if (session.disableWebAudio) { then run this instead; or if webaudio nodes fail.} + // if (iOS || iPad){return session.streamSrc;} // Original comment: iOS devices can't remap video tracks, else KABOOM. Might as well do this for android also. + // This iOS specific return was in comments, if it's critical, it should be uncommented. + // However, the logic below also attempts to handle stream cloning. + + if (session.streamSrcClone) { + log("disabledWebAudioPathway: Cleaning up existing session.streamSrcClone"); + session.streamSrcClone.getTracks().forEach(function (track) { + session.streamSrcClone.removeTrack(track); + track.stop(); + }); + session.streamSrcClone = null; + } + + var newStream = createMediaStream(); // This will be the returned stream + + if (session.streamSrc && typeof session.streamSrc.clone === 'function' && !window.obsstudio) { // Prefer cloning if available and not obsstudio + log("disabledWebAudioPathway: Cloning session.streamSrc (non-obsstudio path)"); + newStream = session.streamSrc.clone(); + } else { + log("disabledWebAudioPathway: Creating new stream and adding tracks manually."); + if (session.streamSrc) { + session.streamSrc.getAudioTracks().forEach(function (track) { + if (track.readyState === 'live') { + // For obsstudio, audio tracks can also be cloned for consistency, though less critical than video. + // For simplicity here, adding original, but could be `track.clone()` + newStream.addTrack(track); + log("disabledWebAudioPathway: Added audio track " + track.id); + } + }); + } + + let videoSourceForDisabledPath = null; + if (session.videoElement && session.videoElement.srcObject) { + videoSourceForDisabledPath = session.videoElement.srcObject; + } else if (session.streamSrc) { + videoSourceForDisabledPath = session.streamSrc; + } + + if (videoSourceForDisabledPath) { + videoSourceForDisabledPath.getVideoTracks().forEach(function (track) { + if (track.readyState === 'live') { + if (window.obsstudio) { + log(`disabledWebAudioPathway (obsstudio): Cloning video track ${track.id}`); + try { + const clonedVideoTrack = track.clone(); + newStream.addTrack(clonedVideoTrack); + } catch (e_clone_video) { + errorlog(`disabledWebAudioPathway (obsstudio): Failed to clone video track ${track.id}. Adding original. Error:`, e_clone_video); + newStream.addTrack(track); // Fallback + } + } else { + log(`disabledWebAudioPathway (non-obsstudio): Adding original video track ${track.id}`); + newStream.addTrack(track); // Original behavior + } + } + }); + } + } + if (iOS || iPad || session.streamSrcClone) { + session.streamSrcClone = newStream; // Store the newly created/cloned stream + } + return newStream; +} + +function outboundAudioPipeline(sourceStream = false) { + + if (session.disableWebAudio) { + return disabledWebAudioPathway(); // Safemode + } + + if (!session.streamSrc && !sourceStream) { + errorlog("STREAM DOES NOT EXIST. This is a problem"); + checkBasicStreamsExist(); + return session.streamSrc; + } + + var streamSrc = sourceStream || session.streamSrc; + + if (iOS || iPad) { + if (session.streamSrcClone) { + var audioTracksForCleanup = session.streamSrcClone.getAudioTracks(); + if (audioTracksForCleanup.length) { + for (var waid in session.webAudios) { + if (session.webAudios[waid] && typeof session.webAudios[waid].stop === 'function') { + session.webAudios[waid].stop(); + } + delete session.webAudios[waid]; + } + } + session.streamSrcClone.getTracks().forEach(function (track) { + session.streamSrcClone.removeTrack(track); + track.stop(); + }); + session.streamSrcClone = null; + } + + // Create iOS-compatible stream (always clone for iOS) + if (session.streamSrc && typeof session.streamSrc.clone === 'function') { + log("iOS: Cloning session.streamSrc"); + streamSrc = session.streamSrc.clone(); + session.streamSrcClone = streamSrc; + } else { + log("iOS: Creating new stream as backup"); + session.streamSrcClone = createOptimizedStream(session.streamSrc, true); + streamSrc = session.streamSrcClone; + } + } + + for (var waid in session.webAudios) { + if (session.webAudios[waid] && typeof session.webAudios[waid].stop === 'function') { + session.webAudios[waid].stop(); + } + delete session.webAudios[waid]; + } + session.webAudios = session.webAudios || {}; + + try { + log("Web Audio processing initiated."); + var audioTracks = streamSrc.getAudioTracks(); + + if (audioTracks.length) { + var webAudio = initWebAudioNode(audioTracks[0].id); + + if (audioTracks.length > 1) { + try { + setupMultiTrackAudio(audioTracks, webAudio); + } catch (e_multi) { + errorlog("Error in multi-track audio setup, falling back: ", e_multi); + try { + webAudio.mediaStreamSource = webAudio.audioContext.createMediaStreamSource(streamSrc); + webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext); + } catch (e_multi_fallback) { + errorlog("Fallback failed: ", e_multi_fallback); + return disabledWebAudioPathway(); + } + } + } else { + try { + webAudio.mediaStreamSource = webAudio.audioContext.createMediaStreamSource(streamSrc); + webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext); + } catch (e_single) { + errorlog("Error creating single track setup: ", e_single); + return disabledWebAudioPathway(); + } + } + + var anonNode = applyAudioProcessing(webAudio, streamSrc); + + const finalOutputStream = createMediaStream(); + webAudio.destination.stream.getAudioTracks().forEach(audioTrack => { + finalOutputStream.addTrack(audioTrack); + }); + + addVideoTracksToStream(finalOutputStream, streamSrc); + + if (webAudio.audioContext && webAudio.audioContext.state === "suspended") { + webAudio.audioContext.resume().catch(e => errorlog("AudioContext resume failed:", e)); + } + + return finalOutputStream; + + } else { + log("No audio tracks found. Handling video passthrough."); + // Return video-only stream + if (window.obsstudio) { + log("OBS (no audio): Creating stream with cloned video tracks"); + const newStream = createMediaStream(); + addVideoTracksToStream(newStream, streamSrc); + return newStream; + } else { + log("Non-OBS (no audio): Using direct video source"); + if (session.videoElement && session.videoElement.srcObject) { + return session.videoElement.srcObject; + } + + const newStream = createMediaStream(); + addVideoTracksToStream(newStream, streamSrc); + return newStream; + } + } + } catch (e_main) { + errorlog("Critical error in outboundAudioPipeline: "); + errorlog(e_main); + return streamSrc; + } +} + +function createOptimizedStream(source, shouldClone = false) { + const newStream = createMediaStream(); + + if (!source) return newStream; + + source.getAudioTracks().forEach(track => { + if (track.readyState === 'live') { + if (shouldClone) { + try { + newStream.addTrack(track.clone()); + } catch (e) { + errorlog("Failed to clone audio track. Adding original. Error:", e); + newStream.addTrack(track); + } + } else { + newStream.addTrack(track); + } + } + }); + + addVideoTracksToStream(newStream, source); + + return newStream; +} + +function addVideoTracksToStream(targetStream, sourceStream) { + let videoSourceStream = sourceStream; + + // Find video source with fallback + if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks().length > 0) { + videoSourceStream = session.videoElement.srcObject; + } else if (!videoSourceStream || videoSourceStream.getVideoTracks().length === 0) { + videoSourceStream = session.streamSrc; + } + + if (videoSourceStream) { + videoSourceStream.getVideoTracks().forEach(track => { + if (track.readyState === 'live') { + // Only clone for OBS Studio + if (window.obsstudio) { + log(`OBS: Cloning video track ${track.id}`); + try { + const clonedTrack = track.clone(); + targetStream.addTrack(clonedTrack); + } catch (e_clone) { + errorlog(`OBS: Failed to clone track ${track.id}. Adding original. Error:`, e_clone); + targetStream.addTrack(track); + } + } else { + targetStream.addTrack(track); + } + } + }); + } +} + +function initWebAudioNode(trackId) { + var webAudio = { + id: trackId, + micDelay: false, + compressor: false, + analyser: false, + gainNode: false, + splitter: false, + subGainNodes: false, + lowEQ: false, + midEQ: false, + highEQ: false, + lowcut1: false, + lowcut2: false, + lowcut3: false, + waveShaper_vc: null, + oscillator_vc: null, + oscillatorGain_vc: null, + delay_vc: null, + lowEQ_vc: null, + mid_vc: null + }; + + // Create audio context if needed + if (session.audioCtxOutbound) { + // Already created + } else if (session.outboundSampleRate) { + try { + session.audioCtxOutbound = new AudioContext({ sampleRate: session.outboundSampleRate }); + } catch (e) { + session.audioCtxOutbound = new AudioContext(); + errorlog(e); + } + } else if (session.outboundSampleRate === false || Firefox || SafariVersion || session.mobile) { + session.audioCtxOutbound = new AudioContext(); + } else if (session.audioLatency !== false) { + session.audioCtxOutbound = new AudioContext({ + latencyHint: session.audioLatency / 1000.0, + sampleRate: 48000 + }); + } else { + try { + session.audioCtxOutbound = new AudioContext({ sampleRate: 48000 }); + } catch (e) { + session.audioCtxOutbound = new AudioContext(); + errorlog(e); + } + } + + if (session.audioCtxOutbound && session.audioCtxOutbound.sampleRate > 192000) { + console.error("Warning: Your audio playback device has a very high sample rate set; lower it to 48000-Hz to avoid audio issues"); + } + + webAudio.audioContext = session.audioCtxOutbound; + webAudio.destination = session.audioCtxOutbound.createMediaStreamDestination(); + + return webAudio; +} + +// Helper function to handle multi-track audio setup +function setupMultiTrackAudio(audioTracks, webAudio) { + var maxChannelCount = session.stereo === false ? 1 : 2; + webAudio.subGainNodes = {}; + + var mergerNode = webAudio.audioContext.createChannelMerger(maxChannelCount); + + for (var i = 0; i < audioTracks.length; i++) { + try { + var tempIndividualTrackStream = createMediaStream(); + tempIndividualTrackStream.addTrack(audioTracks[i]); + var trackAudioSourceNode = webAudio.audioContext.createMediaStreamSource(tempIndividualTrackStream); + + webAudio.subGainNodes[audioTracks[i].id] = webAudio.audioContext.createGain(); + trackAudioSourceNode.connect(webAudio.subGainNodes[audioTracks[i].id]); + + if (maxChannelCount == 2) { + var individualSplitter = webAudio.audioContext.createChannelSplitter(2); + webAudio.subGainNodes[audioTracks[i].id].connect(individualSplitter); + individualSplitter.connect(mergerNode, 0, 0); + try { + individualSplitter.connect(mergerNode, 1, 1); + } catch (e_stereo) { + errorlog("Stereo connect ch1->input1 failed: ", e_stereo); + try { + individualSplitter.connect(mergerNode, 0, 1); + } catch (e_stereo_fallback) { + errorlog("Stereo connect ch0->input1 fallback failed: ", e_stereo_fallback); + } + } + } else { + webAudio.subGainNodes[audioTracks[i].id].connect(mergerNode, 0, 0); + } + } catch (e_track) { + errorlog("Error processing track: ", e_track); + throw e_track; + } + } + + webAudio.mediaStreamSource = mergerNode; + webAudio.gainNode = audioGainNode(webAudio.mediaStreamSource, webAudio.audioContext); +} + +function applyAudioProcessing(webAudio, streamSrc) { + try { + var anonNode = webAudio.gainNode; + + // Channel downmixing + if (session.audioInputChannels == 1) { + anonNode = applyDownmixing(anonNode, webAudio); + } + + // Low cut filter + if (session.lowcut) { + anonNode = applyLowCut(anonNode, webAudio); + } + + // Voice changer + if (session.voicechanger) { + anonNode = applyVoiceChanger(anonNode, webAudio); + } + + // Equalizer + if (session.equalizer) { + anonNode = applyEqualizer(anonNode, webAudio); + } + + // Compressor/Limiter + if (session.compressor === 1) { + webAudio.compressor = audioCompressor(anonNode, webAudio.audioContext); + anonNode = webAudio.compressor; + } else if (session.compressor === 2) { + webAudio.compressor = audioLimiter(anonNode, webAudio.audioContext); + anonNode = webAudio.compressor; + } + + // Mic panning (publisher-side): force mono, then pan to stereo + if (session.micPanning !== false) { + anonNode = applyMicPanning(anonNode, webAudio, session.micPanning); + } + + // Mic delay + if (session.micDelay !== false) { + webAudio.micDelay = micDelayNode(anonNode, webAudio.audioContext); + anonNode = webAudio.micDelay; + } + + // Twilio mix + if (session.twilio && session.twilio.element && session.twilio.element.srcObject && session.twilio.element.srcObject.getAudioTracks().length) { + const twilioSource = webAudio.audioContext.createMediaStreamSource(session.twilio.element.srcObject); + twilioSource.connect(anonNode); + } + + // Noise gate + if (session.noisegate !== false) { + webAudio.analyser = audioMeter(anonNode, webAudio.audioContext); + anonNode = webAudio.analyser; + webAudio.gatingNode = audioGatingNode(anonNode, webAudio.audioContext); + webAudio.gatingNode.connect(webAudio.destination); + } else { + webAudio.analyser = audioMeter(anonNode, webAudio.audioContext); + webAudio.analyser.connect(webAudio.destination); + } + + webAudio.stop = createStopFunction(webAudio); + + if (streamSrc && webAudio.mediaStreamSource) { + const tracks = streamSrc.getTracks(); + if (tracks.length) { + tracks.forEach(track => { + track.addEventListener('ended', () => { + log("Track ended, stopping webAudio"); + webAudio.stop(); + }); + }); + } else if (webAudio.mediaStreamSource.onended !== undefined) { + // Fallback for older browsers + webAudio.mediaStreamSource.onended = () => { + log("MediaStreamSource ended, stopping webAudio"); + webAudio.stop(); + }; + } + } + + session.webAudios[webAudio.id] = webAudio; + } catch (e) { + console.error(e); + return webAudio; + } + + return anonNode; +} + +function applyMicPanning(inputNode, webAudio, value) { + // Convert 0..180 to -1..1 (90 center) + if (value === true || value === "true") { + value = 90; + } + value = parseFloat(value); + if (isNaN(value)) { value = 90; } + var panNorm = (value / 90.0) - 1.0; + if (panNorm < -1) panNorm = -1; + if (panNorm > 1) panNorm = 1; + + // Downmix to mono explicitly + let splitter = webAudio.audioContext.createChannelSplitter(2); + let mono = webAudio.audioContext.createChannelMerger(1); + try { + inputNode.connect(splitter); + splitter.connect(mono, 0, 0); + splitter.connect(mono, 1, 0); + } catch (e) { + // If connection fails (e.g., mono input), fallback to direct + try { inputNode.connect(mono, 0, 0); } catch (ee) { } + } + + // Pre-pan gain reduction to avoid clipping when panned + webAudio.micPanGainNode = webAudio.audioContext.createGain(); + webAudio.micPanGainNode.gain.value = 1 - Math.abs(panNorm) / 2; + mono.connect(webAudio.micPanGainNode); + + // Create panner with Safari fallback + if (webAudio.audioContext.createStereoPanner) { + webAudio.micPanType = "stereo"; + webAudio.micPanNode = webAudio.audioContext.createStereoPanner(); + webAudio.micPanNode.pan.value = panNorm; + } else { + webAudio.micPanType = "panner"; + webAudio.micPanNode = webAudio.audioContext.createPanner(); + webAudio.micPanNode.panningModel = "equalpower"; + webAudio.micPanNode.distanceModel = "inverse"; + let x = panNorm; + let z = 1 - Math.abs(panNorm); + try { + if (typeof webAudio.micPanNode.positionX !== "undefined") { + webAudio.micPanNode.positionX.value = x; + webAudio.micPanNode.positionY.value = 0; + webAudio.micPanNode.positionZ.value = z; + } else { + webAudio.micPanNode.setPosition(x, 0, z); + } + } catch (e) { } + } + + webAudio.micPanGainNode.connect(webAudio.micPanNode); + return webAudio.micPanNode; +} + +function changeMicPanning(value, deviceid = null) { + // Update all active outbound webAudio chains + let pan = parseFloat(value); + if (isNaN(pan)) { pan = 90; } + if (pan < 0) pan = 0; + if (pan > 180) pan = 180; + let norm = (pan / 90.0) - 1.0; + if (norm < -1) norm = -1; + if (norm > 1) norm = 1; + session.micPanning = pan; + + for (var waid in session.webAudios) { + try { + let wa = session.webAudios[waid]; + if (!wa) continue; + if (wa.micPanNode) { + if (wa.micPanType === "stereo" && wa.micPanNode.pan) { + wa.micPanNode.pan.setValueAtTime(norm, wa.audioContext.currentTime); + } else { + let x = norm; + let z = 1 - Math.abs(norm); + if (typeof wa.micPanNode.positionX !== "undefined") { + wa.micPanNode.positionX.setValueAtTime(x, wa.audioContext.currentTime); + wa.micPanNode.positionY.setValueAtTime(0, wa.audioContext.currentTime); + wa.micPanNode.positionZ.setValueAtTime(z, wa.audioContext.currentTime); + } else if (wa.micPanNode.setPosition) { + wa.micPanNode.setPosition(x, 0, z); + } + } + } + if (wa.micPanGainNode && wa.micPanGainNode.gain) { + wa.micPanGainNode.gain.setValueAtTime(1 - Math.abs(norm) / 2, wa.audioContext.currentTime); + } + } catch (e) { errorlog(e); } + } +} + +// helper to keep approval popup text current with label/streamID +session.updateApprovalPrompt = function (UUID) { + try { + if (!session.director || !session.approval_popup) { return; } + var label = (session.rpcs[UUID] && session.rpcs[UUID].label) || ("Guest " + (UUID || '').substring(0, 8)); + var sid = (session.rpcs[UUID] && session.rpcs[UUID].streamID) || UUID; + try { label = ("" + label).replace(/[<>]/g, ""); sid = ("" + sid).replace(/[<>]/g, ""); } catch (e) { } + var line = "A guest is waiting to be admitted.\n\n" + + "Guest: " + label + "\n" + + "ID: " + sid + "\n\n" + + (session.directorState === false ? "Approve?\n(This sends the action to the main director.)" : "Approve?"); + updateConfirmAlt('approval-' + UUID, line); + } catch (e) { errorlog(e); } +}; + +function requestChangeMicPanning(value, UUID, track = 0) { + var msg = {}; + msg.requestChangeMicPanning = true; + msg.value = value; + msg.UUID = UUID; + msg.track = track; + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-change-micpanning", { value: value, track: track }, UUID); +} +function createStopFunction(webAudio) { + return function () { + // Prevent multiple calls + if (webAudio.stopped) { + errorlog("Trying to stop webaudio more than once"); + return; + } + webAudio.stopped = true; + + // Clear analyzer interval if it exists + try { + if (webAudio.analyser && webAudio.analyser.interval) { + clearInterval(webAudio.analyser.interval); + } + } catch (e) { + errorlog("Error clearing analyser interval:", e); + } + + // Special handling for subGainNodes (collection of nodes) + if (webAudio.subGainNodes) { + for (var id in webAudio.subGainNodes) { + try { + if (webAudio.subGainNodes[id]) { + webAudio.subGainNodes[id].disconnect(); + webAudio.subGainNodes[id] = null; + } + } catch (e) { + errorlog("Error disconnecting subGainNode " + id + ":", e); + } + } + webAudio.subGainNodes = null; + } + + // List of properties to skip disconnecting + const skipProperties = ["stop", "id", "audioContext", "mediaStreamSource", "subGainNodes", "stopped"]; + + // Disconnect all other nodes + for (var node in webAudio) { + if (!webAudio[node] || skipProperties.includes(node)) { + continue; + } + + try { + // Only disconnect if it has a disconnect method (is an audio node) + if (typeof webAudio[node].disconnect === 'function') { + webAudio[node].disconnect(); + log("Disconnected node: " + node); + } + webAudio[node] = null; + } catch (e) { + errorlog("Error disconnecting node " + node + ":", e); + } + } + + // Remove from session tracking + if (session.webAudios && webAudio.id && session.webAudios[webAudio.id]) { + delete session.webAudios[webAudio.id]; + } + }; +} + +function changeLowCut(freq, deviceid = null) { + log("LOW EQ"); + + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].lowcut1) { + errorlog("EQ not setup"); + return; + } + if (!session.webAudios[webAudio].lowcut2) { + errorlog("EQ not setup"); + return; + } + if (!session.webAudios[webAudio].lowcut3) { + errorlog("EQ not setup"); + return; + } + session.webAudios[webAudio].lowcut1.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); + session.webAudios[webAudio].lowcut2.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); + session.webAudios[webAudio].lowcut3.frequency.setValueAtTime(freq, session.webAudios[webAudio].audioContext.currentTime); + } +} + +function changeLowEQ(lowEQ, deviceid = null) { + log("LOW EQ"); + + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].lowEQ) { + errorlog("EQ not setup"); + return; + } + session.webAudios[webAudio].lowEQ.gain.setValueAtTime(lowEQ, session.webAudios[webAudio].audioContext.currentTime); + } +} + +function changeMidEQ(midEQ, deviceid = null) { + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].midEQ) { + errorlog("EQ not setup"); + return; + } + session.webAudios[webAudio].midEQ.gain.setValueAtTime(midEQ, session.webAudios[webAudio].audioContext.currentTime); + } +} + +function changeHighEQ(highEQ, deviceid = null) { + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].highEQ) { + errorlog("EQ not setup"); + return; + } + session.webAudios[webAudio].highEQ.gain.setValueAtTime(highEQ, session.webAudios[webAudio].audioContext.currentTime); + } +} + +function changeMicDelay(delay, deviceid = null) { + log("changeMicDelay :" + delay); + for (var waid in session.webAudios) { + // add a mic delay + if (!session.webAudios[waid].micDelay) { + errorlog("Mic Delay not setup"); + } else { + session.webAudios[waid].micDelay.delayTime.setValueAtTime(delay / 1000, session.webAudios[waid].audioContext.currentTime); + } + } +} + +function changeSubGain(gain, deviceid = null) { + if (gain !== false) { + gain = parseFloat(gain / 100.0) || 0; + } else { + gain = 1.0; + } + for (var webAudio in session.webAudios) { + try { + if (!session.webAudios[webAudio].subGainNodes) { + errorlog("EQ not setup"); + return; + } + if (deviceid in session.webAudios[webAudio].subGainNodes) { + session.webAudios[webAudio].subGainNodes[deviceid].gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); + } else { + errorlog("NOT FOUND:" + deviceid); + } + break; + } catch (e) { + errorlog(e); + } + } +} + +function changeMainGain(gain, fadeout = 0) { + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].gainNode) { + return; + } + if (gain !== false) { + gain = parseFloat(gain / 100.0) || 0; + } else { + gain = 1.0; + } + if (fadeout) { + try { + session.webAudios[webAudio].gainNode.gain.linearRampToValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime + fadeout / 1000); + } catch (e) { + session.webAudios[webAudio].gainNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); + } + } else { + session.webAudios[webAudio].gainNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); + } + } +} + +function changeGatingGain(gain, fadeout = 0) { + for (var webAudio in session.webAudios) { + if (!session.webAudios[webAudio].gatingNode) { + return; + } + if (gain !== false) { + gain = parseFloat(gain / 100.0) || 0; + } else { + gain = 1.0; + } + if (fadeout) { + try { + session.webAudios[webAudio].gatingNode.gain.linearRampToValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime + fadeout / 1000); + } catch (e) { + session.webAudios[webAudio].gatingNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); + } + } else { + session.webAudios[webAudio].gatingNode.gain.setValueAtTime(gain, session.webAudios[webAudio].audioContext.currentTime); + } + } +} + +function applyDownmixing(inputNode, webAudio) { + // Complex downmixing logic with channel counting and gain adjustment + let totalChannels = 0; + let activeChannels = 0; + let tracks = webAudio.audioContext._stream ? webAudio.audioContext._stream.getAudioTracks() : []; + + tracks.forEach(track => { + if (track.getSettings && track.getSettings().channelCount) { + let trackChannels = track.getSettings().channelCount; + totalChannels += trackChannels; + if (track.enabled) { + activeChannels += trackChannels; + } + } else { + // Fallback if getSettings is not available + totalChannels += 2; // Assume stereo + if (track.enabled) { + activeChannels += 2; + } + } + }); + + totalChannels = Math.max(totalChannels, 1); + activeChannels = Math.max(activeChannels, 1); + + webAudio.splitter = webAudio.audioContext.createChannelSplitter(totalChannels); + inputNode.connect(webAudio.splitter); + webAudio.merger = webAudio.audioContext.createChannelMerger(1); + + // Create a gain node for volume adjustment + webAudio.downmixGain = webAudio.audioContext.createGain(); + + // Connect splitter outputs to merger through the gain node + for (let i = 0; i < totalChannels; i++) { + webAudio.splitter.connect(webAudio.downmixGain, i, 0); + } + + webAudio.downmixGain.connect(webAudio.merger, 0, 0); + + // Set gain to 1 / sqrt(activeChannels) to maintain perceived loudness + let gainValue = 1 / Math.sqrt(activeChannels); + webAudio.downmixGain.gain.setValueAtTime(gainValue, webAudio.audioContext.currentTime); + + log(`Downmixing ${totalChannels} total channels (${activeChannels} active) to mono. Gain set to ${gainValue.toFixed(3)}`); + + return webAudio.merger; +} + +function applyLowCut(inputNode, webAudio) { + // Apply high-pass filter chain for low frequency cut + webAudio.lowcut1 = webAudio.audioContext.createBiquadFilter(); + webAudio.lowcut1.type = "highpass"; + webAudio.lowcut1.frequency.value = session.lowcut; + + webAudio.lowcut2 = webAudio.audioContext.createBiquadFilter(); + webAudio.lowcut2.type = "highpass"; + webAudio.lowcut2.frequency.value = session.lowcut; + + webAudio.lowcut3 = webAudio.audioContext.createBiquadFilter(); + webAudio.lowcut3.type = "highpass"; + webAudio.lowcut3.frequency.value = session.lowcut; + + inputNode.connect(webAudio.lowcut1); + webAudio.lowcut1.connect(webAudio.lowcut2); + webAudio.lowcut2.connect(webAudio.lowcut3); + + return webAudio.lowcut3; +} + +function applyVoiceChanger(inputNode, webAudio) { + function makeDistortionCurve(amount = 10) { + var sampleRate = webAudio.audioContext.sampleRate || 48000; + var curve = new Float32Array(sampleRate); + var x; + for (let i = 0; i < sampleRate; ++i) { + x = (i * 2) / sampleRate - 1; + curve[i] = ((3 + amount) * x * 20 * (Math.PI / 180)) / (Math.PI + amount * Math.abs(x)); + } + return curve; + } + + let waveShaper = webAudio.audioContext.createWaveShaper(); + waveShaper.curve = makeDistortionCurve(5); + + var realCoeffs = new Float32Array([1, 0]); + var imagCoeffs = new Float32Array([0, 1]); + + var numCoeffs = 20; // The more coefficients you use, the better the approximation + var realCoeffs = new Float32Array(numCoeffs); + var imagCoeffs = new Float32Array(numCoeffs); + + realCoeffs[0] = 0.5; + for (var i = 1; i < numCoeffs; i++) { + // note i starts at 1 + imagCoeffs[i] = (1 / (i * Math.PI)) * (1 - Math.random() / 2); + } + + let oscillator = webAudio.audioContext.createOscillator(); + oscillator.frequency.value = 10; + + const wave = webAudio.audioContext.createPeriodicWave(realCoeffs, imagCoeffs); + oscillator.setPeriodicWave(wave); + + let oscillatorGain = webAudio.audioContext.createGain(); + oscillatorGain.gain.value = 0.005; + oscillator.connect(oscillatorGain); + oscillator.start(0); + + let delay = webAudio.audioContext.createDelay(); + delay.delayTime.value = 0.01; + oscillatorGain.connect(delay.delayTime); + + let lowEQ = webAudio.audioContext.createBiquadFilter(); + lowEQ.type = "peaking"; + lowEQ.frequency.value = 200; + lowEQ.Q.value = 0.5; + lowEQ.gain.value = 6; + + let mid = webAudio.audioContext.createBiquadFilter(); + mid.type = "peaking"; + mid.frequency.value = 500; + mid.Q.value = 0.5; + mid.gain.value = -10; + + inputNode.connect(delay); + delay.connect(waveShaper); + waveShaper.connect(mid); + mid.connect(lowEQ); + + return lowEQ; +} + +function applyEqualizer(inputNode, webAudio) { + // https://webaudioapi.com/samples/frequency-response/ for a tool to help set values + webAudio.lowEQ = webAudio.audioContext.createBiquadFilter(); + webAudio.lowEQ.type = "lowshelf"; + webAudio.lowEQ.frequency.value = 100; + webAudio.lowEQ.gain.value = 0; + + webAudio.midEQ = webAudio.audioContext.createBiquadFilter(); + webAudio.midEQ.type = "peaking"; + webAudio.midEQ.frequency.value = 1000; + webAudio.midEQ.Q.value = 0.5; + webAudio.midEQ.gain.value = 0; + + webAudio.highEQ = webAudio.audioContext.createBiquadFilter(); + webAudio.highEQ.type = "highshelf"; + webAudio.highEQ.frequency.value = 10000; + webAudio.highEQ.gain.value = 0; + + inputNode.connect(webAudio.lowEQ); + webAudio.lowEQ.connect(webAudio.midEQ); + webAudio.midEQ.connect(webAudio.highEQ); + + return webAudio.highEQ; +} + +function micDelayNode(mediaStreamSource, audioContext) { + if (session.micDelay !== false) { + var delay = parseFloat(session.micDelay / 1000) || 0; + var delayNode = audioContext.createDelay(delay + 3); + } else { + var delay = 0; + var delayNode = audioContext.createDelay(3); + } + delayNode.delayTime.value = delay; + mediaStreamSource.connect(delayNode); + return delayNode; +} + +function audioGainNode(mediaStreamSource, audioContext) { + var gainNode = audioContext.createGain(); + if (session.audioGain !== false) { + var gain = parseFloat(session.audioGain / 100.0) || 0; + } else { + var gain = 1.0; + } + gainNode.gain.value = gain; + mediaStreamSource.connect(gainNode); + return gainNode; +} + +function audioGatingNode(mediaStreamSource, audioContext) { + var gateNode = audioContext.createGain(); + gateNode.gain.value = 1.0; + mediaStreamSource.connect(gateNode); + return gateNode; +} + +function audioMeter(mediaStreamSource, audioContext) { + var analyser = audioContext.createAnalyser(); + mediaStreamSource.connect(analyser); + analyser.fftSize = 256; + analyser.smoothingTimeConstant = 0.05; + + var bufferLength = analyser.frequencyBinCount; + var dataArray = new Uint8Array(bufferLength); + var timer = null; + + var meter1 = document.getElementById("meter1") || false; + var meter2 = document.getElementById("meter2") || false; + var meter3 = document.getElementById("meter3") || false; + var meter4 = document.getElementById("meter4") || false; + + var currentlyActive = 0; // mode 5 + + var ng1 = 10; + var ng2 = 25; + var ng3 = 30; + + if (session.noisegateSettings) { + if (session.noisegateSettings.length) { + ng1 = parseInt(session.noisegateSettings[0]) || 0; // gated volume target (lower to this level) + } + if (session.noisegateSettings.length > 1) { + ng2 = parseInt(session.noisegateSettings[1]) || ng2; // not loud (threshold level) + } + if (session.noisegateSettings.length > 2) { + ng3 = parseInt(session.noisegateSettings[2]) || 0; // stickiness; time (ms) + ng3 = ng3 / 100.0; // convert to the actual units (100ms) + } + } + if (session.noisegate) { + changeGatingGain(ng1, 200); + } + + function draw() { + try { + analyser.getByteFrequencyData(dataArray); + var total = 0; + for (var i = 0; i < dataArray.length; i++) { + total += dataArray[i]; + } + total = total / 100; + if (session.quietOthers && session.quietOthers == 2) { + if (total > 10) { + if (session.muted_activeSpeaker == false) { + session.muted_activeSpeaker = true; + session.speakerMuted = true; + clearTimeout(timer); + toggleSpeakerMute(true); // okay, sicne this is quietOthers + } + } else if (session.muted_activeSpeaker == true) { + session.speakerMuted = false; + session.muted_activeSpeaker = false; + session.activelySpeaking = false; + clearTimeout(timer); + timer = setTimeout(function () { + toggleSpeakerMute(true); + }, 250); // okay, sicne this is quietOthers + } + } + + if (session.pushLoudness == true) { + var loudnessObj = {}; + loudnessObj[session.streamID] = parseInt(total); + if (isIFrame) { + parent.postMessage({ loudness: loudnessObj, action: "loudness", value: total }, session.iframetarget); + } + } + + if (session.noisegate) { + if (total <= ng2) { + if (currentlyActive == ng3) { + changeGatingGain(ng1, 200); // set volume to 40% relative to what it is now. + log("GAIN LOWERED"); + currentlyActive = ng3 + 1; + } else if (currentlyActive < ng3) { + currentlyActive += 1; + } + } else if (currentlyActive == ng3 + 1) { + changeGatingGain(100, 200); + currentlyActive = 0; + log("GAIN INCREASED"); + } else { + currentlyActive = 0; + } + } + + if (meter1) { + if (document.getElementById("meter1")) { + if (total == 0) { + meter1.style.width = "1px"; + meter2.style.width = "0px"; + } else if (total <= 1) { + meter1.style.width = "1px"; + meter2.style.width = "0px"; + } else if (total <= 150) { + meter1.style.width = total + "px"; + meter2.style.width = "0px"; + } else if (total > 150) { + if (total > 200) { + total = 200; + } + meter1.style.width = "150px"; + meter2.style.width = total - 150 + "px"; + } + } else { + meter1 = false; + } + if (session.audioGain !== false) { + if (document.getElementById("previewWebcam")) { + changeMainGain(100); // full volume while in preview mode + } else { + changeMainGain(session.audioGain); + } + } + return; + } else if (toggleSettingsState && document.getElementById("meter3")) { + if (total == 0) { + meter3.style.width = "1px"; + meter4.style.width = "0px"; + } else if (total <= 1) { + meter3.style.width = "1px"; + meter4.style.width = "0px"; + } else if (total <= 150) { + meter3.style.width = total + "px"; + meter4.style.width = "0px"; + } else if (total > 150) { + if (total > 200) { + meter3.style.width = "150px"; + meter4.style.width = "50px"; + } else { + meter3.style.width = "150px"; + meter4.style.width = total - 150 + "px"; + } + } + if (document.getElementById("mutetoggle")) { + total *= 3; + if (total > 255) { + total = 255; + } + total = parseInt(total); + document.getElementById("mutetoggle").style.color = "rgb(" + (255 - total) + ",255," + (255 - total) + ")"; + } + meter1 = false; + return; + } else if (session.cleanOutput) { + meter1 = false; + return; + } else if (document.getElementById("mutetoggle")) { + total *= 3; + if (total > 255) { + total = 255; + } + total = parseInt(total); + document.getElementById("mutetoggle").style.color = "rgb(" + (255 - total) + ",255," + (255 - total) + ")"; + } else { + clearInterval(analyser.interval); + warnlog("METERS NOT FOUND"); + } + meter1 = false; + } catch (e) { + errorlog(e); + } + } + + analyser.interval = setInterval(function () { + draw(); + }, 100); + return analyser; +} + +function audioCompressor(mediaStreamSource, audioContext) { + var compressor = audioContext.createDynamicsCompressor(); + compressor.threshold.value = -40; + compressor.knee.value = 10; + compressor.ratio.value = 4; // 3 + compressor.attack.value = 0.002; // 0.001 + compressor.release.value = 0.1; // 0.06 + mediaStreamSource.connect(compressor); + return compressor; +} + +function audioLimiter(mediaStreamSource, audioContext) { + var compressor = audioContext.createDynamicsCompressor(); + compressor.threshold.value = -5; + compressor.knee.value = 0; + compressor.ratio.value = 20.0; // 1 to 20 + compressor.attack.value = 0.001; + compressor.release.value = 0.1; + mediaStreamSource.connect(compressor); + return compressor; +} + +function activeSpeaker(border = false) { + var lastActiveSpeaker = null; + + var someoneElseIfSpeaking = false; + var anyoneIsSpeaking = 0; + var defaultSpeaker = false; + var anyVideoAvailable = false; // Track if any video streams are available at all + var changed = false; + + // First pass: check if any video is available + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && + session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { + anyVideoAvailable = true; + break; + } + } + + for (var UUID in session.rpcs) { + + if (session.scene) { + let pass = checkMuteState(UUID); + // If no one is visible and this person has video, show them immediately + if (pass && !anyoneIsSpeaking && !defaultSpeaker && anyVideoAvailable === false && + session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && + session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { + session.rpcs[UUID].defaultSpeaker = true; + defaultSpeaker = true; + anyVideoAvailable = true; + changed = true; + continue; + } else if (pass) { + session.rpcs[UUID].activelySpeaking = false; + if (session.rpcs[UUID].defaultSpeaker && session.rpcs[UUID].defaultSpeaker !== true) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } + session.rpcs[UUID].defaultSpeaker = false; + continue; + } + } + + if (session.activeSpeaker > 2 && !(session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted)) { + session.rpcs[UUID].activelySpeaking = false; // we're not showing audio-only sources in this mode. + if (session.rpcs[UUID].defaultSpeaker && session.rpcs[UUID].defaultSpeaker !== true) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } + session.rpcs[UUID].defaultSpeaker = false; + continue; + } + + if (session.rpcs[UUID].stats._Audio_Loudness_average) { + if (session.rpcs[UUID].stats.Audio_Loudness && session.rpcs[UUID].stats.Audio_Loudness > 10) { + session.rpcs[UUID].stats._Audio_Loudness_average = parseFloat(session.rpcs[UUID].stats.Audio_Loudness * 0.07 + session.rpcs[UUID].stats._Audio_Loudness_average * 0.93); + } else { + session.rpcs[UUID].stats._Audio_Loudness_average = parseFloat(session.rpcs[UUID].stats._Audio_Loudness_average * 0.975); + } + } else { + session.rpcs[UUID].stats._Audio_Loudness_average = 1; + } + if (session.rpcs[UUID].stats._Audio_Loudness_average > 13) { + if (border) { + if (session.rpcs[UUID].videoElement) { + session.rpcs[UUID].videoElement.style.border = "green solid 1px"; + session.rpcs[UUID].videoElement.style.padding = "0"; + } + } else if (!session.rpcs[UUID].activelySpeaking) { + session.rpcs[UUID].activelySpeaking = true; + lastActiveSpeaker = UUID; + session.rpcs[UUID].stats._Audio_Loudness_average += 50; + } + } else if (session.rpcs[UUID].stats._Audio_Loudness_average > 6) { + // + } else { + if (border) { + if (session.rpcs[UUID].videoElement) { + session.rpcs[UUID].videoElement.style.border = ""; + session.rpcs[UUID].videoElement.style.padding = "1px"; + } + } else if (session.rpcs[UUID].activelySpeaking) { + session.rpcs[UUID].activelySpeaking = false; + lastActiveSpeaker = UUID; + } + } + if (session.rpcs[UUID].stats.Audio_Loudness > 13 || (session.rpcs[UUID].stats.Audio_Loudness > 5 && session.rpcs[UUID].stats._Audio_Loudness_average > 3) || session.rpcs[UUID].stats._Audio_Loudness_average > 6) { + someoneElseIfSpeaking = true; + } + if (session.rpcs[UUID].activelySpeaking) { + anyoneIsSpeaking += 1; + } + if (session.rpcs[UUID].defaultSpeaker === true) { + defaultSpeaker = true; + } + } + + var loudest = null; + var loudestActive = null; + + if (session.activeSpeaker === 1 || session.activeSpeaker === 3) { + // will only show one speaker at a time; the loudest or last-loud speaker + if (!anyoneIsSpeaking) { + if (defaultSpeaker) { + // already good to go. + } else if (lastActiveSpeaker) { + if (session.rpcs[lastActiveSpeaker].defaultSpeaker !== false) { + clearTimeout(session.rpcs[lastActiveSpeaker].defaultSpeaker); + } else { + changed = true; + log("lastActiveSpeaker is default"); + } + session.rpcs[lastActiveSpeaker].defaultSpeaker = true; + } else if (session.scene === false || (session.nopreview === false && session.minipreview !== 1)) { + // we don't need to care. + } else if (anyVideoAvailable === false) { + // Immediately select the first available video source if no one is currently visible + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && + session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + log(UUID + " is speaker now (no lull)"); + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } + } + // Fall through to original logic if needed + if (!changed) { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + log(UUID + " is speaker now"); + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } + } + if (!changed && session.activeSpeaker <= 2) { + // switch to streams that have no video track + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].label) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + log(UUID + " is speaker now"); + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } else if (!changed) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + log(UUID + " is speaker now"); + } + session.rpcs[UUID].defaultSpeaker = true; + } + } + } + } + } else { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + log(UUID + " is speaker now"); + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } + } + if (!changed && session.activeSpeaker <= 2) { + // switch to streams that have no video track + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].label) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + log(UUID + " is speaker now"); + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } else if (!changed) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + log(UUID + " is speaker now"); + } + session.rpcs[UUID].defaultSpeaker = true; + } + } + } + } + } else { + for (var UUID in session.rpcs) { + if (!("_Audio_Loudness_average" in session.rpcs[UUID].stats)) { + // never could have been loudest, since no loudness value. + continue; + } + + if (session.rpcs[UUID].activelySpeaking) { + if (!loudestActive) { + loudestActive = UUID; + } else if (session.rpcs[UUID].stats._Audio_Loudness_average > session.rpcs[loudestActive].stats._Audio_Loudness_average) { + if (session.rpcs[loudestActive].defaultSpeaker === true) { + if (!session.activeSpeakerTimeout) { + session.rpcs[loudestActive].defaultSpeaker = false; + changed = true; + log(loudestActive + " is loudest but not speaker anymore"); + } else { + session.rpcs[loudestActive].defaultSpeaker = setTimeout( + function (uuid) { + session.rpcs[uuid].defaultSpeaker = false; + updateMixer(); + }, + session.activeSpeakerTimeout, + loudestActive + ); + } + } + loudestActive = UUID; + } else if (session.rpcs[UUID].defaultSpeaker === true) { + if (!session.activeSpeakerTimeout) { + session.rpcs[UUID].defaultSpeaker = false; + changed = true; + log(UUID + " is not speaker anymore"); + } else { + session.rpcs[UUID].defaultSpeaker = setTimeout( + function (uuid) { + session.rpcs[uuid].defaultSpeaker = false; + updateMixer(); + }, + session.activeSpeakerTimeout, + UUID + ); + } + } + } else if (session.rpcs[UUID].defaultSpeaker === true) { + if (!session.activeSpeakerTimeout) { + session.rpcs[UUID].defaultSpeaker = false; + changed = true; + log(UUID + " is not speaker anymore"); + } else { + session.rpcs[UUID].defaultSpeaker = setTimeout( + function (uuid) { + session.rpcs[uuid].defaultSpeaker = false; + updateMixer(); + }, + session.activeSpeakerTimeout, + UUID + ); + } + } + } + + if (loudestActive && session.rpcs[loudestActive].defaultSpeaker !== true) { + if (session.rpcs[loudestActive].defaultSpeaker) { + clearTimeout(session.rpcs[loudestActive].defaultSpeaker); + } else { + changed = true; + } + for (let UUID in session.rpcs) { + if (loudestActive !== UUID) { + if (session.rpcs[UUID].defaultSpeaker === true) { + session.rpcs[UUID].defaultSpeaker = false; // Reset immediately before any new logic + changed = true; + } else if (session.rpcs[UUID].defaultSpeaker) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + session.rpcs[UUID].defaultSpeaker = false; + } + } + } + + session.rpcs[loudestActive].defaultSpeaker = true; + } + } + } else if (session.activeSpeaker === 2 || session.activeSpeaker === 4) { + // will show whoever is talking; mixed together; if no one is talking, just shows yourself + + if (!anyoneIsSpeaking) { + if (defaultSpeaker) { + // already good to go. + } else if (lastActiveSpeaker) { + if (session.rpcs[lastActiveSpeaker].defaultSpeaker !== false) { + clearTimeout(session.rpcs[lastActiveSpeaker].defaultSpeaker); + } else { + changed = true; + } + session.rpcs[lastActiveSpeaker].defaultSpeaker = true; + } else if (session.scene === false || (session.nopreview === false && session.minipreview !== 1)) { + // we don't need to care. + } else if (anyVideoAvailable === false) { + // Immediately select the first available video source if no one is currently visible + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && + session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + log(UUID + " is speaker now (no lull)"); + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } + } + // Fall through to original logic if needed + if (!changed) { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } + } + if (!changed && session.activeSpeaker <= 2) { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].label) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } else if (!changed) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + } + session.rpcs[UUID].defaultSpeaker = true; + } + } + } + } + } else { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.srcObject && session.rpcs[UUID].videoElement.srcObject.getVideoTracks().length && !session.rpcs[UUID].videoMuted) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } + } + if (!changed && session.activeSpeaker <= 2) { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].label) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + } + session.rpcs[UUID].defaultSpeaker = true; + break; + } else if (!changed) { + if (session.rpcs[UUID].defaultSpeaker !== false) { + clearTimeout(session.rpcs[UUID].defaultSpeaker); + } else { + changed = true; + } + session.rpcs[UUID].defaultSpeaker = true; + } + } + } + } + } else { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].activelySpeaking && !session.rpcs[UUID].defaultSpeaker) { + session.rpcs[UUID].defaultSpeaker = true; + changed = true; + } else if (!session.rpcs[UUID].activelySpeaking && session.rpcs[UUID].defaultSpeaker) { + if (!session.activeSpeakerTimeout) { + session.rpcs[UUID].defaultSpeaker = false; + changed = true; + } else if (session.rpcs[UUID].defaultSpeaker === true) { + session.rpcs[UUID].defaultSpeaker = setTimeout( + function (uuid) { + session.rpcs[uuid].defaultSpeaker = false; + updateMixer(); + }, + session.activeSpeakerTimeout, + UUID + ); + } + } + } + } + } + if (session.quietOthers && session.quietOthers === 1) { + if (someoneElseIfSpeaking) { + if (session.muted_activeSpeaker == false) { + session.muted_activeSpeaker = true; + session.muted = true; + toggleMute(true); + } + } else if (session.muted_activeSpeaker == true) { + session.muted = false; + session.muted_activeSpeaker = false; + toggleMute(true); + } + } else if (session.quietOthers && session.quietOthers === 3) { + // purely for fun. It's the opposite of a noise-gate I guess. + if (someoneElseIfSpeaking) { + if (session.muted_activeSpeaker == false) { + session.muted_activeSpeaker = true; + session.speakerMuted = true; + toggleSpeakerMute(true); // okay, sicne this is quietOthers + } + } else if (session.muted_activeSpeaker == true) { + session.speakerMuted = false; + session.muted_activeSpeaker = false; + toggleSpeakerMute(true); // okay, sicne this is quietOthers + } + } + + if (changed) { + setTimeout(function () { + updateMixer(); + }, 0); + } +} + +function randomizeArray(unshuffled) { + var arr = unshuffled + .map(a => ({ + sort: Math.random(), + value: a + })) + .sort((a, b) => a.sort - b.sort) + .map(a => a.value); // shuffle once + + for (var i = arr.length - 1; i > 0; i--) { + // shuffle twice + var j = Math.floor(Math.random() * (i + 1)); + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + return arr; +} + +async function joinRoom(roomname) { + if (roomname.length) { + roomname = sanitizeRoomName(roomname); + log("Join room: " + roomname); + + // In auth mode, use auth-aware room joining + if (session.authMode && window.vdoAuth) { + const hasAccess = await window.vdoAuth.joinRoom(roomname); + if (!hasAccess) { + return; // Access denied or auth required + } + // Room ID might have changed if it was an alias + roomname = session.roomid; + } + + updateVolume(false); // chance of a race condition, but unlikely and not a big deal if so. + session.joinRoom(roomname).then( + function (response) { + // callback from server; we've joined the room. Just the listing is returned + + if (session.joiningRoom === "seedPlz") { + // allow us to seed, now that we have joined the room. + session.joiningRoom = false; // joined + session.seedStream(); + } else { + session.joiningRoom = false; // no seeding callback + } + + // Create universal token for directors in auth mode + if (session.director && session.authMode && session.authToken && !session.universalViewToken) { + vdoAuth.createUniversalToken().then(() => { + if (session.universalViewToken) { + updateAllSoloLinks(); + } + }); + } + + // Apply any pending room settings selected pre-join (access mode, allowlist) + if (session.director && session.authMode && window.vdoAuth && session.authToken && session.pendingRoomSettings) { + try { + window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, session.pendingRoomSettings); + } catch (e) { console.error(e); } + // Clear once applied + session.pendingRoomSettings = null; + } + var token = ""; + if (session.token) { + token += "&token=" + session.token; + } + + if (!session.cleanOutput) { + if (session.roomhost) { + if (session.defaultPassword === false) { + if (session.password === false) { + var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + "&password=false" + token; + warnUser("You can invite others with:\n\n" + invite + "", false, false); + } else { + generateHash(session.password + session.salt, 4).then(function (hash) { + // change the hash length from 4 to 3 when VDO.Ninja v24.10 or newer is in production. + var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + "&hash=" + hash + token; + warnUser("You can invite others with:\n\n" + invite + "", false, false); + }); + } + } else { + var invite = "https://" + location.host + location.pathname + "?room=" + session.roomid + token; + warnUser("You can invite others with:\n\n" + invite + "", false, false); + } + } + } + + log("Members in Room"); + log(response); + + if (session.randomize === true) { + response = randomizeArray(response); + log("Randomized List of Viewers"); + log(response); + for (var i in response) { + if ("UUID" in response[i]) { + if (response[i].streamID) { + if (response[i].UUID in session.rpcs) { + log("RTC already connected"); /// lets just say instead of Stream, we have + } else { + log(response[i].streamID); + var streamID = session.desaltStreamID(response[i].streamID); + if (session.queue) { + if (session.directorList.indexOf(response[i].UUID) >= 0) { + // Only queueType 2 (&screen) sees director immediately. + // queueType 3 (&hold) and 4 (&holdwithvideo) are fully isolated + // from the director until activated. + if (session.queueType == 2) { + warnlog("PLAYING DIRECTOR"); + play(streamID, response[i].UUID); + } + } else if (session.view_set && session.view_set.includes(streamID)) { + play(streamID, response[i].UUID); + } else if (session.queueList.length < 5000) { + if (!(streamID in session.watchTimeoutList) && !session.queueList.includes(streamID)) { + session.queueList.push(streamID); + } + } + } else { + log("STREAM ID DESALTED 3: " + streamID); + setTimeout( + function (sid) { + play(sid); + }, + Math.floor(Math.random() * 100), + streamID + ); // add some furtherchance with up to 100ms added latency + } + } + } + } + } + } else { + for (var i in response) { + if ("UUID" in response[i]) { + if (response[i].streamID) { + if (response[i].UUID in session.rpcs) { + log("RTC already connected"); /// lets just say instead of Stream, we have + } else { + log(response[i].streamID); + var streamID = session.desaltStreamID(response[i].streamID); + if (session.queue) { + if (session.directorList.indexOf(response[i].UUID) >= 0) { + // Only queueType 2 (&screen) sees director immediately. + // queueType 3 (&hold) and 4 (&holdwithvideo) are fully isolated + // from the director until activated. + if (session.queueType == 2) { + play(streamID, response[i].UUID); + } + } else if (session.view_set && session.view_set.includes(streamID)) { + play(streamID, response[i].UUID); + } else if (session.queueList.length < 5000) { + if (!(streamID in session.watchTimeoutList) && !session.queueList.includes(streamID)) { + session.queueList.push(streamID); + } + } + } else { + log("STREAM ID DESALTED 4: " + streamID); + play(streamID, response[i].UUID); // play handles the group room mechanics here + } + } + } + } + } + } + updateQueue(); + pokeIframeAPI("joined-room-complete"); + + if (session.include.length) { + // we want to request what hasn't been requested already, since we are joining a room. + session.include.forEach(sid => { + if (sid in session.waitingWatchList) { + return; + } else { + session.watchStream(sid); + } + }); + } + }, + function (error) { + return {}; + } + ); + } else { + log("Room name not long enough or contained all bad characaters"); + } +} + +async function createRoom(roomname = false, reload = false) { + if (reload === true) { + let oldDirectorSettings = getStorage("directorOtherSettings"); + var passwordRoom = oldDirectorSettings.password; + if (passwordRoom === session.defaultPassword) { + passwordRoom = ""; + } else if (passwordRoom === false) { + passwordRoom = ""; + session.password = false; + } + roomname = oldDirectorSettings.roomid; + if (!roomname) { + warnUser("Couldn't load previous session"); + return; + } + if (urlParams.has("dir")) { + updateURL("dir=" + roomname, true, false); // make the link reloadable. + } else { + updateURL("director=" + roomname, true, false); // make the link reloadable. + } + + session.codecGroupFlag = session.codecGroupFlag || oldDirectorSettings.codecGroupFlag || session.codecGroupFlag; + + session.label = session.label || oldDirectorSettings.label || session.label; + session.codecGroupFlag = session.codecGroupFlag || oldDirectorSettings.codecGroupFlag || session.codecGroupFlag; + session.showDirector = session.showDirector || oldDirectorSettings.showDirector || session.showDirector; + + if (oldDirectorSettings.broadcast) { + getById("broadcastFlag").checked = true; + } + if (session.showDirector) { + getById("showdirectorFlag").checked = true; + } + } else { + if (roomname == false) { + roomname = getById("videoname1").value; + roomname = sanitizeRoomName(roomname); + + clearDirectorSettings(); + + if (roomname.length != 0) { + if (urlParams.has("dir")) { + updateURL("dir=" + roomname, true, false); // make the link reloadable. + } else { + updateURL("director=" + roomname, true, false); // make the link reloadable. + } + } + } + if (roomname.length == 0) { + //if (!(session.cleanOutput)) { + // warnUser("Please enter a room name before continuing"); + //} + + getById("videoname1").focus(); + getById("videoname1").classList.remove("shake"); + setTimeout(function () { + getById("videoname1").classList.add("shake"); + }, 10); + + return; + } + log(roomname); + + var passwordRoom = document.getElementById("passwordRoom") ? sanitizePassword(document.getElementById("passwordRoom").value) : ""; + + // Pre-join SSO room setup (optional) + try { + var ssoBox = getById('useSSOForRoom'); + if (ssoBox && ssoBox.checked) { + // Enable auth mode for this room + session.authMode = true; + // Director should sign in before managing the room + // Note: join gating handled by vdoAuth.joinRoom in joinRoom() + // Capture desired access mode to apply after join + var selected = document.querySelector('input[name="ssoAccessMode"]:checked'); + var accessMode = (selected && selected.value) ? selected.value : 'public'; + var allowlist = []; + if (accessMode === 'allowlist') { + var csv = (getById('preAllowlistCSV') && getById('preAllowlistCSV').value) ? getById('preAllowlistCSV').value : ''; + if (csv) { + allowlist = csv.split(',').map(x => x.trim()).filter(x => x.length > 0); + } + } + // Store to apply after join + session.pendingRoomSettings = { accessMode: accessMode, allowlist: allowlist }; + // If guests must sign in (authenticated/allowlist), mark as requireAuth for UX + if (accessMode === 'authenticated' || accessMode === 'allowlist') { + session.requireAuth = true; + } + } + } catch (e) { } + } + + session.roomid = roomname; + getById("dirroomid").innerHTML = decodeURIComponent(session.roomid); + getById("roomid").innerHTML = session.roomid; + + var passAdd = ""; + var passAdd2 = ""; + + if (passwordRoom.length) { + session.password = passwordRoom; + session.defaultPassword = false; + + if (session.password === "false" || session.password === "0" || session.password === "off") { + session.password = false; + if (urlParams.has("pass")) { + updateURL("pass=0"); + passAdd = "&pass=0"; + passAdd2 = "&pass=0"; + } else if (urlParams.has("pw")) { + updateURL("pw=0"); + passAdd = "&pw=0"; + passAdd2 = "&pw=0"; + } else if (urlParams.has("p")) { + updateURL("p=0"); + passAdd = "&p=0"; + passAdd2 = "&p=0"; + } else if (urlParams.has("password")) { + updateURL("password=false"); + passAdd = "&password=false"; + passAdd2 = "&password=false"; + } else { + updateURL("p=0"); + passAdd = "&p=0"; + passAdd2 = "&p=0"; + } + } else { + if (urlParams.has("pass")) { + updateURL("pass=" + session.password); + } else if (urlParams.has("pw")) { + updateURL("pw=" + session.password); + } else if (urlParams.has("p")) { + updateURL("p=" + session.password); + } else { + updateURL("password=" + session.password); + } + } + } + + await registerToken(); + + if (session.defaultPassword === false && session.password) { + passAdd2 = "&password=" + session.password; + return generateHash(session.password + session.salt, 4) + .then(async function (hash) { + passAdd = "&hash=" + hash; + await createRoomCallback(passAdd, passAdd2); + }) + .catch(errorlog); + } else if (session.defaultPassword === false && session.password === false) { + passAdd = "&p=0"; + passAdd2 = "&p=0"; + await createRoomCallback(passAdd, passAdd2); + } else { + await createRoomCallback(passAdd, passAdd2); + } +} + +function copyVideoFrameToClipboard(videoElement, e = false) { + try { + var canvas = document.createElement("canvas"); + + canvas.width = videoElement.videoWidth; + canvas.height = videoElement.videoHeight; + + var ctx = canvas.getContext("2d"); + ctx.drawImage(videoElement, 0, 0); + + var img = new Image(); + img.src = canvas.toDataURL(); + + canvas.toBlob(function (blob) { + navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]); + }, "image/png"); + + popupMessage(e, "Frame copied to clipboard as as PNG Image"); + } catch (e) { + errorlog(e); + } +} + +function saveVideoFrameToDisk(videoElement, e = false, filename = false) { + try { + var canvas = document.createElement("canvas"); + + canvas.width = videoElement.videoWidth; + canvas.height = videoElement.videoHeight; + + var ctx = canvas.getContext("2d"); + ctx.drawImage(videoElement, 0, 0); + + var img = new Image(); + img.src = canvas.toDataURL(); + + canvas.toBlob(function (blob) { + var link = document.createElement("a"); + if (filename) { + link.download = filename; + } else if (e) { + link.download = (videoElement.id || "video") + "_" + parseInt(performance.now()) + ".png"; + } else { + link.download = (videoElement.id || "video") + "_" + parseInt(Date.now()) + ".png"; + } + link.href = URL.createObjectURL(blob); + link.click(); + URL.revokeObjectURL(link.href); + }, "image/png"); + if (e) { + popupMessage(e, "Saving current frame to disk"); + } + } catch (e) { + errorlog(e); + } +} + +function sendVideoFrameToIframe(videoElement, e = false, request = {}) { + try { + var canvas = document.createElement("canvas"); + + canvas.width = videoElement.videoWidth; + canvas.height = videoElement.videoHeight; + + var ctx = canvas.getContext("2d"); + ctx.drawImage(videoElement, 0, 0); + + var img = new Image(); + img.src = canvas.toDataURL(); + + canvas.toBlob(function (blob) { + var response = {}; + response.imageData = blob; + response.imageType = "png"; + if (request.streamID) { + response.streamID = request.streamID; + } + if (request.UUID) { + response.UUID = request.UUID; + } + if (request.cib) { + response.cib = request.cib; + } + if (videoElement.id) { + response.videoID = videoElement.id; + } + + pokeIframeAPI("image-frame-capture", response); + }, "image/png"); + } catch (e) { + errorlog(e); + } +} + +function isLivePeerConnection(pc) { + if (!pc) { + return false; + } + var state = pc.connectionState || pc.iceConnectionState || ""; + if (!state) { + return true; + } + state = state.toLowerCase(); + return !(state === "failed" || state === "disconnected" || state === "closed"); +} + +async function checkDirectorStreamID() { + if (session.directorStreamID) { + for (var UUID in session.rpcs) { + if (!isLivePeerConnection(session.rpcs[UUID])) { + continue; + } + if (session.rpcs[UUID].streamID) { + var hashedSID = await generateHash(session.rpcs[UUID].streamID); + if (hashedSID === session.directorStreamID) { + session.directorUUID = UUID; // main director + session.directorList = []; + session.directorList.push(UUID); // approved co/directors + session.directorUUID = UUID; + session.newMainDirectorSetup(); + return; + } + } + } + for (var UUID in session.pcs) { + if (!isLivePeerConnection(session.pcs[UUID])) { + continue; + } + if (session.pcs[UUID].streamID) { + var hashedSID = await generateHash(session.pcs[UUID].streamID); + if (hashedSID === session.directorStreamID) { + session.directorList = []; + session.directorList.push(UUID); + session.directorUUID = UUID; + session.newMainDirectorSetup(); + return; + } + } + } + if (session.streamID == session.directorStreamID) { + session.directorState = true; + session.directorUUID = false; + pokeAPI("director", true); + pokeIframeAPI("director", true); + warnlog("You are joining with a token, but are the director?"); + } + session.directorList = []; + } +} + +async function checkToken() { + // this lets us use a server+password validation method for the director. + if (!session.token) { + return; + } + if (!session.roomid) { + return; + } + if (session.mainDirectorPassword) { + return; + } + + try { + var request = new XMLHttpRequest(); + + var hashedRoom = session.roomid; + if (session.password) { + hashedRoom += session.password; + } + hashedRoom += "i^4&u#Fz5Eu#MsK^chF5*XAEYi1g"; + hashedRoom = await generateHash(hashedRoom); + hashedRoom = hashedRoom.slice(0, 50); + + request.open("GET", "https://tokens.vdo.ninja/?token=" + session.token + "&room=" + hashedRoom, false); + request.send(null); + + if (request.status === 200) { + try { + var result = JSON.parse(request.responseText); + if ("UUID" in result) { + session.directorUUID = result.UUID; + session.directorList = []; + session.directorList.push(session.directorUUID); + session.directorStreamID = false; + session.newMainDirectorSetup(); + } else if ("streamID" in result) { + session.directorStreamID = result.streamID; + checkDirectorStreamID(); + } + } catch (e) { + session.directorUUID = false; + session.directorStreamID = false; + session.directorList = []; + errorlog(e); + } + } else { + session.directorUUID = false; + session.directorStreamID = false; + session.directorList = []; + errorlog("Didn't get a token response"); + } + } catch (e) { + errorlog(e); + } +} + +async function registerToken() { + // this lets us use a server+password validation method for the director. + if (!session.roomid) { + return; + } + if (!session.streamID) { + return; + } + if (!session.mainDirectorPassword) { + return; + } + + var longToken = session.mainDirectorPassword + "3wJVW^5qYU4DxGi6VhxN6RF04Q%$"; // this lets us use the same token across multiple rooms + var hashedToken = await generateHash(longToken); // keep it anonymous + hashedToken = hashedToken.slice(0, 50); + + var hashedRoom = session.roomid; + if (session.password) { + hashedRoom += session.password; + } + hashedRoom += "i^4&u#Fz5Eu#MsK^chF5*XAEYi1g"; + hashedRoom = await generateHash(hashedRoom); + hashedRoom = hashedRoom.slice(0, 50); + + var data2send = {}; + var hashedSID = await generateHash(session.streamID); + data2send.streamID = hashedSID; // not sure if there's a way around this. + data2send = JSON.stringify(data2send); + + var request = new XMLHttpRequest(); + request.open("POST", "https://tokens.vdo.ninja/?token=" + hashedToken + "&room=" + hashedRoom, false); + console.log("https://tokens.vdo.ninja/?token=" + hashedToken + "&room=" + hashedRoom); + request.send(data2send); + + if (request.status === 200) { + try { + if (request.responseText && request.responseText.length === 16) { + session.token = request.responseText; + console.log("share token: " + session.token); + session.directorState = true; + pokeAPI("director", true); + pokeIframeAPI("director", true); + } + } catch (e) { + session.directorState = false; + pokeAPI("director", false); + pokeIframeAPI("director", false); + } + } else { + session.directorState = false; + pokeAPI("director", false); + pokeIframeAPI("director", false); + } +} + +function hideDirectorinvites(ele, skip = true) { + if (getById("directorLinks2").style.display == "none") { + ele.innerHTML = ' LINKS (GUEST INVITES & SCENES)'; + getById("directorLinks2").style.display = "inline-block"; + getById("customizeLinks").classList.remove("hidden"); + } else { + ele.innerHTML = ' LINKS (GUEST INVITES & SCENES)'; + getById("directorLinks2").style.display = "none"; + getById("help_directors_room").style.display = "none"; + getById("roomnotes2").style.display = "none"; + getById("customizeLinks").classList.add("hidden"); + } + if (getById("directorLinks1").style.display == "none") { + getById("directorLinks1").style.display = "inline-block"; + getById("customizeLinks").classList.remove("hidden"); + } else { + getById("directorLinks1").style.display = "none"; + getById("help_directors_room").style.display = "none"; + getById("roomnotes2").style.display = "none"; + getById("customizeLinks").classList.add("hidden"); + } + if (skip) { + saveDirectorSettings(); + } +} + +function toggleCoDirector_changeurl(ele) { + session.codirector_changeURL = ele.checked; // doesn't do anything yet though. +} + +function toggleCoDirector_transfer(ele) { + session.codirector_transfer = ele.checked; +} + +function updateConfirmAlt(context, inputText) { + try { + if (!context) { return; } + var ctx = ("" + context).replace(/["<>]/g, ""); + var modal = document.querySelector('.promptModal[data-context="' + ctx + '"]'); + if (!modal) { return; } + var text = "" + ("" + inputText).replace("\n", "
    ") + ""; + text = text.replace(/\n/g, "
    "); + var msg = modal.querySelector('.promptModalMessage'); + if (msg) { msg.innerHTML = text; } + } catch (e) { /* noop */ } +} + +function toggleCoDirector_approve(ele) { + // UI label: "Allow co-directors to approve held guests" + // Checked means approvals allowed; unchecked means disabled + return; +} + +// Route approvals are default; no UI toggle needed anymore. + +function toggleApprovalPopup(ele) { + session.approval_popup = ele.checked; + try { + var token = ""; + if (session.token) { token += "&token=" + session.token; } + var url = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token; + if (session.approval_popup) { url += "&approvepopup"; } + try { console.log("[flags] toggled approval_popup=" + session.approval_popup + "; co-director invite=" + url); } catch (e) { } + if (session.password !== session.sitePassword) { + if (session.password === false) { url += "&password=false"; } + else { url += "&password=" + session.password; } + } + if (getById("codirectorSettings_invite")) { + getById("codirectorSettings_invite").value = url; + } + } catch (e) { /* noop */ } +} + +async function toggleCoDirector(ele) { + //session.coDirectorAllowed = ele.checked; + if (!ele.checked) { + getById("codirectorSettings").style.display = "none"; + return; + } + if (!session.directorPassword) { + session.directorPassword = await promptAlt(getTranslation("enter-new-codirector-password"), false); + if (!session.directorPassword) { + session.directorPassword = false; + ele.checked = false; + return; + } + session.directorPassword = sanitizePassword(session.directorPassword); + } + updateURL("codirector=" + session.directorPassword, true, false); + getById("coDirectorEnableSpan").style.display = "none"; + + await generateHash(session.directorPassword + session.salt + "abc123", 12) + .then(function (hash) { + // million to one error. + log("dir room hash is " + hash); + session.directorHash = hash; + return; + }) + .catch(errorlog); + + if (session.codirector_transfer) { + getById("codirectorSettings_transfer").checked = true; + } else { + getById("codirectorSettings_transfer").checked = false; + } + if (session.codirector_changeURL) { + getById("codirectorSettings_changeurl").checked = true; + } else { + getById(codirectorSettings_changeurl).checked = false; + } + + var token = ""; + if (session.token) { + token += "&token=" + session.token; + } + + getById("codirectorSettings_invite").value = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token; + if (session.approval_popup) { + getById("codirectorSettings_invite").value += "&approvepopup"; + } + if (session.password !== session.sitePassword) { + if (session.password === false) { + getById("codirectorSettings_invite").value += "&password=false"; + } else { + getById("codirectorSettings_invite").value += "&password=" + session.password; + } + } + + getById("codirectorSettings").style.display = "block"; +} + +function getParentHostname() { + const parentUrl = document.referrer; + if (parentUrl) { + const url = new URL(parentUrl); + return url.hostname; + } + return null; +} + +async function toggleWidgetURL(ele) { + if (ele.id === "widgetURL") { + ele = getById("widgetURCheck"); + } else if (!ele.checked) { + getById("widgetURL").classList.add("hidden"); + session.widget = false; + + var data = {}; + data.widgetSrc = false; + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowWidget === true) { + session.sendMessage(data, UUID); + } + } + + if (session.director) { + let widget = document.getElementById("widget"); + if (widget) { + getById("widget").remove(); + getById("directorlayout").classList.remove("widget"); + getById("directorlayout").classList.remove("left"); + } + } + pokeIframeAPI("widget-src", session.widget); + return; + } + var widget = await promptAlt(getTranslation("enter-url-for-widget"), false, false, session.widget); + if (widget !== null) { + session.widget = widget; + } + if (session.widget) { + getById("widgetURL").value = session.widget; + getById("widgetURL").classList.remove("hidden"); + updateMixer(); + } else { + session.widget = false; + getById("widgetURL").classList.add("hidden"); + ele.checked = false; + } + + var data = {}; + data.widgetSrc = session.widget; + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowWidget === true) { + session.sendMessage(data, UUID); + } + } + + if (session.director) { + let widget = document.getElementById("widget"); + if (!widget && session.widget && session.iFramesAllowed) { + widget = document.createElement("iframe"); + widget.id = "widget"; + widget = loadIframe(parseURL4Iframe(session.widget), widget); + if (widget) { + getById("directorlayout").classList.add("widget"); + + if (session.widgetleft) { + widget.classList.add("left"); + getById("directorlayout").classList.add("left"); + } + + log(widget.src); + document.body.appendChild(widget); + } + } else if (session.widget && widget && session.iFramesAllowed) { + loadIframe(parseURL4Iframe(session.widget), widget); + } else if (widget) { + getById("widget").remove(); + getById("directorlayout").classList.remove("widget"); + getById("directorlayout").classList.remove("left"); + } + } + + pokeIframeAPI("widget-src", session.widget); +} + +async function createRoomCallback(passAdd, passAdd2) { + if (session.meshcast) { + if (!session.cleanOutput && !session.cleanDirector) { + document.getElementById("meshcastMenu").classList.remove("hidden"); + } + } + + if (!session.switchMode) { + getById("directorlayout").classList.remove("hidden"); + getById("gridlayout").classList.add("hidden"); + } + + var broadcastFlag = getById("broadcastFlag"); + try { + if (broadcastFlag.checked) { + broadcastFlag = true; + } else { + broadcastFlag = false; + } + } catch (e) { + broadcastFlag = false; + } + + var broadcastString = ""; + if (broadcastFlag) { + broadcastString = "&broadcast"; + getById("broadcastSlider").checked = true; + //customizeLinks1 + //saveDirectorSettings + } + + var wss = ""; + if (session.wssSetViaUrl) { + if (session.customWSS && session.customWSS !== true) { + wss = "&pie=" + session.customWSS; + } else if (session.customWSS == true) { + wss = "&wss=" + session.wss; + } else { + wss = "&wss2=" + session.wss; + } + } + + var queue = ""; + if (session.queue) { + queue = "&queue"; + getById("directorLinks2").style.opacity = "0.2"; + getById("directorLinks2").style.pointerEvents = "none"; + getById("directorLinks2").style.cursor = "not-allowed"; + } + + var showdirectorFlag = getById("showdirectorFlag"); + try { + if (showdirectorFlag.checked) { + showdirectorFlag = true; + } else { + showdirectorFlag = false; + } + } catch (e) { + showdirectorFlag = false; + } + + if (showdirectorFlag) { + updateURL("showdirector", true, false); + session.showDirector = session.showDirector || true; + //getById("broadcastSlider").checked=true; + } + + var codecGroupFlag = getById("codecGroupFlag"); + + if (session.codecGroupFlag) { + codecGroupFlag = session.codecGroupFlag || ""; + } else if (codecGroupFlag) { + if (codecGroupFlag.value) { + if (codecGroupFlag.value === "vp9") { + codecGroupFlag = "&codec=vp9"; + getById("codech264toggle").disabled = true; + } else if (codecGroupFlag.value === "h264") { + codecGroupFlag = "&codec=h264"; + getById("codech264toggle").checked = true; + } else if (codecGroupFlag.value === "vp8") { + codecGroupFlag = "&codec=vp8"; + getById("codech264toggle").disabled = true; + } else if (codecGroupFlag.value === "av1") { + codecGroupFlag = "&codec=av1"; + getById("codech264toggle").disabled = true; + } else { + codecGroupFlag = ""; + } + } else { + codecGroupFlag = ""; + } + + session.codecGroupFlag = session.codecGroupFlag || codecGroupFlag || session.codecGroupFlag; + } + if (session.bitrateGroupFlag) { + codecGroupFlag += session.bitrateGroupFlag; + } + + stashRoomSession(broadcastFlag); + + formSubmitting = false; + try { + var m = getById("mainmenu"); + m.remove(); + document.querySelectorAll(".hidden2").forEach(ele2 => { + ele2.classList.remove("hidden2"); + }); + } catch (e) { } + + getById("head1").className = "hidden"; + getById("head2").className = "hidden"; + getById("head4").className = ""; + + try { + if (session.label === false) { + document.title = "Control Room"; + } + } catch (e) { + errorlog(e); + } + + session.director = true; + screensharesupport = false; + + if (session.meterStyle === false) { + session.meterStyle = 1; // director specific style + } + if (session.signalMeter === null) { + session.signalMeter = true; + } + if (session.batteryMeter === null) { + session.batteryMeter = true; + } + + if (session.directorPassword) { + getById("coDirectorEnable").checked = true; + getById("coDirectorEnableSpan").style.display = "none"; + + var token = ""; + if (session.token) { + token += "&token=" + session.token; + } + + getById("codirectorSettings_invite").value = "https://" + location.host + location.pathname + "?dir=" + session.roomid + "&codirector=" + session.directorPassword + token; + if (session.approval_popup) { + getById("codirectorSettings_invite").value += "&approvepopup"; + } + if (session.password !== session.sitePassword) { + if (session.password == false) { + getById("codirectorSettings_invite").value += "&password=false"; + } else { + getById("codirectorSettings_invite").value += "&password=" + session.password; + } + } + + if (session.codirector_transfer) { + getById("codirectorSettings_transfer").checked = true; + } else { + getById("codirectorSettings_transfer").checked = false; + } + if (session.codirector_changeURL) { + getById("codirectorSettings_changeurl").checked = true; + } else { + getById("codirectorSettings_changeurl").checked = false; + } + getById("codirectorSettings").style.display = "block"; + } + + window.onresize = updateMixer; + window.onorientationchange = function () { + if (Firefox) { + updateForceRotate(true); + } + setTimeout(async function () { + if (session.forceAspectRatio) { + await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + } + if (session.effect && session.effect === "7") { + digitalZoom(); + } + updateForceRotate(); + updateMixer(); + }, 200); + }; + getById("reshare").parentNode.removeChild(getById("reshare")); + + //getById("mutespeakerbutton").style.display = null; + if (session.speakerMuted_default === false) { + //session.speakerMuted = false; // the director will start with audio playback muted. + toggleSpeakerMute(true); // let it be what it is. + } else { + session.speakerMuted = true; // the director will start with audio playback muted. + toggleSpeakerMute(true); // okay since only run on start + } + + var token = ""; + if (session.token) { + token += "&token=" + session.token; + } + + // Add auth parameters if in auth mode + var authParams = ""; + if (session.authMode) { + authParams = "&auth=true"; + + // Create universal token for scene links if we're authenticated + if (session.authToken && !session.universalViewToken) { + vdoAuth.createUniversalToken().then(() => { + // Update all links once token is created + if (session.universalViewToken) { + // Update scene link with universal token + var sceneAuthParams = "&universaltoken=" + session.universalViewToken; + getById("director_block_3").dataset.raw = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; + getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; + getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; + + // Update all solo links + updateAllSoloLinks(); + } + }); + } + } + + getById("director_block_1").dataset.raw = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams; + getById("director_block_1").href = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams; + getById("director_block_1").innerText = "https://" + location.host + location.pathname + "?room=" + session.roomid + broadcastString + passAdd + wss + queue + token + authParams; + + // For scene links, use universal token if available + var sceneAuthParams = ""; + if (session.authMode && session.universalViewToken) { + sceneAuthParams = "&universaltoken=" + session.universalViewToken; + } else if (session.authMode) { + sceneAuthParams = authParams; + } + + getById("director_block_3").dataset.raw = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; + getById("director_block_3").href = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; + getById("director_block_3").innerText = "https://" + location.host + location.pathname + "?scene&room=" + session.roomid + codecGroupFlag + passAdd2 + wss + token + sceneAuthParams; + + if (session.cleanDirector == false && session.cleanOutput == false) { + getById("roomHeader").style.display = ""; + //getById("directorLinks").style.display = ""; + getById("directorLinks1").style.display = "inline-block"; + getById("directorLinks2").style.display = "inline-block"; + + getById("calendarButton").style.display = "inline-block"; + } else { + getById("guestFeeds").innerHTML = ""; + } + + getById("guestFeeds").style.display = ""; + + if (!session.cleanOutput) { + if (session.queue) { + getById("queuebutton").classList.remove("hidden"); + } + getById("chatbutton").classList.remove("hidden"); + getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. + getById("controlButtons").classList.remove("hidden"); + // getById("legal").classList.remove("hidden"); + getById("mutespeakerbutton").classList.remove("hidden"); + getById("websitesharebutton").classList.remove("hidden"); + //getById("screensharebutton").classList.remove("hidden"); + + if (session.totalRoomBitrate) { + getById("roomsettingsbutton").classList.remove("hidden"); + } + + if (!session.showDirector) { + // if null or false, we want to show the solo link, since the director won't have their control box. The director will be visible in their solo link + getById("miniPerformer").innerHTML = ''; + //miniTranslate(getById("miniPerformer")); + + // Use soloLinkGenerator to get proper auth parameters + var directorSoloLink = soloLinkGenerator(session.streamID, true); + getById("grabDirectorSoloLink").dataset.raw = directorSoloLink; + getById("grabDirectorSoloLink").href = directorSoloLink; + getById("grabDirectorSoloLink").innerText = directorSoloLink; + getById("grabDirectorSoloLinkParent").classList.remove("hidden"); + } else { + getById("miniPerformer").innerHTML = ''; + } + miniTranslate(getById("miniPerformer")); + + getById("miniPerformer").className = ""; + + var tabindex = 26; + if (session.rooms && session.rooms.length > 0) { + var container = getById("rooms"); + container.innerHTML += 'Arm Transfer: '; + session.rooms.forEach(function (r) { + // if(session.roomid == r) return; //don't include self + container.innerHTML += '"; + tabindex++; + }); + } + } else { + getById("miniPerformer").style.display = "none"; + getById("controlButtons").classList.add("hidden"); + // getById("legal").classList.add("hidden"); + } + + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("hidden"); + getById("controlButtons").classList.remove("hidden"); + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("hidden"); + } + + if (session.effect === false) { + session.effect = null; // so the director can see the effects + } + + getById("avatarDiv3").classList.remove("hidden"); // lets the director see the avatar option + + clearInterval(session.updateLocalStatsInterval); + session.updateLocalStatsInterval = setInterval(function () { + updateLocalStats(); + }, session.statsInterval); + + var directorWebsiteShare = getStorage("directorWebsiteShare"); // {"website":session.iframeSrc, "roomid":session.roomid} + + if (typeof directorWebsiteShare === "object" && directorWebsiteShare !== null && "website" in directorWebsiteShare) { + if (directorWebsiteShare.website == false) { + clearDirectorSettings(); + } else if (directorWebsiteShare.roomid && directorWebsiteShare.roomid == session.roomid) { + session.iframeSrc = directorWebsiteShare.website; + session.defaultIframeSrc = directorWebsiteShare.website; + + getById("websitesharebutton").classList.add("hidden"); + getById("websitesharebutton2").classList.remove("hidden"); + } + } + + session.group.forEach(group => { + // changeGroupDirectorAPI(group, state=null, update=true) + changeGroupDirectorAPI(group, true, false); // update the UI only + }); + + session.groupView.forEach(group => { + // changeGroupDirectorAPI(group, state=null, update=true) + changeGroupViewDirectorAPI(group, true); // update the UI only + }); + + if (session.showDirector) { + getById("highlightDirectorSpan").style.display = "none"; + getById("highlightDirectorSpan").remove(); + } else { + getById("highlightDirector").dataset.sid = session.streamID; + } + + setTimeout(() => { + loadDirectorSettings(); + if (broadcastFlag) { + saveDirectorSettings(); + } + }, 100); + + joinRoom(session.roomid); + + pokeIframeAPI("create-room", session.roomid); + + try { + if (!gotDevices2AlreadyRan && (iOS || iPad)) { + await enumerateDevices().then(gotDevices2); // this is needed for iOS; was previous set to timeout at 100ms, but would be useful everywhere I think. (Breaks director's auto start, so just iOS for now) + } + } catch (e) { + errorlog(e); + } + + if (session.autostart) { + setTimeout(function () { + press2talk(true); + }, 400); + } else { + session.seeding = true; + session.seedStream(); + } +} // createRoomCallback + +function handleRoomSelect(room) { + var elems = document.querySelectorAll(".btnArmTransferRoom"); + [].forEach.call(elems, function (el) { + el.classList.remove("selected"); + }); + if (previousRoom == room) { + previousRoom = ""; + armedTransfer = false; + stillNeedRoom = true; + } else { + previousRoom = room; + stillNeedRoom = false; + armedTransfer = true; + getById("roomselect_" + room).classList.add("selected"); + } +} + +function getDirectorSettings(scene = false) { + var settings = {}; + + var eles = document.querySelectorAll('[data-action-type="solo-video"]'); + settings.soloVideo = false; + var soloVideoMode = null; + for (var i = 0; i < eles.length; i++) { + if (eles[i].value == 1) { + warnlog(eles[i]); + if (eles[i].dataset.sid) { + if (eles[i].classList && eles[i].classList.contains("altpress")) { + soloVideoMode = "alt"; + } + settings.soloVideo = eles[i].dataset.sid; // who is solo, if someone is solo + } + } + } + if (soloVideoMode) { + settings.soloVideoMode = soloVideoMode; + } else { + delete settings.soloVideoMode; + } + if (scene) { + var eles = document.querySelectorAll('[data-action-type="addToScene"][data-scene="' + scene + '"'); + settings.scene = {}; + for (var i = 0; i < eles.length; i++) { + if (eles[i].value == 1) { + if (eles[i].dataset.sid) { + var msg = {}; + msg.scene = scene; + msg.action = "display"; + msg.value = eles[i].value; + msg.target = eles[i].dataset.sid; + + settings.scene[eles[i].dataset.sid] = msg; + } + } + } + } + + settings.showDirector = session.showDirector; + + settings.mute = {}; + var eles = document.querySelectorAll('[data-action-type="mute-scene"]'); + for (var i = 0; i < eles.length; i++) { + if (eles[i].value == 1) { + // if muted + if (eles[i].dataset.sid) { + var msg = {}; + msg.action = "mute"; + msg.scene = true; + msg.value = 1; + msg.target = eles[i].dataset.sid; + settings.mute[eles[i].dataset.sid] = msg; + } + } + } + return settings; +} + +function normalizeLayoutStateValue(state) { + if (typeof state === "undefined") { + return undefined; + } + if (state === null) { + return false; + } + if (state === true) { + return false; + } + if (state === false) { + return false; + } + if (typeof state === "number") { + return state ? state : false; + } + if (typeof state === "string") { + const normalized = state.trim().toLowerCase(); + if (!normalized) { + return false; + } + if (normalized === "false" || normalized === "off" || normalized === "auto" || normalized === "0") { + return false; + } + if (normalized === "true") { + return false; + } + } + return state; +} + +function isAutoLayoutState(state) { + const normalized = normalizeLayoutStateValue(state); + return normalized === false || typeof normalized === "undefined" || normalized === null; +} + +function requestInfocus(ele, evt = null, value = null) { + try { + var sid = ele.dataset.sid; + } catch (e) { + warnlog("no stream ID found; requestinfocus"); + var sid = false; + if (ele.id === "highlightDirector") { + if (session.streamID) { + sid = session.streamID; + } + } + } + + if (value !== null) { + if (value) { + ele.value == 0; // we will toggle it in a second anyways. + } else { + ele.value == 1; + } + } + + var special = false; + if (evt) { + special = evt.ctrlKey || evt.metaKey || false; + if (special) { + special = true; + } + } + + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + ele.classList.remove("altpress"); + var actionMsg = {}; + actionMsg.infocus = false; + //session.sendMessage(actionMsg); + } else { + var actionMsg = {}; + if (special) { + actionMsg.infocus2 = sid; + } else { + actionMsg.infocus = sid; + } + //session.sendMessage(actionMsg); + + var eles = document.querySelectorAll('[data-action-type="solo-video"]'); + for (var i = 0; i < eles.length; i++) { + log(eles); + eles[i].classList.remove("pressed"); + eles[i].ariaPressed = "false"; + eles[i].classList.remove("altpress"); + eles[i].value = 0; + } + ele.value = 1; + + if (special) { + ele.classList.add("altpress"); + } else { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } + if (ele.id !== "highlightDirector") { + getById("highlightDirector").checked = false; + } + } + + for (var uuid in session.pcs) { + var layoutState = session.pcs[uuid].layoutState; + if (!session.pcs[uuid].solo && isAutoLayoutState(layoutState)) { + // only issue highlight commands to non-solo links when the scene is auto mixing + session.sendMessage(actionMsg, uuid); + } + } + + syncDirectorState(ele); + + if (ele.value == 1) { + return true; + } else { + return false; + } +} + +var fixScrollReset = null; +var fixScrollResetValue = null; + +function requestAudioSettings(ele) { + var UUID = ele.dataset.UUID; + + try { + clearTimeout(fixScrollReset); + fixScrollResetValue = getById("directorlayout").scrollTop; + fixScrollReset = setTimeout( + function (scrollpos) { + fixScrollReset = null; + getById("directorlayout").scrollTop = scrollpos; + }, + 1000, + fixScrollResetValue + ); + + query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").value = 0; + query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").classList.remove("pressed"); + query("#container_" + UUID + " [data-action-type='advanced-camera-settings']").ariaPressed = "false"; + query("#container_" + UUID + " .advancedVideoSettings").classList.add("hidden"); + query("#container_" + UUID + " .advancedVideoSettings").innerHTML = ""; + } catch (e) { } + + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + query("#container_" + UUID + " .advancedAudioSettings").classList.add("hidden"); + query("#container_" + UUID + " .advancedAudioSettings").innerHTML = ""; + return false; + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + query("#container_" + UUID + " .advancedAudioSettings").innerHTML = ""; + var actionMsg = {}; + actionMsg.getAudioSettings = true; + session.sendRequest(actionMsg, UUID); + return true; + } +} + +function requestVideoSettings(ele) { + var UUID = ele.dataset.UUID; + + try { + clearTimeout(fixScrollReset); + fixScrollResetValue = getById("directorlayout").scrollTop; + fixScrollReset = setTimeout( + function (scrollpos) { + fixScrollReset = null; + getById("directorlayout").scrollTop = scrollpos; + }, + 1000, + fixScrollResetValue + ); + + query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").value = 0; + query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").classList.remove("pressed"); + query("#container_" + UUID + " [data-action-type='advanced-audio-settings']").ariaPressed = "false"; + query("#container_" + UUID + " .advancedAudioSettings").classList.add("hidden"); + query("#container_" + UUID + " .advancedAudioSettings").innerHTML = ""; + } catch (e) { } + + if (ele.value == 1) { + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + query("#container_" + UUID + " .advancedVideoSettings").classList.add("hidden"); + query("#container_" + UUID + " .advancedVideoSettings").innerHTML = ""; + return false; + } else { + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + query("#container_" + UUID + " .advancedVideoSettings").innerHTML = ""; + var actionMsg = {}; + actionMsg.getVideoSettings = true; + session.sendRequest(actionMsg, UUID); + return true; + } +} + +function combinedLayoutSimple(layout) { + var combined = {}; + Object.keys(layout).forEach(i => { + if (!layout[i]) { + return; + } + + if (i === "") { + layout[i].forEach(j => { + if (!j) { + return; + } + var streamID = null; + if ("slot" in j) { + try { + streamID = session.currentSlots[parseInt(j.slot) + 1]; + } catch (e) { + errorlog(e); + streamID = null; + } + } + if (!streamID) { + if (!combined[""]) { + combined[""] = []; + } + combined[""].push(j); + } else { + combined[streamID] = j; + } + }); + } else { + var streamID = null; + if ("slot" in layout[i]) { + try { + streamID = session.currentSlots[parseInt(layout[i].slot) + 1]; + } catch (e) { + errorlog(e); + streamID = null; + } + } + if (!streamID) { + if (!combined[""]) { + combined[""] = []; + } + combined[""].push(layout[i]); + } else { + combined[streamID] = layout[i]; + } + } + }); + return combined; +} + +async function createDirectorOnlyBox() { + var soloLink = soloLinkGenerator(session.streamID); + + if (document.getElementById("deleteme")) { + getById("deleteme").parentNode.removeChild(getById("deleteme")); + } + var controls = getById("controls_directors_blank").cloneNode(true); + controls.classList.remove("hidden"); + controls.id = "controls_director"; + + var container = document.createElement("div"); + container.className = "vidcon directorMargins"; + container.id = "container_director"; // needed to delete on user disconnect + container.setAttribute("aria-label", miscTranslations["your-camera"]); + container.setAttribute("role", "region"); + + var buttons = ""; + if (session.slotmode && session.showDirector) { + var biggestSlot = 0; + var slotDefault = null; + + // Check past slots first + if (session.streamID in session.pastSlots) { + slotDefault = session.pastSlots[session.streamID]; + } + + // Get all current slots from RPC state + var allSlots = []; + if (session.slotmode == 1) { + Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => { + if (currentSlot) { + if (parseInt(currentSlot) > biggestSlot) { + biggestSlot = parseInt(currentSlot); + } + if (slotDefault === parseInt(currentSlot)) { + slotDefault = null; + } + allSlots.push(parseInt(currentSlot)); + } + }); + biggestSlot += 1; + } else if (slotDefault !== null && session.slotmode == 2) { + // Check if slot is already in use - include director's stream + const slotInUse = Object.keys(session.rpcs).some(UUID => + getSlotState(UUID) === slotDefault + ) || getSlotState(session.streamID) === slotDefault || getSlotState(session.streamID + ":s") === slotDefault; + + if (slotInUse) { + slotDefault = null; // This slot is already in use + } + } + + // Determine final slot value + if (slotDefault !== null) { + biggestSlot = slotDefault; + } else if (session.slotmode == 1) { + var bestfree = 0; + for (var i = 1; i <= biggestSlot; i++) { + if (allSlots.includes(i)) { + continue; + } else { + bestfree = i; + break; + } + } + biggestSlot = bestfree; + } + + // Set slot name + var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; + var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : ""; + + // Build HTML with same structure + buttons += + `
    + +
    `; + + // Sync the initial state + syncSlotState(session.streamID, biggestSlot, false); // false since UI is being created here + } + buttons += + "
    \ +
    ID: " + + session.streamID + + "\ + \ + \ + " + + getTranslation("add-a-label") + + "\ +
    \ +
    "; + + container.innerHTML = buttons; + + var oldGroups = []; + document.querySelectorAll("#groups [data-action-type='toggle-group'][data-group]:not(.green)").forEach(ee => { + oldGroups.push(ee.dataset.group); + }); + getById("groups").remove(); + + if (session.hidesololinks == false) { + // won't be updating the solo link to a view-only one ever, since director is always expected to be in a room + controls.innerHTML += + "
    \ + " + + sanitizeChat(soloLink) + + "\ + \ +
    \ +
    "; + if (session.directorUUID) { + controls.innerHTML += "

    This is you, a co-director.
    You are also a performer.

    "; + } else { + controls.innerHTML += "

    This is you, the director.
    You are also a performer.

    "; + } + } + + controls.querySelectorAll("[data-action-type]").forEach(ele => { + // give action buttons some self-reference + ele.dataset.sid = session.streamID; + }); + + container.appendChild(controls); + + getById("guestFeeds").appendChild(container); + + Object.keys(session.sceneList).forEach((scene, index) => { + if (document.getElementById("container_director")) { + if (!getById("container_director").querySelectorAll('[data-scene="' + scene + '"]').length) { + var newScene = document.createElement("div"); + newScene.innerHTML = '"; + newScene.classList.add("customScene"); + //getById("container_director").appendChild(newScene); + + var added = false; + getById("container_director") + .querySelectorAll(".customScene>[data-scene]") + .forEach(ele => { + if (!added && ele.dataset.scene > scene + "") { + ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); + added = true; + } + }); + if (!added) { + getById("container_director").appendChild(newScene); + } + } + } + }); + + getById("groups").showDirector = true; + + session.group.forEach(group => { + // changeGroupDirectorAPI(group, state=null, update=true) + changeGroupDirectorAPI(group, true, false); // update the UI only / + }); + + oldGroups.forEach(group => { + // changeGroupDirectorAPI(group, state=null, update=true) + changeGroupDirectorAPI(group, false, false); // update the UI only / + }); + + var labelID = document.getElementById("label_director"); + + labelID.onclick = async function (ee) { + var oldlabel = ee.target.innerText; + if (session.label === false) { + oldlabel = ""; + } + window.focus(); + var newlabel = await promptAlt(getTranslation("enter-new-display-name"), false, false, oldlabel); + if (newlabel !== null) { + newlabel = newlabel.trim(); + if (newlabel === "") { + newlabel = false; + //ee.target.innerHTML = getTranslation("add-a-label"); + miniTranslate(ee.target, "add-a-label"); + ee.target.classList.add("addALabel"); + } else { + ee.target.innerText = newlabel; + ee.target.classList.remove("addALabel"); + } + session.label = newlabel; + var data = {}; + data.changeLabel = true; + data.value = session.label; + session.sendMessage(data); + stashRoomSession(); + } + }; + labelID.style.float = "left"; + labelID.style.top = "2px"; + labelID.style.marginLeft = "5px"; + labelID.style.position = "relative"; + labelID.style.cursor = "pointer"; + if (session.label) { + labelID.innerText = session.label; + } + pokeIframeAPI("control-box", true, true); + +} + +async function createDirectorScreenshareOnlyBox() { + // sstype=3 + var screenStreamID = session.streamID + ":s"; + var soloLink = soloLinkGenerator(screenStreamID); + + if (document.getElementById("deleteme")) { + getById("deleteme").parentNode.removeChild(getById("deleteme")); + } + var controls = getById("controls_directors_blank").cloneNode(true); + controls.classList.remove("hidden"); + controls.id = "controls_screen_director"; + + var container = document.createElement("div"); + container.className = "vidcon directorMargins"; + container.id = "container_screen_director"; // needed to delete on user disconnect + container.setAttribute("aria-label", miscTranslations["your-screenshare"]); + container.setAttribute("role", "region"); + + var buttons = ""; + if (session.slotmode) { + var biggestSlot = 0; + var slotDefault = null; + + if (screenStreamID in session.pastSlots) { + slotDefault = session.pastSlots[screenStreamID]; + } + + var allSlots = []; + if (session.slotmode == 1) { + Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => { + if (currentSlot) { + if (parseInt(currentSlot) > biggestSlot) { + biggestSlot = parseInt(currentSlot); + } + if (slotDefault === parseInt(currentSlot)) { + slotDefault = null; + } + allSlots.push(parseInt(currentSlot)); + } + }); + biggestSlot += 1; + } + + // Determine final slot value + if (slotDefault !== null) { + biggestSlot = slotDefault; + } else if (session.slotmode == 1) { + var bestfree = 0; + for (var i = 1; i <= biggestSlot; i++) { + if (allSlots.includes(i)) { + continue; + } else { + bestfree = i; + break; + } + } + biggestSlot = bestfree; + } + + var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; + var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : ""; + + buttons += + `
    + +
    `; + + // Sync the initial state + syncSlotState(screenStreamID, biggestSlot, false); // false since UI is being created here + } + buttons += + "
    \ +
    ID: " + + screenStreamID + + "\ + \ + \ + " + + getTranslation("add-a-label") + + "\ +
    \ +
    "; + + container.innerHTML = buttons; + + var oldGroups = []; + document.querySelectorAll("#groups [data-action-type='toggle-group'][data-group]:not(.green)").forEach(ee => { + oldGroups.push(ee.dataset.group); + }); + getById("groups").remove(); + + if (session.hidesololinks == false) { + // won't be updating the solo link to a view-only one ever, since director is always expected to be in a room + controls.innerHTML += + "
    \ + " + + sanitizeChat(soloLink) + + "\ + \ +
    \ +
    "; + if (session.directorUUID) { + controls.innerHTML += "

    This is you, a co-director.
    You are also a performer.

    "; + } else if (session.showDirector === false) { + try { + controls.querySelectorAll('[data-action-type="addToScene"]').forEach(ele => { + ele.classList.add("hidden"); + }); + } catch (e) { + errorlog(e); + } + controls.innerHTML += "

    This is your screen share
    It's *not* a performer.

    "; + } else { + controls.innerHTML += "

    This your screen share.
    It's also a performer.

    "; + } + } + + controls.querySelectorAll("[data-action-type]").forEach(ele => { + // give action buttons some self-reference + ele.dataset.sid = screenStreamID; + }); + + container.appendChild(controls); + + getById("guestFeeds").appendChild(container); + + Object.keys(session.sceneList).forEach((scene, index) => { + if (document.getElementById("container_screen_director")) { + if (!getById("container_screen_director").querySelectorAll('[data-scene="' + scene + '"]').length) { + var newScene = document.createElement("div"); + newScene.innerHTML = '"; + newScene.classList.add("customScene"); + //getById("container_screen_director").appendChild(newScene); + + var added = false; + getById("container_screen_director") + .querySelectorAll(".customScene>[data-scene]") + .forEach(ele => { + if (!added && ele.dataset.scene > scene + "") { + ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); + added = true; + } + }); + if (!added) { + getById("container_screen_director").appendChild(newScene); + } + } + } + }); + + getById("groups").showDirector = true; + + session.group.forEach(group => { + // changeGroupDirectorAPI(group, state=null, update=true) + changeGroupDirectorAPI(group, true, false); // update the UI only / + }); + + oldGroups.forEach(group => { + // changeGroupDirectorAPI(group, state=null, update=true) + changeGroupDirectorAPI(group, false, false); // update the UI only / + }); + + document.querySelectorAll("#container_screen_director #label_director").forEach(elex => { + elex.remove(); + }); + + pokeIframeAPI("control-box", true, true); + +} + +function shiftPC(ele, shift, director = false) { + if (director) { + var target = document.getElementById("container_director"); + } else { + var target = document.getElementById("container_" + ele.dataset.UUID); + } + + if (!target) { + return; + } + + target.shifted = true; + + var target2 = false; + + if (shift == 1) { + if (target.nextSibling) { + target2 = target.nextSibling; + target.parentNode.insertBefore(target.nextSibling, target); + } + } else { + if (target.previousSibling) { + target2 = target.previousSibling; + target.parentNode.insertBefore(target, target.previousSibling); + } + } + updateLockedElements(); + + if (session.api) { + var slots = {}; + var elements = getById("guestFeeds").children; + for (var i = 0; i < elements.length; i++) { + if (elements[i] === target) { + var tmp = target.querySelector("[data-sid]"); + if (tmp) { + var lock = target.querySelector("[data-locked]"); + if (lock) { + lock = parseInt(lock.dataset.locked); + } + tmp = tmp.dataset.sid; + slots[tmp] = lock || i + 1; + } + } else if (elements[i] === target2) { + var tmp2 = target2.querySelector("[data-sid]"); + if (tmp2) { + var lock = target2.querySelector("[data-locked]"); + if (lock) { + lock = parseInt(lock.dataset.locked); + } + tmp2 = tmp2.dataset.sid; + slots[tmp2] = lock || i + 1; + } + } + } + pokeAPI("positionChange", slots); + } +} + +function updateLockedElements() { + var eles = getById("guestFeeds").children; + for (var i = 0; i < eles.length; i++) { + try { + var UUID = eles[i].UUID; + var lock = document.getElementById("position_" + UUID).dataset.locked; + if (parseInt(lock)) { + lockPosition(document.getElementById("position_" + UUID), true); + } + } catch (e) { } + } +} + +function lockPosition(ele, apply = false) { + var UUID = ele.dataset.UUID; + if (apply) { + if (ele.dataset.locked && parseInt(ele.dataset.locked)) { + if (getById("guestFeeds")) { + var currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1; + ele.innerHTML = "#" + ele.dataset.locked + ""; + ele.parentNode.classList.add("locked"); + + while (currentPosition > parseInt(ele.dataset.locked)) { + var node = document.getElementById("container_" + UUID); + (parent = node.parentNode), (prev = node.previousSibling), (oldChild = parent.removeChild(node)); + parent.insertBefore(oldChild, prev); + currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1; + } + + while (currentPosition < parseInt(ele.dataset.locked) && getById("guestFeeds").children.length > currentPosition) { + var node = document.getElementById("container_" + UUID); + (parent = node.parentNode), (next = node.nextSibling), (oldChild = parent.removeChild(node)); + parent.insertBefore(node, next.nextSibling); + currentPosition = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1; + } + } + } else { + ele.dataset.locked = 0; + ele.innerHTML = ""; + ele.parentNode.classList.remove("locked"); + } + } else { + if (ele.dataset.locked && parseInt(ele.dataset.locked)) { + ele.dataset.locked = 0; + ele.innerHTML = ""; + ele.parentNode.classList.remove("locked"); + } else { + if (getById("guestFeeds")) { + ele.dataset.locked = Array.prototype.indexOf.call(getById("guestFeeds").children, document.getElementById("container_" + UUID)) + 1; + ele.innerHTML = "#" + ele.dataset.locked + ""; + ele.parentNode.classList.add("locked"); + } + } + } +} + +function allowDropSlot(event) { + event.preventDefault(); +} + +function dragSlot(event) { + log("drag"); + + var ele = event.target; + if (!ele.dataset.sid && ele.parentNode.dataset.sid) { + ele = ele.parentNode; + } + + event.dataTransfer.setDragImage(getById("dragImage"), 24, 24); + event.dataTransfer.setData("text", ele.dataset.sid); + + var eles = document.querySelectorAll(".slotsbar"); + for (var i = 0; i < eles.length; i++) { + if (eles[i].dataset.sid == ele.dataset.sid) { + continue; + } + eles[i].style.boxShadow = "0px 0px 8px 2px #FFF"; + } +} + +function dragendSlot(event) { + var eles = document.querySelectorAll(".slotsbar"); + for (var i = 0; i < eles.length; i++) { + eles[i].style.boxShadow = "unset"; + } + return true; +} + +function dropSlot(event) { + log("drop"); + event.preventDefault(); + event.stopPropagation(); + + // Get the dragged streamID + var SID = event.dataTransfer.getData("text"); + if (!SID) return; + + var origThing = document.querySelector("[data-sid='" + SID + "'][data-slot]"); + if (!origThing) return; + + // Get target streamID + var targetSID = event.target.dataset.sid || event.target.parentNode.dataset.sid; + if (!targetSID) return; + + var targetThing = document.querySelector("[data-sid='" + targetSID + "'][data-slot]"); + if (!targetThing) return; + + // Get original slots + const origSlot = parseInt(origThing.dataset.slot); + const targetSlot = parseInt(targetThing.dataset.slot); + + // Key fix: We need to swap the DOM elements *and* swap the session.currentSlots entries + // Save the original values + const tempStreamID = session.currentSlots[targetSlot]; // Save the target slot's original value + + // Update session.currentSlots (this is the crucial part) + session.currentSlots[targetSlot] = SID; + session.currentSlots[origSlot] = tempStreamID; + + // Update the data-sid attributes for the visual swap + targetThing.dataset.sid = SID; + origThing.dataset.sid = targetSID; + + // Update the UI text as well + const targetButton = targetThing.querySelector('button'); + const origButton = origThing.querySelector('button'); + + if (targetButton) { + targetButton.innerText = targetSlot ? `slot: ${targetSlot}` : 'unset'; + } + + if (origButton) { + origButton.innerText = origSlot ? `slot: ${origSlot}` : 'unset'; + } + + // Update past slots for future reference + session.pastSlots[SID] = targetSlot; // we don't need to run syncSlotState(), as this handles it + session.pastSlots[targetSID] = origSlot; + + // Tell any iframes about the swap + pokeIframeAPI("slot-updated", targetSlot, null, SID); + pokeIframeAPI("slot-updated", origSlot, null, targetSID); + + // Notify all peers of the update + broadcastSlotUpdate(); + + return false; +} + +function dragenterSlot(event) { + event.preventDefault(); + if (event.target.classList.contains("slotsbar")) { + event.target.style.border = "3px dotted black"; + } +} + +function dragleaveSlot(event) { + event.preventDefault(); + if (event.target.classList.contains("slotsbar")) { + event.target.style.border = ""; + } +} + +async function changeSlot(event, ele) { + var picker = document.getElementById("slotPicker"); + + if (picker) { + clearTimeout(modalTimeout); + + if (document.getElementById("modalBackdrop")) { + getById("alertModal").innerHTML = ""; // Delete modal + getById("alertModal").remove(); + getById("modalBackdrop").innerHTML = ""; // Delete modal + getById("modalBackdrop").remove(); + } + + zindex = 31 + document.querySelectorAll(".alertModal").length; + message = picker.innerHTML; + + modalTemplate = `
    +
    + × + ${message} +
    +
    +
    `; + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + + document.getElementById("modalBackdrop").addEventListener("click", closeModal); + + document + .getElementById("alertModalMessage") + .querySelectorAll("div[data-slot]") + .forEach(choice => { + choice.onclick = function () { + setSlot(ele, parseInt(this.dataset.slot)); + closeModal(); + }; + }); + + if (event) { + positionAlertModalNearEvent(document.getElementById("alertModal"), event); + } + + getById("alertModal").addEventListener("click", function (e) { + e.stopPropagation(); + return false; + }); + } else { + var slot = await promptAlt("Which slot to change to?"); + setSlot(ele, slot); + } +} + +function setSlot(ele, slot) { + log("setSlot()"); + getById("slotPicker").classList.add("hidden"); + if (slot !== null) { + try { + slot = parseInt(slot) || 0; + + // Find container with stream ID + const container = ele.closest('[data-sid]'); + const streamID = container ? container.dataset.sid : null; + + if (!streamID) { + return false; + } + + // Critical part: Check if the slot is already occupied and swap instead of replace + const existingStreamID = session.currentSlots[slot]; + if (existingStreamID && existingStreamID !== streamID) { + // Find the current slot of the stream we're moving + let currentSlot = null; + Object.entries(session.currentSlots).forEach(([key, value]) => { + if (value === streamID) { + currentSlot = parseInt(key); + } + }); + + // If the stream we're moving is already in a slot, update that slot's value + if (currentSlot !== null) { + // Perform the swap + session.currentSlots[slot] = streamID; + session.currentSlots[currentSlot] = existingStreamID; + + // Update the other element's UI + const otherSlotBar = document.querySelector(`[data-sid="${existingStreamID}"][data-slot]`); + if (otherSlotBar) { + otherSlotBar.dataset.slot = currentSlot; + applySlotColor(otherSlotBar, currentSlot); + const otherButton = otherSlotBar.querySelector('button'); + if (otherButton) { + otherButton.innerText = currentSlot ? `slot: ${currentSlot}` : 'unset'; + } + } + + // Update UI for the element we're setting + container.dataset.slot = slot; + applySlotColor(container, slot); + ele.innerText = slot ? `slot: ${slot}` : 'unset'; + + // Update pastSlots + session.pastSlots[streamID] = slot; + session.pastSlots[existingStreamID] = currentSlot; + + // Update iframes + pokeIframeAPI("slot-updated", slot, null, streamID); + pokeIframeAPI("slot-updated", currentSlot, null, existingStreamID); + } else { + // We're moving a stream that wasn't in a slot before + // First clear any current assignment for this stream + Object.entries(session.currentSlots).forEach(([key, value]) => { + if (value === streamID) { + delete session.currentSlots[key]; + } + }); + + // Then assign it to the new slot + session.currentSlots[slot] = streamID; + + // Update UI + container.dataset.slot = slot; + applySlotColor(container, slot); + ele.innerText = slot ? `slot: ${slot}` : 'unset'; + + // Update pastSlots + session.pastSlots[streamID] = slot; + + // Update iframe + pokeIframeAPI("slot-updated", slot, null, streamID); + } + } else { + // No conflict, just set the slot normally + // Clear any existing slot for this stream + Object.entries(session.currentSlots).forEach(([key, value]) => { + if (value === streamID) { + delete session.currentSlots[key]; + } + }); + + // Set the new slot + if (slot) { + session.currentSlots[slot] = streamID; + } + + // Update UI + container.dataset.slot = slot; + applySlotColor(container, slot); + ele.innerText = slot ? `slot: ${slot}` : 'unset'; + + // Update pastSlots + session.pastSlots[streamID] = slot; + + // Update iframe + pokeIframeAPI("slot-updated", slot, null, streamID); + } + + // Always update all peers + broadcastSlotUpdate(); + + } catch (e) { + errorlog(e); + return false; + } + return true; + } + return false; +} + +function swapNodes(n1, n2) { + log("swapping nodes"); + var p1 = n1.parentNode; + var p2 = n2.parentNode; + var i1, i2; + + if (!p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1)) return; + + for (var i = 0; i < p1.children.length; i++) { + if (p1.children[i].isEqualNode(n1)) { + i1 = i; + } + } + for (var i = 0; i < p2.children.length; i++) { + if (p2.children[i].isEqualNode(n2)) { + i2 = i; + } + } + + if (p1.isEqualNode(p2) && i1 < i2) { + i2++; + } + p1.insertBefore(n2, p1.children[i1]); + p2.insertBefore(n1, p2.children[i2]); +} + +function getCurrentSlot(streamID) { + const slotEntry = Object.entries(session.currentSlots).find(([_, sid]) => sid === streamID); + return slotEntry ? slotEntry[0] : false; +} + +function getSlotState(UUID) { + if (!UUID || !(UUID in session.rpcs)) return false; + return getCurrentSlot(session.rpcs[UUID].streamID); +} + +function combinedLayout(layout) { + if (!Array.isArray(layout)) return layout || {}; + + var combined = {}; + for (var i = 0; i < layout.length; i++) { + if (!layout[i] || !("slot" in layout[i])) { + if (!combined[""]) combined[""] = []; + combined[""].push(layout[i]); + continue; + } + + const slotNumber = parseInt(layout[i].slot || 0) + 1; + const streamID = session.currentSlots[slotNumber]; + + if (!streamID) { + if (!combined[""]) combined[""] = []; + combined[""].push(layout[i]); + continue; + } + + combined[streamID] = layout[i]; + } + return combined; +} + +function syncSlotState(streamID, slotValue = false, updateUI = true) { + // Clear any existing slots for this stream + Object.entries(session.currentSlots).forEach(([slot, sid]) => { + if (sid === streamID) delete session.currentSlots[slot]; + }); + + // Set new slot if one provided + if (slotValue) { + session.currentSlots[slotValue] = streamID; + } + + // Update UI if requested + if (updateUI) { + const slotsBar = document.querySelector(`[data-sid="${streamID}"][data-slot]`); + if (slotsBar) { + slotsBar.dataset.slot = slotValue; + applySlotColor(slotsBar, slotValue); + const slotButton = slotsBar.querySelector('button'); + if (slotButton) { + slotButton.innerText = slotValue ? `slot: ${slotValue}` : 'unset'; + } + } + } + + pokeIframeAPI("slot-updated", slotValue, null, streamID); // need to support self-director + session.pastSlots[streamID] = slotValue || 0; + + clearTimeout(session.slotBroadcastThrottle); + session.slotBroadcastThrottle = setTimeout(function () { broadcastSlotUpdate(); }, 10); + return true; +} +function broadcastSlotUpdate(UUID = false) { + try { + if (!session.slotmode || !session.director) { + return; + } + if (!UUID) { + if (session.slotBroadcastThrottle) { + clearTimeout(session.slotBroadcastThrottle); + session.slotBroadcastThrottle = null; + } + session.sendMessage({ slotsUpdate: session.currentSlots }); + } else { + session.sendMessage({ slotsUpdate: session.currentSlots }, UUID); + } + } catch (e) { + errorlog(e); + } +} +function updateSlotUI() { + // Update all slot UI elements based on the current state in session.currentSlots + Object.entries(session.currentSlots).forEach(([slot, streamID]) => { + const slotBar = document.querySelector(`[data-sid="${streamID}"][data-slot]`); + if (slotBar) { + slotBar.dataset.slot = slot; + applySlotColor(slotBar, slot); + const button = slotBar.querySelector('button'); + if (button) { + button.innerText = slot ? `slot: ${slot}` : 'unset'; + } + } + }); +} + +function createControlBox(UUID, soloLink, streamID, slot_init = false) { + if (document.getElementById("deleteme")) { + getById("deleteme").parentNode.removeChild(getById("deleteme")); + } + + // Remove any existing container with this UUID to prevent stacking + var existingContainer = document.getElementById("container_" + UUID); + if (existingContainer) { + existingContainer.parentNode.removeChild(existingContainer); + } + + var controls = getById("controls_blank").cloneNode(true); + controls.classList.remove("hidden"); + controls.id = "controls_" + UUID; + + var container = document.createElement("div"); + container.className = "vidcon directorMargins"; + container.id = "container_" + UUID; // needed to delete on user disconnect + container.UUID = UUID; + container.dataset.UUID = UUID; + container.dataset.sid = streamID; + + if (session.orderby) { + try { + var added = false; + for (var i = 0; i < getById("guestFeeds").children.length; i++) { + if (getById("guestFeeds").children[i].UUID && !getById("guestFeeds").children[i].shifted) { + if (getById("guestFeeds").children[i].UUID in session.rpcs) { + if (session.rpcs[getById("guestFeeds").children[i].UUID].streamID.toLowerCase() > streamID.toLowerCase()) { + getById("guestFeeds").insertBefore(container, getById("guestFeeds").children[i]); + added = true; + break; + } + } + } + } + if (!added) { + getById("guestFeeds").appendChild(container); + } + } catch (e) { + getById("guestFeeds").appendChild(container); + } + } else { + getById("guestFeeds").appendChild(container); + } + + //controls.innerHTML += ""; + if (session.rpcs[UUID].pseudoguest) { + controls.querySelectorAll("[data-action-type]").forEach(ele => { + ele.dataset.UUID = UUID; + ele.dataset.sid = streamID; + }); + return; + } + //controls.innerHTML += ""; + + if (!session.rpcs[UUID].voiceMeter) { + if (session.meterStyle == 1) { + // director specific style + session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate2").cloneNode(true); + } else { + session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate").cloneNode(true); + session.rpcs[UUID].voiceMeter.style.opacity = 0; + if (session.meterStyle == 2) { + session.rpcs[UUID].voiceMeter.classList.add("video-meter-2"); + session.rpcs[UUID].voiceMeter.classList.remove("video-meter"); + } else { + session.rpcs[UUID].voiceMeter.classList.add("video-meter-director"); + } + } + session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID; + session.rpcs[UUID].voiceMeter.dataset.level = 0; + session.rpcs[UUID].voiceMeter.classList.remove("hidden"); + } + + session.rpcs[UUID].remoteMuteElement = getById("muteStateTemplate").cloneNode(true); + session.rpcs[UUID].remoteMuteElement.id = ""; + session.rpcs[UUID].remoteMuteElement.style.top = "5px"; + session.rpcs[UUID].remoteMuteElement.style.right = "7px"; + + session.rpcs[UUID].remoteVideoMuteElement = getById("videoMuteStateTemplate").cloneNode(true); + session.rpcs[UUID].remoteVideoMuteElement.id = ""; + session.rpcs[UUID].remoteVideoMuteElement.style.top = "5px"; + session.rpcs[UUID].remoteVideoMuteElement.style.right = "28px"; + + session.rpcs[UUID].remoteRaisedHandElement = getById("raisedHandTemplate").cloneNode(true); + session.rpcs[UUID].remoteRaisedHandElement.id = ""; + session.rpcs[UUID].remoteRaisedHandElement.style.top = "5px"; + session.rpcs[UUID].remoteRaisedHandElement.style.right = "49px"; + + var handsID = "hands_" + UUID; + + // controls.innerHTML += "
    Links
    "; //Seems to create an empty div. + + if (session.hidesololinks == false) { + controls.innerHTML += + "
    \ + " + + sanitizeChat(soloLink) + + "\ + \ +
    "; + } + + controls.innerHTML += + '\ + '; + + controls.innerHTML += + '\ + '; + + controls.querySelectorAll("[data-action-type]").forEach(ele => { + // give action buttons some self-reference + ele.dataset.UUID = UUID; + ele.dataset.sid = streamID; + }); + + + var buttons = ""; + if (session.slotmode && slot_init !== 0) { // slot_init === 0 means guest explicitly opted out of slots + var biggestSlot = 0; + var slotDefault = null; + + // Handle initial slot value from initialization or past slots + if (slot_init && session.slotmode == 1) { + slotDefault = slot_init || null; + } + if (streamID in session.pastSlots) { + slotDefault = session.pastSlots[streamID]; + } + + // Get all current slots from RPC state + var allSlots = []; + if (session.slotmode == 1) { + Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => { + if (currentSlot) { + if (parseInt(currentSlot) > biggestSlot) { + biggestSlot = parseInt(currentSlot); + } + if (slotDefault === parseInt(currentSlot)) { + slotDefault = null; + } + allSlots.push(parseInt(currentSlot)); + } + }); + biggestSlot += 1; + } else if (slotDefault !== null && session.slotmode == 2) { + // Check if slot is already in use by any remote participant + const remoteSlotInUse = Object.keys(session.rpcs).some(UUID => + getSlotState(UUID) === slotDefault + ); + + // Check if slot is used by the director + const directorSlotInUse = Object.entries(session.currentSlots).some(([slot, sid]) => + parseInt(slot) === slotDefault && (sid === session.streamID || sid === session.streamID + ":s") + ); + + if (remoteSlotInUse || directorSlotInUse) { + slotDefault = null; // This slot is already in use + } + } + + // Determine final slot value + if (slotDefault !== null) { + // the default slot is available + biggestSlot = slotDefault; + } else if (slot_init && session.slotmode == 1) { + // was manually set, so can't be something else but 0 + biggestSlot = 0; + } else if (session.slotmode == 1) { + var bestfree = 0; + for (var i = 1; i <= biggestSlot; i++) { + if (allSlots.includes(i)) { + continue; + } else { + bestfree = i; + break; + } + } + biggestSlot = bestfree; + } + + // Set slot name + var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; + var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : ""; + + buttons += + `
    + +
    `; + + // Sync the initial state + syncSlotState(streamID, biggestSlot, false); // false since UI is being created here + } + buttons += + "
    ID: " + + streamID + + "\ + \ + \ + \ +
    "; + + container.innerHTML = buttons; + updateLockedElements(); + + var videoContainerControlBox = document.createElement("div"); + videoContainerControlBox.className = "controlVideoBox"; + container.containerControlBox = videoContainerControlBox; + container.appendChild(videoContainerControlBox); + + var videoContainer = document.createElement("div"); + videoContainer.id = "videoContainer_" + UUID; // needed to delete on user disconnect + videoContainer.style.margin = "0"; + videoContainer.style.position = "relative"; + videoContainer.style.minHeight = "30px"; + + videoContainerControlBox.appendChild(videoContainer); + + if (session.signalMeter) { + if (!session.rpcs[UUID].signalMeter) { + session.rpcs[UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true); + session.rpcs[UUID].signalMeter.id = "signalMeter_" + UUID; + session.rpcs[UUID].signalMeter.dataset.level = 0; + session.rpcs[UUID].signalMeter.classList.remove("hidden"); + session.rpcs[UUID].signalMeter.dataset.UUID = UUID; + session.rpcs[UUID].signalMeter.title = getTranslation("signal-meter"); + + if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.cpuLimited) { + // was quality_limitation_reason + session.rpcs[UUID].signalMeter.dataset.cpu = "1"; + } + + if (session.statsMenu !== false) { + session.rpcs[UUID].signalMeter.addEventListener("click", function (e) { + // show stats of video if double clicked + log("clicked signal meter"); + try { + e.preventDefault(); + if (session.statsMenu !== false) { + var uid = e.currentTarget.dataset.UUID; + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, uid); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + e.stopPropagation(); + return false; + } catch (e) { + errorlog(e); + } + }); + } + } + videoContainer.appendChild(session.rpcs[UUID].signalMeter); + } + + if (session.batteryMeter) { + if (!session.rpcs[UUID].batteryMeter) { + session.rpcs[UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true); + session.rpcs[UUID].batteryMeter.id = "batteryMeter_" + UUID; + batteryMeterInfoUpdate(UUID); + } + videoContainer.appendChild(session.rpcs[UUID].batteryMeter); + } + + if (session.showConnections) { + if (!session.rpcs[UUID].connectionDetails) { + createConnectionDetailsEle(UUID); + } + videoContainer.appendChild(session.rpcs[UUID].connectionDetails); + } + + var iframeDetails = document.createElement("div"); + iframeDetails.id = "iframeDetails_" + UUID; // needed to delete on user disconnect + iframeDetails.className = "iframeDetails hidden"; + + videoContainer.appendChild(session.rpcs[UUID].voiceMeter); + videoContainer.appendChild(session.rpcs[UUID].remoteMuteElement); + videoContainer.appendChild(session.rpcs[UUID].remoteVideoMuteElement); + videoContainer.appendChild(session.rpcs[UUID].remoteRaisedHandElement); + videoContainer.appendChild(iframeDetails); + container.appendChild(controls); + + session.group.forEach(group => { + var ele = controls.querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]'); + if (!ele) { + var newGroup = htmlToElement('"); + + var added = false; + container.querySelectorAll(".customGroup>[data-group]").forEach(ele => { + log(ele); + if (!added && ele.dataset.group > group + "") { + ele.parentNode.insertBefore(newGroup, ele); + added = true; + } + }); + if (!added) { + var newGroupCon = container.querySelector(".customGroup"); + if (!newGroupCon) { + newGroupCon = document.createElement("div"); + newGroupCon.classList.add("customGroup"); + container.appendChild(newGroupCon); + } + newGroupCon.appendChild(newGroup); + } + } + }); + + initSceneList(UUID); + syncSceneState(streamID); + syncOtherState(streamID); + + pokeIframeAPI("control-box", true, UUID); + + // Broadcast updated slots immediately so scenes with &viewslot update + if (session.slotmode && session.director) { + broadcastSlotUpdate(); + } +} + +function createControlBoxScreenshare(UUID, soloLink, streamID) { + if (document.getElementById("deleteme")) { + getById("deleteme").parentNode.removeChild(getById("deleteme")); + } + var controls = getById("controls_blank").cloneNode(true); + controls.classList.remove("hidden"); + controls.id = "controls_" + UUID; + + var container = document.createElement("div"); + container.className = "vidcon directorMargins"; + container.id = "container_" + UUID; // needed to delete on user disconnect + container.UUID = UUID; + container.dataset.UUID = UUID; + container.dataset.sid = streamID; + + if (session.orderby) { + try { + var added = false; + for (var i = 0; i < getById("guestFeeds").children.length; i++) { + if (getById("guestFeeds").children[i].UUID && !getById("guestFeeds").children[i].shifted) { + if (getById("guestFeeds").children[i].UUID in session.rpcs) { + if (session.rpcs[getById("guestFeeds").children[i].UUID].streamID.toLowerCase() > streamID.toLowerCase()) { + getById("guestFeeds").insertBefore(container, getById("guestFeeds").children[i]); + added = true; + break; + } + } + } + } + if (!added) { + getById("guestFeeds").appendChild(container); + } + } catch (e) { + getById("guestFeeds").appendChild(container); + } + } else { + getById("guestFeeds").appendChild(container); + } + + controls.querySelector(".controlsGrid").classList.add("notmain"); + + if (!session.rpcs[UUID].voiceMeter) { + if (session.meterStyle == 1) { + session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate2").cloneNode(true); + } else { + session.rpcs[UUID].voiceMeter = getById("voiceMeterTemplate").cloneNode(true); + session.rpcs[UUID].voiceMeter.style.opacity = 0; + if (session.meterStyle == 2) { + session.rpcs[UUID].voiceMeter.classList.add("video-meter-2"); + session.rpcs[UUID].voiceMeter.classList.remove("video-meter"); + } else { + session.rpcs[UUID].voiceMeter.classList.add("video-meter-director"); + } + } + session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID; + session.rpcs[UUID].voiceMeter.dataset.level = 0; + session.rpcs[UUID].voiceMeter.classList.remove("hidden"); + } + + session.rpcs[UUID].remoteMuteElement = getById("muteStateTemplate").cloneNode(true); + session.rpcs[UUID].remoteMuteElement.id = ""; + session.rpcs[UUID].remoteMuteElement.style.top = "5px"; + session.rpcs[UUID].remoteMuteElement.style.right = "7px"; + + session.rpcs[UUID].remoteVideoMuteElement = getById("videoMuteStateTemplate").cloneNode(true); + session.rpcs[UUID].remoteVideoMuteElement.id = ""; + session.rpcs[UUID].remoteVideoMuteElement.style.top = "5px"; + session.rpcs[UUID].remoteVideoMuteElement.style.right = "28px"; + + session.rpcs[UUID].remoteRaisedHandElement = getById("raisedHandTemplate").cloneNode(true); + session.rpcs[UUID].remoteRaisedHandElement.id = ""; + session.rpcs[UUID].remoteRaisedHandElement.style.top = "5px"; + session.rpcs[UUID].remoteRaisedHandElement.style.right = "49px"; + + var videoContainer = document.createElement("div"); + videoContainer.id = "videoContainer_" + UUID; // needed to delete on user disconnect + videoContainer.style.margin = "0"; + videoContainer.style.position = "relative"; + videoContainer.style.minHeight = "30px"; + + var iframeDetails = document.createElement("div"); + iframeDetails.id = "iframeDetails_" + UUID; // needed to delete on user disconnect + iframeDetails.className = "iframeDetails hidden"; + + //controls.innerHTML += ""; + //controls.innerHTML += ""; + + var handsID = "hands_" + UUID; + + controls.innerHTML += "
    "; + + if (session.hidesololinks == false) { + controls.innerHTML += + "
    \ + " + + sanitizeChat(soloLink) + + "\ + \ +
    "; + } + + controls.innerHTML += + '\ +
    '; + + controls.querySelectorAll("[data-action-type]").forEach(ele => { + // give action buttons some self-reference + ele.dataset.UUID = UUID; + ele.dataset.sid = streamID; + }); + + var buttons = ""; + if (session.slotmode) { + var biggestSlot = 0; + var slotDefault = null; + + // Check past slots first + if (streamID in session.pastSlots) { + slotDefault = session.pastSlots[streamID]; + } + + // Get all current slots from RPC state + var allSlots = []; + if (session.slotmode == 1) { + Object.entries(session.currentSlots).forEach(([currentSlot, sid]) => { + if (currentSlot) { + if (parseInt(currentSlot) > biggestSlot) { + biggestSlot = parseInt(currentSlot); + } + if (slotDefault === parseInt(currentSlot)) { + slotDefault = null; + } + allSlots.push(parseInt(currentSlot)); + } + }); + biggestSlot += 1; + } + + // Determine final slot value + if (slotDefault !== null) { + biggestSlot = slotDefault; + } else if (session.slotmode == 1) { + var bestfree = 0; + for (var i = 1; i <= biggestSlot; i++) { + if (allSlots.includes(i)) { + continue; + } else { + bestfree = i; + break; + } + } + biggestSlot = bestfree; + } + + // Set slot name and update past slots + var slotName = biggestSlot ? "slot: " + biggestSlot : "unset"; + var slotStyle = biggestSlot ? " style='background:" + getSlotColor(biggestSlot - 1) + ";'" : ""; + session.pastSlots[streamID] = biggestSlot; + + // Build HTML with same structure + buttons += + `
    + +
    `; + + // Sync the initial state + syncSlotState(streamID, biggestSlot, false); // false since UI is being created here + } + + buttons += + "
    ID: " + + streamID + + "\ + \ + \ + \ +
    "; + + container.innerHTML = buttons; + updateLockedElements(); + + var videoContainerControlBox = document.createElement("div"); + videoContainerControlBox.className = "controlVideoBox"; + container.containerControlBox = videoContainerControlBox; + + container.appendChild(videoContainerControlBox); + videoContainerControlBox.appendChild(videoContainer); + + if (session.signalMeter) { + if (!session.rpcs[UUID].signalMeter) { + session.rpcs[UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true); + session.rpcs[UUID].signalMeter.id = "signalMeter_" + UUID; + session.rpcs[UUID].signalMeter.dataset.level = 0; + session.rpcs[UUID].signalMeter.classList.remove("hidden"); + session.rpcs[UUID].signalMeter.dataset.UUID = UUID; + session.rpcs[UUID].signalMeter.title = getTranslation("signal-meter"); + + //if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.cpu_maxed){ + // session.rpcs[UUID].signalMeter.dataset.cpu = "1"; + //} + + session.rpcs[UUID].signalMeter.addEventListener("click", function (e) { + // show stats of video if double clicked + log("clicked signal meter"); + try { + e.preventDefault(); + if (session.statsMenu !== false) { + var uid = e.currentTarget.dataset.UUID; + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, uid); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + e.stopPropagation(); + return false; + } catch (e) { + errorlog(e); + } + }); + } + videoContainer.appendChild(session.rpcs[UUID].signalMeter); + } + + if (session.batteryMeter) { + //////// + if (!session.rpcs[UUID].batteryMeter) { + session.rpcs[UUID].batteryMeter = getById("batteryMeterTemplate").cloneNode(true); + session.rpcs[UUID].batteryMeter.id = "batteryMeter_" + UUID; + batteryMeterInfoUpdate(UUID); + } + videoContainer.appendChild(session.rpcs[UUID].batteryMeter); + } + + if (session.showConnections) { + if (!session.rpcs[UUID].connectionDetails) { + createConnectionDetailsEle(UUID); + } + videoContainer.appendChild(session.rpcs[UUID].connectionDetails); + } + + videoContainer.appendChild(session.rpcs[UUID].voiceMeter); + videoContainer.appendChild(session.rpcs[UUID].remoteMuteElement); + videoContainer.appendChild(session.rpcs[UUID].remoteVideoMuteElement); + videoContainer.appendChild(session.rpcs[UUID].remoteRaisedHandElement); + videoContainer.appendChild(iframeDetails); + videoContainer.appendChild(session.rpcs[UUID].videoElement); + container.appendChild(controls); + + session.group.forEach(group => { + var ele = controls.querySelector('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"][data-group="' + group + '"]'); + if (!ele) { + var newGroup = htmlToElement('"); + + var added = false; + container.querySelectorAll(".customGroup>[data-group]").forEach(ele => { + log(ele); + if (!added && ele.dataset.group > group + "") { + ele.parentNode.insertBefore(newGroup, ele); + added = true; + } + }); + if (!added) { + var newGroupCon = container.querySelector(".customGroup"); + if (!newGroupCon) { + newGroupCon = document.createElement("div"); + newGroupCon.classList.add("customGroup"); + container.appendChild(newGroupCon); + } + newGroupCon.appendChild(newGroup); + } + } + }); + + initSceneList(UUID); + pokeIframeAPI("control-box", true, UUID); +} + +function remoteRemoveQueue(ele) { + let ts = { ...transferSettings }; + ts.justResetting = true; + session.directMigrateIssue(session.roomid, ts, ele.dataset.UUID); + + ele.classList.add("hidden"); + try { + session.applyQueueStateChange(ele.dataset.UUID, false, "remote-remove-queue"); + } catch (e) { + errorlog(e); + } +} +function minimizeMe(button, director = false) { + var container = null; + if (!director) { + container = getById("container_" + button.dataset.UUID); + } else { + container = getById(director); + } + if (!container) { + return; + } + + var wasMinimized = container.classList.contains("minimized"); + if (!wasMinimized) { + var measuredWidth = container.offsetWidth || container.scrollWidth; + if (measuredWidth) { + container.dataset.minimizedWidth = measuredWidth; + } + } + + var isMinimized = container.classList.toggle("minimized"); + if (isMinimized) { + var storedWidth = parseFloat(container.dataset.minimizedWidth); + if (!storedWidth) { + storedWidth = container.scrollWidth || container.offsetWidth; + } + if (storedWidth) { + container.style.width = storedWidth + "px"; + container.style.minWidth = storedWidth + "px"; + } + } else { + container.style.removeProperty("width"); + container.style.removeProperty("min-width"); + var currentWidth = container.offsetWidth || container.scrollWidth; + if (currentWidth) { + container.dataset.minimizedWidth = currentWidth; + } else { + delete container.dataset.minimizedWidth; + } + } +} + +function blackoutMode() { + var overlay = document.getElementById("blackoutOverlay"); + if (!overlay) { + overlay = document.createElement('div'); + overlay.id = "blackoutOverlay"; + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: black; + color: white; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + cursor: pointer; + z-index: 9999; + `; + overlay.textContent = 'Click to exit black-out mode'; + document.body.appendChild(overlay); + } else { + overlay.classList.remove("hidden"); + } + + function exitBlackout() { + overlay.classList.add("hidden"); + overlay.removeEventListener('click', exitBlackout); + } + + overlay.addEventListener('click', exitBlackout); +} + +function cycleCameras() { + if (session.screenShareState) { + warnUser("Stop the screen-share first."); + return; + } + var videoSelect = document.querySelector("select#videoSource3").options; + // don't show flip option if only one camera. + // don't show if not a mobile device + // don't show if AD=0 + + var matched = false; + var maxIndex = parseInt(getById("flipcamerabutton").dataset.maxIndex) || parseInt(videoSelect.length); + if (maxIndex > parseInt(videoSelect.length)) { + maxIndex = parseInt(videoSelect.length); + } + + for (var i = 0; i < maxIndex; i++) { + var selOption = videoSelect[i]; + if (selOption.selected) { + matched = true; + } else if (matched) { + if (getById("flipcamerabutton").classList.contains("flip")) { + getById("flipcamerabutton").classList.remove("flip"); + getById("flipcamerabutton").classList.add("flip2"); + } else { + getById("flipcamerabutton").classList.remove("flip2"); + getById("flipcamerabutton").classList.add("flip"); + } + document.querySelector("select#videoSource3").value = selOption.value; + activatedPreview = false; + grabVideo(session.quality, "videosource", "select#videoSource3"); + return; + } + } + for (var i = 0; i < maxIndex; i++) { + var selOption = videoSelect[i]; + if (selOption.selected) { + return; // do nothing; the camera that is selected is the only camera available it seems. + } else { + if (getById("flipcamerabutton").classList.contains("flip")) { + getById("flipcamerabutton").classList.remove("flip"); + getById("flipcamerabutton").classList.add("flip2"); + } else { + getById("flipcamerabutton").classList.remove("flip2"); + getById("flipcamerabutton").classList.add("flip"); + } + document.querySelector("select#videoSource3").value = selOption.value; + activatedPreview = false; + grabVideo(session.quality, "videosource", "select#videoSource3"); + return; + } + } +} + +function addToGoogleCalendar() { + var title = "Live Stream"; + //var dates = "20180512T230000Z/20180513T030000Z"; + var linkout = getById("director_block_1").innerText; + var details = "Join the live stream as a performer at the following link:

    ===> " + linkout + "

    To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest

    Do not share the details of this invite with others, unless explicitly told to."; + details = details.split(" ").join("+"); + details = details.split("&").join("%26"); + var linkToOpen = "https://calendar.google.com/calendar/r/eventedit?text=" + title + "&details=" + details; + //https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134 + + window.open(linkToOpen); +} + +function addToOutlookCalendar() { + var title = "Live Stream"; + var linkout = getById("director_block_1").innerText; + var details = "Join the live stream as a performer at the following link:

    ===> " + linkout + "

    To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest

    Do not share the details of this invite with others, unless explicitly told to."; + details = details.split(" ").join("%20"); + details = details.split("&").join("%26"); + + var linkToOpen = "https://outlook.live.com/owa/?path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&subject=" + title + "&body=" + details; + //https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134 + + window.open(linkToOpen); +} + +function addToYahooCalendar() { + var title = "Live Stream"; + var linkout = getById("director_block_1").innerText; + var details = "Join the live stream as a performer at the following link:

    ===> " + linkout + "

    To test your connection and camera ahead of time, please visit https://vdo.ninja/speedtest

    Do not share the details of this invite with others, unless explicitly told to."; + details = details.split(" ").join("%20"); + details = details.split("&").join("%26"); + var linkToOpen = "https://calendar.yahoo.com?v60&title=" + title + "&desc=" + details; + //https://calendar.google.com/calendar/r/eventedit?text=My+Custom+Event&dates=20180512T230000Z/20180513T030000Z&details=For+details,+link+here:+https://example.com/tickets-43251101208&location=Garage+Boston+-+20+Linden+Street+-+Allston,+MA+02134 + + window.open(linkToOpen); +} + +function toggle(ele, tog = false, inline = true) { + var x = ele; + if (x.style.display === "none") { + if (inline) { + x.style.display = "inline-block"; + } else { + x.style.display = "block"; + } + } else { + x.style.display = "none"; + } + if (tog) { + if (tog.dataset.saved) { + tog.innerHTML = tog.dataset.saved; + delete tog.dataset.saved; + } else { + tog.dataset.saved = tog.innerHTML; + tog.innerHTML = "Hide This"; + } + } +} + +function toggleByDataset(filter) { + var elements = document.querySelectorAll('[data-cluster="' + filter + '"]'); // ie: .cluster1 + for (var i = 0; i < elements.length; i++) { + elements[i].classList.toggle("hidden"); + } +} + +var SelectedAudioOutputDevices = false; // session.sink +var SelectedAudioInputDevices = []; // .. +var SelectedVideoInputDevices = []; // .. + +async function enumerateDevices() { + log("enumerated start"); + + const timeout = new Promise((_, reject) => + setTimeout(() => reject(function () { + if (!session.cleanOutput) { + warnUser("The browser has not responded to our request to list available media devices.\n\nPossible solutions:\n\n- Restart the computer and try again\n- Try another browser\n- Remove or uninstall devices that are not needed\n- Uninstall and reinstall your browser"); + } + new Error("Device enumeration timed out.\n\nThe browser has not responded to our request to list available media devices.\n\nPossible solutions:\n\n- Restart the computer and try again\n- Try another browser\n- Remove or uninstall devices that are not needed\n- Uninstall and reinstall your browser"); + }), 15000) + ); + + const enumeratePromise = new Promise(async (resolve, reject) => { + try { + if (typeof navigator.mediaDevices === "object" && typeof navigator.mediaDevices.enumerateDevices === "function") { + resolve(await navigator.mediaDevices.enumerateDevices()); + } else if (typeof navigator.enumerateDevices === "function") { + log("enumerated failed 1"); + resolve(await navigator.enumerateDevices()); + } else { + window.MediaStreamTrack.getSources(devices => { + resolve( + devices + .filter(device => { + return device.kind.toLowerCase() === "video" || device.kind.toLowerCase() === "videoinput"; + }) + .map(device => { + return { + deviceId: device.deviceId != null ? device.deviceId : "", + groupId: device.groupId, + kind: "videoinput", + label: device.label, + toJSON: /* istanbul ignore next */ function () { + return this; + } + }; + }) + ); + }); + } + } catch (e) { + errorlog(e); + if (!session.cleanOutput) { + if (location.protocol !== "https:") { + warnUser("Error listing the media devices.\n\nYour browser will not allow access to media devices without SSL enabled.\n\nPossible solutions include switching to https, accessing the site from http://localhost, or enabling the `unsafely-treat-insecure-origin-as-secure` browser switch."); + } else if ("isSecureContext" in window && window.isSecureContext === false) { + warnUser("Error listing the media devices.\n\nThe website may have assets loaded in an insecure context."); + } else { + warnUser("An unknown error occured while trying to list the media devices."); + } + } + reject(e); + } + }); + + return Promise.race([enumeratePromise, timeout]); +} + +function requestOutputAudioStream() { + try { + //warnlog("GET USER MEDIA"); + warnlog("navigator.mediaDevices.getUserMedia starting..."); + return navigator.mediaDevices + .getUserMedia({ + audio: true, + video: false + }) + .then(function (stream1) { + // Apple needs thi to happen before I can access EnumerateDevices. + log("get media sources; request audio stream"); + return enumerateDevices().then(function (deviceInfos) { + stream1.getTracks().forEach(function (track) { + // We don't want to keep it without audio; so we are going to try to add audio now. + track.stop(); // I need to do this after the enumeration step, else it breaks firefox's labels + }); + const audioOutputSelect = getById("outputSourceScreenshare"); + audioOutputSelect.remove(0); + audioOutputSelect.removeAttribute("onclick"); + + for (let i = 0; i !== deviceInfos.length; ++i) { + const deviceInfo = deviceInfos[i]; + if (deviceInfo == null) { + continue; + } + const option = document.createElement("option"); + option.value = deviceInfo.deviceId; + if (deviceInfo.kind === "audiooutput") { + const option = document.createElement("option"); + if (audioOutputSelect.length === 0) { + option.dataset.default = true; + } else { + option.dataset.default = false; + } + option.value = deviceInfo.deviceId || "default"; + if (option.value == session.sink) { + option.selected = "true"; + } + option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; + audioOutputSelect.appendChild(option); + } else { + log("Some other kind of source/device: ", deviceInfo); + } + } + }); + }); + } catch (e) { + if (!session.cleanOutput) { + if (window.isSecureContext) { + warnUser("An error has occured when trying to access the default audio device. The reason is not known."); + } else if (iOS || iPad) { + warnUser("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported."); + } else { + warnUser("Error accessing the default audio device.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia"); + } + } + } +} + +let selectedScreenShareAudioDevices = []; +async function requestAudioStream() { // for the screen share. + const deviceList = document.getElementById('audioDeviceList'); + const errorElement = document.getElementById('audioSelectError'); + const showDevicesButton = document.getElementById('showAudioDevices'); + + try { + // Request audio permission first + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: false + }); + + // Stop tracks after getting permission + stream.getTracks().forEach(track => track.stop()); + + // Enumerate devices + const devices = await navigator.mediaDevices.enumerateDevices(); + const audioInputs = devices.filter(device => device.kind === 'audioinput'); + + // Clear and show device list + deviceList.innerHTML = ''; + deviceList.style.display = 'block'; + showDevicesButton.style.display = 'none'; + + selectedScreenShareAudioDevices = []; + + // Create checkbox for each device + audioInputs.forEach(device => { + const deviceLabel = device.label || `Microphone ${device.deviceId.slice(0, 4)}`; + + const div = document.createElement('div'); + div.className = 'audio-device-item'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = device.deviceId; + checkbox.value = device.deviceId; + + // Check if device was previously selected + if (session.audioDevice && ((typeof session.audioDevice === 'object' && session.audioDevice.includes(device.deviceId)) || normalizeDeviceLabel(deviceLabel).includes(session.audioDevice))) { + checkbox.checked = true; + selectedScreenShareAudioDevices.push(device.deviceId); + } + + checkbox.addEventListener('change', function () { + // Update session.audioDevice array + // if (!session.audioDevice || typeof session.audioDevice !== 'object') { + // session.audioDevice = []; + // } + if (!selectedScreenShareAudioDevices || typeof selectedScreenShareAudioDevices !== 'object') { + selectedScreenShareAudioDevices = []; + } + + if (this.checked) { + // if (!session.audioDevice.includes(this.value)) { + // session.audioDevice.push(this.value); + // } + if (!selectedScreenShareAudioDevices.includes(this.value)) { + selectedScreenShareAudioDevices.push(this.value); + } + } else { + //session.audioDevice = session.audioDevice.filter(id => id !== this.value); + selectedScreenShareAudioDevices = selectedScreenShareAudioDevices.filter(id => id !== this.value); + } + }); + + const label = document.createElement('label'); + label.htmlFor = device.deviceId; + label.textContent = deviceLabel; + + div.appendChild(checkbox); + div.appendChild(label); + deviceList.appendChild(div); + }); + + errorElement.style.display = 'none'; + + } catch (e) { + let errorMessage = ''; + + if (!window.isSecureContext) { + errorMessage = 'This website must be loaded in a secure context (HTTPS) to access audio devices.'; + } else if (/ipad|iphone|ipod/.test(navigator.userAgent.toLowerCase())) { + errorMessage = 'iOS 13.4 or later is recommended for audio device access.'; + } else { + errorMessage = 'An error occurred while accessing audio devices.'; + } + + errorElement.textContent = errorMessage; + errorElement.style.display = 'block'; + } +} + +function saveSettings() { + if (session.store) { + try { + var tmp = {}; + if (SelectedAudioInputDevices) { + tmp.SelectedAudioInputDevices = SelectedAudioInputDevices.filter(n => n); + } + if (session.sink && session.sink != "default") { + tmp.SelectedAudioOutputDevices = session.sink; + } else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices != "default") { + tmp.SelectedAudioOutputDevices = SelectedAudioOutputDevices; + } + tmp.SelectedVideoInputDevices = SelectedVideoInputDevices; + setStorage("session_store", JSON.stringify(tmp)); + log("Saving settings"); + } catch (e) { + errorlog(e); + } + } +} + +function loadSettings() { + if (session.store) { + try { + session.store = getStorage("session_store"); + if (session.store) { + session.store = JSON.parse(session.store); + } else { + session.store = {}; + } + + if (session.store && session.store.SelectedAudioOutputDevices) { + if (typeof session.store.SelectedAudioOutputDevices == "string") { + SelectedAudioOutputDevices = session.store.SelectedAudioOutputDevices; + } else if (typeof session.store.SelectedAudioOutputDevices == "object") { + if (session.store.SelectedAudioOutputDevices.length) { + SelectedAudioOutputDevices = session.store.SelectedAudioOutputDevices[0]; + } + } + } + if (session.store && session.store.SelectedAudioInputDevices) { + session.store.SelectedAudioInputDevices = session.store.SelectedAudioInputDevices.filter(n => n); + SelectedAudioInputDevices = session.store.SelectedAudioInputDevices; + } + if (session.store && session.store.SelectedVideoInputDevices) { + SelectedVideoInputDevices = session.store.SelectedVideoInputDevices; + } + } catch (e) { } + } +} + +function normalizeDeviceLabel(deviceName) { + return String(deviceName).replace(/[\W]+/g, "_").toLowerCase(); +} + +// Conservative audio label normalizer to alias Windows "Default -" / "Communications -" prefixed devices +function normalizeAudioAliasLabel(label) { + try { + if (!label) return ""; + let s = String(label).trim(); + // Normalize case and whitespace + s = s.replace(/^\s+|\s+$/g, ""); + // Remove leading Default/Communications prefixes with common separators ("-", ":", em/en dashes) + // Keep the rest intact (do NOT strip digits or other differences) + s = s.replace(/^(?:Default|Communications)\s*[-:\u2013\u2014]?\s*/i, ""); + return s.toLowerCase(); + } catch (e) { + return String(label || "").toLowerCase(); + } +} + +function gotDevices(deviceInfos, miconly = false) { + log("got devices!1"); + log(deviceInfos); + + deviceInfos.sort((a, b) => { + // Put "default" devices first + if (a.deviceId.toLowerCase() === "default") return -1; + if (b.deviceId.toLowerCase() === "default") return 1; + + // Then sort by label if both exist + if (a.label && b.label) { + return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); + } + + return 0; + }); + + try { + if (Firefox && !FirefoxEnumerated) { + if (session.streamSrc && session.streamSrc.getTracks().length) { + FirefoxEnumerated = true; + } + } + + var option = document.createElement("input"); + option.type = "checkbox"; + option.value = "ZZZ"; + option.name = "multiselect1"; + option.id = "multiselect1"; + option.style.display = "none"; + option.checked = true; + + var label = document.createElement("label"); + label.for = option.name; + label.innerHTML = ' No Audio'; + + var listele = document.createElement("li"); + listele.appendChild(option); + listele.appendChild(label); + + const audioInputSelect = document.getElementById("audioSource") || document.getElementById("audioSource3"); + audioInputSelect.innerHTML = ""; + audioInputSelect.appendChild(listele); + + const audioOutputSelect = document.getElementById("outputSource") || document.getElementById("outputSource3"); + audioOutputSelect.innerHTML = ""; + + option.onchange = function (event) { + // make sure to clear 'no audio option' if anything else is selected + if (!getById("multiselect1").checked) { + getById("multiselect1").checked = true; + } else { + var list = audioInputSelect.querySelectorAll("li>input"); + for (var i = 0; i < list.length; i++) { + if (list[i].id !== "multiselect1") { + list[i].checked = false; + } + } + } + SelectedAudioInputDevices = [event.currentTarget.value]; + saveSettings(); + }; + + const multiselectTrigger = document.getElementById("multiselect-trigger") || document.getElementById("multiselect-trigger3"); + multiselectTrigger.dataset.state = "0"; + multiselectTrigger.classList.add("closed"); + multiselectTrigger.classList.remove("open"); + getById("chevarrow1").classList.add("bottom"); + + const videoSelect = document.getElementById("videoSourceSelect") || document.getElementById("videoSource3"); + const selectors = [videoSelect]; + + const values = selectors.map(select => select.value); + selectors.forEach(select => { + while (select.firstChild) { + select.removeChild(select.firstChild); + } + }); + + function comp(a, b) { + if (a.kind === "audioinput") { + return 0; + } else if (a.kind === "audiooutput") { + return 0; + } + const labelA = a.label.toUpperCase(); + const labelB = b.label.toUpperCase(); + if (labelA > labelB) { + return 1; + } else if (labelA < labelB) { + return -1; + } + return 0; + } + //deviceInfos.sort(comp); // I like this idea, but it messes with the defaults. I just don't know what it will do. + var deviceInfo; + + // This is to hide NDI from default device. NDI Tools fucks up. + var tmp = []; + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (!(deviceInfo.kind === "videoinput" && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek")))) { + tmp.push(deviceInfo); + } + } + + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (deviceInfo.kind === "videoinput" && (deviceInfo.label.toLowerCase().startsWith("ndi") || deviceInfo.label.toLowerCase().startsWith("newtek"))) { + tmp.push(deviceInfo); + log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); + } + } + deviceInfos = tmp; + + if (typeof session.audioDevice == "object") { + // this sorts according to users's manual selection + var matched1 = []; + var matched2 = []; + var notmatched = []; + for (let i = 0; i !== deviceInfos.length; ++i) { + if (deviceInfos[i].kind === "audioinput") { + var deviceMatched = false; + if (session.audioDevice.includes(deviceInfos[i].deviceId)) { + matched1.push(deviceInfos[i]); + deviceMatched = true; + } else if (session.audioDevice.includes(normalizeDeviceLabel(deviceInfos[i].label))) { + matched1.push(deviceInfos[i]); + deviceMatched = true; + } else { + for (var j = 0; j < session.audioDevice.length; j++) { + if (normalizeDeviceLabel(deviceInfos[i].label).includes(session.audioDevice[j])) { + matched2.push(deviceInfos[i]); + log("A DEVICE FOUND = " + deviceInfos[i].label); + deviceMatched = true; + break; + } + } + } + if (!deviceMatched) { + notmatched.push(deviceInfos[i]); + } + } else { + notmatched.push(deviceInfos[i]); + } + } + + matched2.sort((a, b) => { + if (a.label && b.label) { + if (a.label.length < b.label.length) { + return -1 + } + return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); + } + return 0; + }); + + var matched = matched1.concat(matched2); + deviceInfos = matched.concat(notmatched); + } else if (session.store && session.store.SelectedAudioInputDevices) { + var matched = []; + var notmatch = []; + for (let i = 0; i < deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (session.store.SelectedAudioInputDevices.includes(deviceInfo.deviceId)) { + matched.push(deviceInfo); + log("EXACT A DEVICE FOUND -- from saved session"); + } else { + notmatch.push(deviceInfo); + } + } + deviceInfos = matched.concat(notmatch); + } + + if (session.sink || SelectedAudioOutputDevices) { + // this sorts according to users's manual selection + var matched = []; + var notmatch = []; + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (deviceInfo.kind === "audiooutput" && deviceInfo.deviceId === session.sink) { + matched.push(deviceInfo); + } else if (!session.sink && deviceInfo.kind === "audiooutput" && deviceInfo.deviceId === SelectedAudioOutputDevices) { + matched.push(deviceInfo); + } else { + notmatch.push(deviceInfo); + } + } + deviceInfos = matched.concat(notmatch); + } + + if (session.videoDevice && session.videoDevice !== 1) { + var tmp = []; + var tmp2 = []; + var tmp3 = []; + var deviceIdMatch = false; + + // First pass - check for label matches + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).startsWith(session.videoDevice)) { + tmp.push(deviceInfo); + log("Starts With V DEVICE FOUND"); + } else if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice)) { + tmp2.push(deviceInfo); + log("Includes With V DEVICE FOUND"); + } else { + tmp3.push(deviceInfo); + } + } + + // If no label matches found, try device ID match + if (tmp.length === 0 && tmp2.length === 0) { + for (let i = 0; i < tmp3.length; ++i) { + deviceInfo = tmp3[i]; + if (deviceInfo.kind === "videoinput" && deviceInfo.deviceId === session.videoDevice) { + tmp.push(deviceInfo); + deviceIdMatch = true; + log("EXACT DEVICE ID MATCH FOUND"); + break; + } + } + } + + if (tmp2.length && !deviceIdMatch) { + tmp = tmp.concat(tmp2); + } + if (tmp3.length) { + tmp = tmp.concat(tmp3); + } + + deviceInfos = tmp; + log("VDEVICE:" + session.videoDevice); + log(deviceInfos); + } else if (session.videoDevice === false && session.facingMode) { + var tmp = []; + if (session.facingMode == "environment") { + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("back")) { + tmp.push(deviceInfo); + log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); + } else if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("rear")) { + tmp.push(deviceInfo); + log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); + } + } + } else if (session.facingMode == "user") { + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("front")) { + tmp.push(deviceInfo); + log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); + } + } + } + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (!(deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice))) { + if (deviceInfo.deviceId !== session.videoDevice) { + tmp.push(deviceInfo); + } + } + } + deviceInfos = tmp; + log("VDECICE:" + session.videoDevice); + log(deviceInfos); + } else if (session.store && session.store.SelectedVideoInputDevices && session.videoDevice === false) { + var matched = []; + var notmatch = []; + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (session.store.SelectedVideoInputDevices.includes(deviceInfo.deviceId)) { + matched.push(deviceInfo); + log("EXACT V DEVICE FOUND -- from saved session"); + } else { + notmatch.push(deviceInfo); + } + } + deviceInfos = matched.concat(notmatch); + delete session.store.SelectedVideoInputDevices; + } else if (session.mobile) { + var tmp = []; + + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes("front")) { + tmp.push(deviceInfo); + log("V DEVICE FOUND = " + normalizeDeviceLabel(deviceInfo.label)); + } + } + + for (let i = 0; i !== deviceInfos.length; ++i) { + deviceInfo = deviceInfos[i]; + if (!(deviceInfo.kind === "videoinput" && normalizeDeviceLabel(deviceInfo.label).includes(session.videoDevice))) { + if (deviceInfo.deviceId !== session.videoDevice) { + tmp.push(deviceInfo); + } + } + } + deviceInfos = tmp; + log("AUTO FRONT:" + session.videoDevice); + log(deviceInfos); + } + + if (session.audioDevice && typeof session.audioDevice == "object") { + var adMatch = [...session.audioDevice]; + } else if (session.store && session.store.SelectedAudioInputDevices && session.store.SelectedAudioInputDevices.length) { + var adMatch = [...session.store.SelectedAudioInputDevices]; + } else { + var adMatch = false; + } + + if (session.store && session.store.SelectedAudioInputDevices) { + delete session.store.SelectedAudioInputDevices; + } + + var counter = 1; + var addedDeviceIds = new Set(); // Track already added devices + for (let i = 0; i !== deviceInfos.length; ++i) { + var deviceInfo = deviceInfos[i]; + if (deviceInfo == null) { + continue; + } + + if (deviceInfo.kind === "audioinput") { + // Skip if this device was already added + if (addedDeviceIds.has(deviceInfo.deviceId)) { + log("Skipping duplicate audio device: " + deviceInfo.label); + continue; + } + addedDeviceIds.add(deviceInfo.deviceId); + option = document.createElement("input"); + option.type = "checkbox"; + counter++; + listele = document.createElement("li"); + listele.style.display = "none"; + + if (typeof adMatch == "object") { + for (var j = 0; j < adMatch.length; j++) { + if (!adMatch[j]) { + // skip, already matched + } else if (adMatch[j] == deviceInfo.deviceId) { + option.checked = true; + listele.style.display = "block"; + option.style.display = "none"; + getById("multiselect1").checked = false; + try { + getById("multiselect1").parentNode.style.display = "none"; + } catch (e) { } + adMatch[j] = null; + break; + } else if (normalizeDeviceLabel(deviceInfo.label).includes(adMatch[j])) { + option.checked = true; + listele.style.display = "block"; + option.style.display = "none"; + getById("multiselect1").checked = false; + try { + getById("multiselect1").parentNode.style.display = "none"; + } catch (e) { } + adMatch[j] = null; + break; + } + } + } + + if (typeof adMatch !== "object" && counter == 2) { + option.checked = true; + listele.style.display = "block"; + option.style.display = "none"; + getById("multiselect1").checked = false; + try { + getById("multiselect1").parentNode.style.display = "none"; + } catch (e) { } + } + + option.value = deviceInfo.deviceId || "default"; + option.name = "multiselect" + counter; + option.id = "multiselect" + counter; + option.label = deviceInfo.label; + + label = document.createElement("label"); + label.for = option.name; + + label.innerHTML = " " + (deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1)); + label.title = "Hold Ctrl to select multiple"; + + listele.appendChild(option); + listele.appendChild(label); + audioInputSelect.appendChild(listele); + + option.onchange = function (event) { + // make sure to clear 'no audio option' if anything else is selected + getById("multiselect1").checked = false; + log("UNCHECKED"); + if (!CtrlPressed) { + SelectedAudioInputDevices = []; + audioInputSelect.querySelectorAll("input[type='checkbox']").forEach(function (item) { + if (event.currentTarget.id !== item.id) { + item.checked = false; + } else { + item.checked = true; + SelectedAudioInputDevices = [event.currentTarget.value]; + } + }); + } else { + if (event.currentTarget.checked) { + if (!SelectedAudioInputDevices) { + SelectedAudioInputDevices = [event.currentTarget.value]; + } else if (!SelectedAudioInputDevices.includes(event.currentTarget.value)) { + SelectedAudioInputDevices.push(event.currentTarget.value); + } + } else if (event.currentTarget.value) { + while (SelectedAudioInputDevices.includes(event.currentTarget.value)) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(event.currentTarget.value), 1); + } + } + } + if (session.mobile && !(iOS || iPad) && event.currentTarget.label === "USB audio" && !session.cleanOutput) { + warnUser("Notice: USB audio devices may not work on all mobile devices.\n\nConsider using FireFox mobile instead, as it tends to work with USB audio devices more often."); + } + saveSettings(); + }; + + if (deviceInfo.label.includes("Yeti ")) { + if (!session.cleanOutput) { + //getById("audioTipContext1").innerHTML = getTranslation("blue-yeti-tip"); + miniTranslate(getById("audioTipContext1"), "blue-yeti-tip"); + getById("audioTip1").classList.remove("hidden"); + } + } + } else if (deviceInfo.kind === "videoinput") { + option = document.createElement("option"); + option.value = deviceInfo.deviceId || "default"; + option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; + videoSelect.appendChild(option); + } else if (deviceInfo.kind === "audiooutput") { + option = document.createElement("option"); + if (audioOutputSelect.length === 0) { + option.dataset.default = true; + } else { + option.dataset.default = false; + } + option.value = deviceInfo.deviceId || "default"; + if (option.value == session.sink) { + option.selected = "true"; + } else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices == option.value) { + option.selected = "true"; + } + option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; + audioOutputSelect.appendChild(option); + } else { + log("Some other kind of source/device: ", deviceInfo); + } + } + + if (Firefox && !session.mobile) { + var option = document.createElement("option"); + option.value = "others"; + option.text = getTranslation("show-more-options"); + audioOutputSelect.appendChild(option); + } + + if (audioOutputSelect.childNodes.length == 0) { + option = document.createElement("option"); + option.value = "default"; + option.text = getTranslation("system-default"); + audioOutputSelect.appendChild(option); + } + + // Add ASIO devices if available (Windows only via Electron Capture) + // Try sync first, then async for sandbox mode + addAsioDevicesToDropdown(audioInputSelect, counter); + + option = document.createElement("option"); + option.text = getTranslation("disable-video"); + option.value = "ZZZ"; + videoSelect.appendChild(option); // NO AUDIO OPTION + + if (miconly) { + option.selected = "true"; + } + + selectors.forEach((select, selectorIndex) => { + if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) { + select.value = values[selectorIndex]; + } + }); + } catch (e) { + errorlog(e); + } +} + +function getUserMediaVideoParams(resolutionFallbackLevel, isSafariBrowser) { + let constraints = {}; + + switch (resolutionFallbackLevel) { + case -1: + constraints = {}; + break; + case -2: + if (isSafariBrowser) { + constraints = { + width: { + min: 360, + ideal: 3840, + max: 3840 + }, + height: { + min: 360, + ideal: 2160, + max: 2160 + } + }; + } else if (Firefox) { + constraints = { + width: { + ideal: 3840 + }, + height: { + ideal: 2160 + } + }; + } else { + constraints = { + width: { + min: 720, + ideal: 3840, + max: 3840 + }, + height: { + min: 720, + ideal: 2160, + max: 2160 + } + }; + } + break; + case -3: + if (isSafariBrowser) { + constraints = { + width: { + min: 360, + ideal: 2560, + max: 1440 + }, + height: { + min: 360, + ideal: 1440, + max: 1440 + } + }; + } else if (Firefox) { + constraints = { + width: { + ideal: 2560 + }, + height: { + ideal: 1440 + } + }; + } else { + constraints = { + width: { + min: 720, + ideal: 2560, + max: 2560 + }, + height: { + min: 720, + ideal: 1440, + max: 1440 + } + }; + } + break; + case 0: + if (isSafariBrowser) { + constraints = { + width: { + min: 360, + ideal: 1920, + max: 1920 + }, + height: { + min: 360, + ideal: 1080, + max: 1080 + } + }; + } else if (Firefox) { + constraints = { + width: { + ideal: 1920 + }, + height: { + ideal: 1080 + } + }; + } else { + constraints = { + width: { + min: 720, + ideal: 1920, + max: 1920 + }, + height: { + min: 720, + ideal: 1080, + max: 1920 + } + }; + } + break; + case 1: + if (isSafariBrowser) { + constraints = { + width: { + min: 360, + ideal: 1280, + max: 1280 + }, + height: { + min: 360, + ideal: 720, + max: 720 + } + }; + } else if (Firefox) { + constraints = { + width: { + ideal: 1280 + }, + height: { + ideal: 720 + } + }; + } else { + constraints = { + width: { + min: 720, + ideal: 1280, + max: 1280 + }, + height: { + min: 720, + ideal: 720, + max: 1280 + } + }; + } + break; + case 2: + if (isSafariBrowser) { + constraints = { + width: { + min: 640 + }, + height: { + min: 360 + } + }; + } else if (Firefox) { + constraints = { + width: { + ideal: 640 + }, + height: { + ideal: 360 + } + }; + } else { + constraints = { + width: { + min: 240, + ideal: 640, + max: 1280 + }, + height: { + min: 240, + ideal: 360, + max: 1280 + } + }; + } + break; + case 3: + constraints = { + width: { + min: 360, + ideal: 1280, + max: 1440 + } + }; + break; + case 4: + if (isSafariBrowser) { + constraints = { + height: { + min: 360, + ideal: 720, + max: 960 + } + }; + } else { + constraints = { + height: { + ideal: 720, + max: 960 + } + }; + } + break; + case 5: + if (isSafariBrowser) { + constraints = { + width: { + min: 360, + ideal: 640, + max: 1440 + }, + height: { + min: 360, + ideal: 360, + max: 720 + } + }; + } else { + constraints = { + width: { + ideal: 640, + max: 1920 + }, + height: { + ideal: 360, + max: 1920 + } + }; // same as default, but I didn't want to mess with frameRates until I gave it all a try first + } + break; + case 6: + if (isSafariBrowser) { + constraints = {}; // iphone users probably don't need to wait any longer, so let them just get to it + } else { + constraints = { + width: { + min: 360, + ideal: 640, + max: 3840 + }, + height: { + min: 360, + ideal: 360, + max: 2160 + } + }; + } + break; + case 7: + constraints = { + // If the camera is recording in low-light, it may have a low frameRate. It coudl also be recording at a very high resolution. + width: { + min: 360, + ideal: 640 + }, + height: { + min: 360, + ideal: 360 + } + }; + break; + case 8: + constraints = { + width: { + min: 360 + }, + height: { + min: 360 + }, + frameRate: 10 + }; // same as default, but I didn't want to mess with frameRates until I gave it all a try first + break; + case 9: + constraints = { + frameRate: 0 + }; // Some Samsung Devices report they can only support a frameRate of 0. + break; + case 10: + constraints = {}; + break; + default: + constraints = {}; + break; + } + + return constraints; +} + +function addScreenDevices(device) { + if (device.kind == "audio") { + const audioInputSelect = getById("audioSource3"); + const listele = document.createElement("li"); + listele.style.display = "block"; + + const option = document.createElement("input"); + option.type = "checkbox"; + option.checked = true; + + if (getById("multiselect-trigger3").dataset.state == 0) { + option.style.display = "none"; + } + + option.value = device.id; + option.name = device.label; + option.dataset.type = "screen"; + option.label = device.label; + + const label = document.createElement("label"); + label.for = option.name; + label.innerHTML = " " + device.label; + listele.appendChild(option); + listele.appendChild(label); + + option.onchange = function (event) { + // make sure to clear 'no audio option' if anything else is selected + log("change 4644"); + if (!CtrlPressed) { + document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { + if (!item.value) { + return; + } + if (event.currentTarget.value !== item.value) { + // this shoulnd't happen, but if it does. + item.checked = false; + if (item.dataset.type == "screen") { + item.parentElement.parentElement.removeChild(item.parentElement); + } + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); + } + activatedPreview = false; + grabAudio("#audioSource3"); // exclude item.id + } else { + if (SelectedAudioInputDevices.indexOf(item.value) == -1) { + if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices.push(item.value); + } + item.checked = true; + activatedPreview = false; + grabAudio("#audioSource3", item.value); // exclude item.id. we will reconnect, even if already connected, as a way to 'reset' a device if it isn't working. + } + }); + } + saveSettings(); + event.stopPropagation(); + return false; + }; + audioInputSelect.appendChild(listele); + getById("audioSourceNoAudio2").checked = false; + } else if (device.kind == "video") { + const videoSelect = getById("videoSource3"); + //const selectors = [ videoSelect]; + //const values = selectors.map(select => select.value); + const option = document.createElement("option"); + option.value = device.id; + option.text = device.label; + option.selected = "true"; + option.label = device.label; + videoSelect.appendChild(option); + } +} + +var gotDevices2AlreadyRan = false; +function gotDevices2(deviceInfos) { + gotDevices2AlreadyRan = true; + log("got devices!2"); + log(deviceInfos); + getById("multiselect-trigger3").dataset.state = "0"; + getById("multiselect-trigger3").classList.add("closed"); + getById("multiselect-trigger3").classList.remove("open"); + getById("chevarrow2").classList.add("bottom"); + + if (!session.streamSrc) { + checkBasicStreamsExist(); + } + + var knownTrack = false; + + try { + const audioInputSelect = getById("audioSource3"); + const videoSelect = getById("videoSource3"); + const audioOutputSelect = getById("outputSource3"); + const selectors = [videoSelect]; + + // Build active audio deviceId and label sets to avoid duplicate selection + const activeAudioIds = new Set(); + const activeAudioLabels = new Set(); + const activeAudioNormLabels = new Set(); + try { + if (session.streamSrc) { + session.streamSrc.getAudioTracks().forEach(function (t) { + try { + if (t.label) { + activeAudioLabels.add(t.label); + activeAudioNormLabels.add(normalizeAudioAliasLabel(t.label)); + } + if (t.getSettings) { + const s = t.getSettings(); + if (s && s.deviceId) { + activeAudioIds.add(s.deviceId); + } + } + } catch (e) { } + }); + } + } catch (e) { } + + // Identify normalized labels that have non-default/communications entries + const nonDefaultNormLabelSet = new Set(); + try { + for (let i = 0; i !== deviceInfos.length; ++i) { + const d = deviceInfos[i]; + if (!d || d.kind !== "audioinput") continue; + const id = (d.deviceId || "").toLowerCase(); + if (id !== "default" && id !== "communications" && d.label) { + nonDefaultNormLabelSet.add(normalizeAudioAliasLabel(d.label)); + } + } + } catch (e) { } + + // Track which normalized labels we've already auto-checked to avoid duplicates + const checkedByNormLabel = new Set(); + // Track deviceIds we've already added to avoid duplicate entries from buggy drivers + const addedDeviceIds = new Set(); + + [audioInputSelect].forEach(select => { + while (select.firstChild) { + select.removeChild(select.firstChild); + } + }); + + const values = selectors.map(select => select.value); + selectors.forEach(select => { + while (select.firstChild) { + select.removeChild(select.firstChild); + } + }); + + [audioOutputSelect].forEach(select => { + while (select.firstChild) { + select.removeChild(select.firstChild); + } + }); + + var counter = 0; + for (let i = 0; i !== deviceInfos.length; ++i) { + const deviceInfo = deviceInfos[i]; + if (deviceInfo == null) { + continue; + } + + if (deviceInfo.kind === "audioinput") { + // Deduplicate by deviceId if possible (defensive against buggy drivers) + try { + if (deviceInfo.deviceId && addedDeviceIds.has(deviceInfo.deviceId)) { + log("Skipping duplicate audio device: " + deviceInfo.label); + continue; + } + if (deviceInfo.deviceId) { + addedDeviceIds.add(deviceInfo.deviceId); + } + } catch (e) { } + + var option = document.createElement("input"); + option.type = "checkbox"; + counter++; + var listele = document.createElement("li"); + listele.style.display = "none"; + + // Auto-check selection based on active track deviceId first, fall back to normalized label + try { + let shouldCheck = false; + const devIdLower = (deviceInfo.deviceId || "").toLowerCase(); + const normLabel = normalizeAudioAliasLabel(deviceInfo.label || ""); + if (activeAudioIds.size && deviceInfo.deviceId && activeAudioIds.has(deviceInfo.deviceId)) { + shouldCheck = true; + } else if (!activeAudioIds.size && deviceInfo.label && activeAudioNormLabels.has(normLabel)) { + // Prefer non-default entries when multiple share a label + const isDefaultish = (devIdLower === "default" || devIdLower === "communications"); + if (checkedByNormLabel.has(normLabel)) { + shouldCheck = false; + } else if (isDefaultish && nonDefaultNormLabelSet.has(normLabel)) { + shouldCheck = false; + } else { + shouldCheck = true; + } + } + if (shouldCheck) { + option.checked = true; + listele.style.display = "inherit"; + if (normLabel) { + checkedByNormLabel.add(normLabel); + } + } + } catch (e) { } + + option.style.display = "none"; + option.value = deviceInfo.deviceId || "default"; + option.name = "multiselecta" + counter; + option.id = "multiselecta" + counter; + option.dataset.label = deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1); + try { option.dataset.norm = normalizeAudioAliasLabel(option.dataset.label); } catch (e) { } + try { option.dataset.groupId = deviceInfo.groupId || ""; } catch (e) { } + + var label = document.createElement("label"); + label.for = option.name; + + label.innerHTML = " " + (deviceInfo.label || "microphone " + ((audioInputSelect.length || 0) + 1)); + label.title = "Hold Ctrl to select multiple"; + + listele.appendChild(option); + listele.appendChild(label); + audioInputSelect.appendChild(listele); + + option.onchange = function (event) { + // make sure to clear 'no audio option' if anything else is selected + log("change 4768"); + if (!CtrlPressed) { + document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { + if (event.currentTarget.value !== item.value) { + item.checked = false; + if (item.dataset.type == "screen") { + item.parentElement.parentElement.removeChild(item.parentElement); + } + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); + } + } else { + item.checked = true; + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { + if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices.push(event.currentTarget.value); + } + } + }); + } else { + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { + if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices.push(event.currentTarget.value); + } + getById("audioSourceNoAudio2").checked = false; + } + saveSettings(); + }; + } else if (deviceInfo.kind === "videoinput") { + var option = document.createElement("option"); + option.value = deviceInfo.deviceId || "default"; + option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; + try { + if (!knownTrack && session.canvasSource) { + session.canvasSource.srcObject.getVideoTracks().forEach(function (track) { + if (option.text == track.label) { + option.selected = "true"; + knownTrack = true; + } + }); + } + if (!knownTrack && session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(function (track) { + if (option.text == track.label) { + option.selected = "true"; + knownTrack = true; + } + }); + } + } catch (e) { + errorlog(e); + } + videoSelect.appendChild(option); + } else if (deviceInfo.kind === "audiooutput") { + var option = document.createElement("option"); + if (audioOutputSelect.length === 0) { + option.dataset.default = true; + } else { + option.dataset.default = false; + } + option.value = deviceInfo.deviceId || "default"; + if (option.value == session.sink) { + option.selected = "true"; + } else if (!session.sink && SelectedAudioOutputDevices && SelectedAudioOutputDevices == option.value) { + option.selected = "true"; + session.sink = option.value; // added 8-dec-22, as the director's saved mic wasn't applying otherwise. + } + option.text = deviceInfo.label || `Speaker ${audioOutputSelect.length + 1}`; + audioOutputSelect.appendChild(option); + } else { + log("Some other kind of source/device: ", deviceInfo); + } + } + + if (Firefox && !session.mobile) { + var option = document.createElement("option"); + option.value = "others"; + option.text = getTranslation("show-more-options"); + audioOutputSelect.appendChild(option); + } + + if (audioOutputSelect.childNodes.length == 0) { + var option = document.createElement("option"); + option.value = "default"; + option.text = getTranslation("system-default"); + audioOutputSelect.appendChild(option); + } + + // Add ASIO devices if available (Windows only via Electron Capture) + // Try sync first, then async for sandbox mode + addAsioDevicesToDropdown(audioInputSelect, counter); + if (videoSelect.childNodes.length <= 1) { + getById("flipcamerabutton").style.display = "none"; // don't show the camera cycle button + getById("flipcamerabutton").dataset.maxndex = videoSelect.childNodes.length; + } else { + getById("flipcamerabutton").style.display = "unset"; + getById("flipcamerabutton").dataset.maxIndex = videoSelect.childNodes.length; + } + + //////////// + session.streamSrc.getAudioTracks().forEach(function (track) { + // add active ScreenShare audio tracks to the list + log("Checking for screenshare audio"); + var matched = false; + for (var i = 0; i !== deviceInfos.length; ++i) { + var deviceInfo = deviceInfos[i]; + if (deviceInfo == null) { + continue; + } + log("---"); + if (track.label == deviceInfo.label) { + matched = true; + continue; + } + } + if (matched == false) { + // Not a gUM device + var listele = document.createElement("li"); + listele.style.display = "block"; + var option = document.createElement("input"); + option.type = "checkbox"; + option.value = track.id; + option.checked = true; + option.style.display = "none"; + option.name = track.label; + option.label = track.label; + option.dataset.type = "screen"; + var label = document.createElement("label"); + label.for = option.name; + label.innerHTML = " " + track.label; + listele.appendChild(option); + listele.appendChild(label); + option.onchange = function (event) { + // make sure to clear 'no audio option' if anything else is selected + log("change 4873"); + var trackid = null; + if (!CtrlPressed) { + document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { + if (event.currentTarget.value !== item.value) { + // this shoulnd't happen, but if it does. + item.checked = false; + if (item.dataset.type == "screen") { + item.parentElement.parentElement.removeChild(item.parentElement); + } + } else { + event.currentTarget.checked = true; + trackid = item.value; + } + }); + } else { + //getById("audioSourceNoAudio2").checked=false; + if (event.currentTarget.dataset.type == "screen") { + event.currentTarget.parentElement.parentElement.removeChild(event.currentTarget.parentElement); + } + } + activatedPreview = false; + grabAudio("#audioSource3", trackid); // exclude item.id. + event.stopPropagation(); + return false; + }; + audioInputSelect.appendChild(listele); + } + }); + + /////////// no video option + var optionss = false; + if (screensharesupport) { + optionss = document.createElement("option"); + optionss.text = "Screen Share (replace camera)"; + optionss.value = "XXX"; + videoSelect.appendChild(optionss); // NO AUDIO OPTION + } + + var option = document.createElement("option"); // no video + option.text = getTranslation("disable-video"); + option.value = "ZZZ"; + videoSelect.appendChild(option); + + if (session.streamSrc.getVideoTracks().length == 0) { + option.selected = "true"; + } else if (knownTrack == false) { + var option = document.createElement("option"); // no video + option.text = session.streamSrc.getVideoTracks()[0].label; + option.value = "YYY"; + videoSelect.appendChild(option); + option.selected = "true"; + } + + if (optionss) { + optionss.lastSelected = videoSelect.selectedIndex; + } + + videoSelect.onchange = function (event) { + try { + if (event.target.options[event.target.options.selectedIndex].value === "XXX") { + videoSelect.selectedIndex = event.target.options[event.target.options.selectedIndex].lastSelected; + if (session.screenShareState == false) { + toggleScreenShare(); + } else { + toggleScreenShare(true); + } + return; + } + } catch (e) { } + activatedPreview = false; + grabVideo(session.quality, "videosource", "select#videoSource3"); + + if (!getById("audioSource3").querySelectorAll("input[data-type='screen']").length) { + if (session.screenShareState) { + session.screenShareState = false; + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + notifyOfScreenShare(); + //session.refreshScale(); + } + getById("screensharebutton").classList.remove("green"); + getById("screensharebutton").ariaPressed = "false"; + } + }; + + ///////////// /// NO AUDIO appended option + + var option = document.createElement("input"); + option.type = "checkbox"; + option.value = "ZZZ"; + option.style.display = "none"; + option.id = "audioSourceNoAudio2"; + + var label = document.createElement("label"); + label.for = option.name; + label.innerHTML = " No Audio"; + var listele = document.createElement("li"); + + if (session.streamSrc.getAudioTracks().length == 0) { + option.checked = true; + } else { + listele.style.display = "none"; + option.checked = false; + } + option.onchange = function (event) { + // make sure to clear 'no audio option' if anything else is selected + log("change 4938"); + if (!CtrlPressed) { + document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { + if (event.currentTarget.value !== item.value) { + item.checked = false; + if (item.dataset.type == "screen") { + item.parentElement.parentElement.removeChild(item.parentElement); + } + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); + } + } else { + item.checked = true; + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { + if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices.push(event.currentTarget.value); + } + } + }); + } else { + document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { + if (event.currentTarget.value === item.value) { + event.currentTarget.checked = true; + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { + if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices.push(event.currentTarget.value); + } + } else { + item.checked = false; + if (item.dataset.type == "screen") { + item.parentElement.parentElement.removeChild(item.parentElement); + } + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); + } + } + }); + } + saveSettings(); + }; + listele.appendChild(option); + listele.appendChild(label); + audioInputSelect.appendChild(listele); + + //////////// + + //selectors.forEach((select, selectorIndex) => { + // if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) { + // select.value = values[selectorIndex]; + // } + //}); + + audioInputSelect.onchange = function () { + log("Audio OPTION HAS CHANGED? 2"); + activatedPreview = false; + setTimeout(function () { + grabAudio("#audioSource3"); + }, 10); + }; + + getById("refreshVideoButton").onclick = function () { + refreshVideoDevice(); + }; + + if (Firefox && !session.mobile && navigator.mediaDevices) { + audioOutputSelect.onclick = function () { + log("audioOutputSelect.onclick = function() {"); + if (audioOutputSelect.options[audioOutputSelect.selectedIndex].value === "others") { + log("Trying to increase the output device list"); + navigator.mediaDevices.selectAudioOutput().then(device => { + if (device.kind == "audiooutput") { + session.sink = device.deviceId; + try { + var matched = false; + audioOutputSelect.childNodes.forEach(ele => { + if (ele.value === device.deviceId) { + matched = true; + ele.selected = true; + } + }); + if (!matched) { + var option = document.createElement("option"); + option.value = device.deviceId; + option.text = device.label; + audioOutputSelect.appendChild(option); + option.selected = true; + } + saveSettings(); // we're saving because there was an explicit action to change devices + } catch (e) { + errorlog(e); + } + if (!session.sink) { + return; + } // Not sure this would ever happen, but whatever. + resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist. + } + }); + } + }; + } else if (!navigator.mediaDevices) { + console.warn("No navigator.mediaDevices found - try a different browser or check your settings."); + } + + audioOutputSelect.onchange = function () { + log("audioOutputSelect.onchange = function() {"); + + if (iOS || iPad) { + return; + } + + if (Firefox && !session.mobile) { + if (audioOutputSelect.options[audioOutputSelect.selectedIndex].value === "others") { + // we handle this elsewhere + return; + } + } + + try { + session.sink = audioOutputSelect.options[audioOutputSelect.selectedIndex].value; + saveSettings(); + } catch (e) { + errorlog(e); + } + if (!session.sink) { + return; + } + + resetupAudioOut(); + + log("done audioOutputSelect.onchange = function() {"); + }; + } catch (e) { + errorlog(e); + } +} + +function refreshMicrophoneDevice(UUID = false) { + if (session.screenShareState || session.mediafileShare) { + log("can't refresh a screenshare or fileshare"); + + if (UUID) { + var data = {}; + data.UUID = UUID; + data.rejected = "can't refresh mic during screen or file share"; + session.sendMessage(data, data.UUID); + } + + return; + } + log("refreshing microphone.."); + activatedPreview = false; + grabAudio("#audioSource3", null, false, UUID); +} + +function refreshVideoDevice(UUID = false) { + if (session.screenShareState || session.mediafileShare) { + log("can't refresh video during screenshare or fileshare"); + + if (UUID) { + var data = {}; + data.UUID = UUID; + data.rejected = "can't refresh video during screen or file share"; + session.sendMessage(data, data.UUID); + } + + return; + } + log("refreshing video device.."); + activatedPreview = false; + grabVideo(session.quality, "videosource", "select#videoSource3"); + +} + +function directRefreshVideo(ele) { + var UUID = ele.dataset.UUID; + if (!UUID) { return; } + + var data = {}; + data.refreshVideo = true; + data.UUID = UUID; + if (session.sendRequest(data, UUID)) { + ele.classList.add("pressed"); + setTimeout((ele) => { + if (ele) { + ele.classList.remove("pressed"); + } + }, 400, ele); + } +} + +function directRefreshConnection(ele) { + var UUID = ele.dataset.UUID; + if (!UUID) { return; } + + var data = {}; + data.refreshConnection = true; + data.UUID = UUID; + if (session.sendRequest(data, UUID)) { + ele.classList.add("pressed"); + setTimeout((ele) => { + if (ele) { + ele.classList.remove("pressed"); + } + }, 400, ele); + } +} + +function directReconnectPeer(guestUUID, peerUUID) { + // Tell a specific guest to reconnect to a specific peer + var data = {}; + data.reconnectPeer = peerUUID; + data.UUID = guestUUID; + session.sendRequest(data, guestUUID); + log("Sent reconnectPeer command to " + guestUUID + " for peer " + peerUUID); +} + +// Mesh diagram action wrappers +function meshRefreshVideo(uuid) { + var data = {}; + data.refreshVideo = true; + data.UUID = uuid; + session.sendRequest(data, uuid); + log("Sent refreshVideo to " + uuid); +} + +function meshRefreshConnection(uuid) { + var data = {}; + data.refreshConnection = true; + data.UUID = uuid; + session.sendRequest(data, uuid); + log("Sent refreshConnection (ICE restart) to " + uuid); +} + +function meshRefreshAll(uuid) { + var data = {}; + data.refreshAll = true; + data.UUID = uuid; + session.sendRequest(data, uuid); + log("Sent refreshAll to " + uuid); +} + +function meshRefreshMic(uuid) { + var data = {}; + data.refreshMicrophone = true; + data.UUID = uuid; + session.sendRequest(data, uuid); + log("Sent refreshMicrophone to " + uuid); +} + +function meshRestartWhip(uuid) { + var data = {}; + data.restartWhip = true; + data.UUID = uuid; + session.sendRequest(data, uuid); + log("Sent restartWhip to " + uuid); +} + +function restartWhipDirector(ele) { + var UUID = ele.dataset.UUID; + if (UUID && session.rpcs[UUID]) { + meshRestartWhip(UUID); + warnUser("WHIP restart command sent"); + } +} + +// ============================================ +// MESH NETWORK VISUALIZATION +// ============================================ + +var meshData = { + nodes: {}, // uuid -> node info + edges: [], // connection info between nodes + pendingResponses: 0, + lastRefresh: 0, + modalOpen: false, + patchedConnections: {}, // "uuidA-uuidB" -> {prevStateA: bool, prevStateB: bool} for connections being relayed via mix-minus + whipStatus: null, // {state, url, connected, reconnectAttempts} - WHIP outbound status + whepConnections: {} // uuid -> {state, connected} - WHEP inbound connections +}; + +// Patch a failed P2P connection via mix-minus relay +// Director becomes the audio bridge between two guests +function patchConnectionViaMixMinus(uuidA, uuidB) { + if (!session.mixMinusState) { + session.mixMinusState = {}; + } + + // Initialize mix-minus state for both guests if needed + if (!session.mixMinusState[uuidA]) { + initMixMinusStateForGuest(uuidA); + } + if (!session.mixMinusState[uuidB]) { + initMixMinusStateForGuest(uuidB); + } + + // Record previous state before modifying (for proper restore on unpatch) + var wasAExcludedFromB = session.mixMinusState[uuidB].excludeSources.includes(uuidA); + var wasBExcludedFromA = session.mixMinusState[uuidA].excludeSources.includes(uuidB); + var wasAEnabled = session.mixMinusState[uuidA].enabled || false; + var wasBEnabled = session.mixMinusState[uuidB].enabled || false; + + // Enable mix-minus for both guests (required for patching to work) + session.mixMinusState[uuidA].enabled = true; + session.mixMinusState[uuidB].enabled = true; + + // Remove A from B's excludeSources (so B hears A via director) + var idxAinB = session.mixMinusState[uuidB].excludeSources.indexOf(uuidA); + if (idxAinB > -1) { + session.mixMinusState[uuidB].excludeSources.splice(idxAinB, 1); + } + + // Remove B from A's excludeSources (so A hears B via director) + var idxBinA = session.mixMinusState[uuidA].excludeSources.indexOf(uuidB); + if (idxBinA > -1) { + session.mixMinusState[uuidA].excludeSources.splice(idxBinA, 1); + } + + // Update the mixes + updateMixMinusForGuest(uuidA); + updateMixMinusForGuest(uuidB); + + // Track this patched connection with previous state for proper restore + // Use sorted order so we can correctly restore regardless of call order + var sorted = [uuidA, uuidB].sort(); + var patchKey = sorted.join("-"); + meshData.patchedConnections[patchKey] = { + // Store as: was sorted[0] excluded from sorted[1]'s mix, and vice versa + wasFirstExcludedFromSecond: sorted[0] === uuidA ? wasAExcludedFromB : wasBExcludedFromA, + wasSecondExcludedFromFirst: sorted[0] === uuidA ? wasBExcludedFromA : wasAExcludedFromB, + // Store enabled state for both guests + wasFirstEnabled: sorted[0] === uuidA ? wasAEnabled : wasBEnabled, + wasSecondEnabled: sorted[0] === uuidA ? wasBEnabled : wasAEnabled + }; + + log("Patched connection via mix-minus: " + uuidA + " <-> " + uuidB); +} + +// Unpatch a connection (when P2P recovers or manually) +function unpatchConnection(uuidA, uuidB) { + if (!session.mixMinusState) return; + + var sorted = [uuidA, uuidB].sort(); + var patchKey = sorted.join("-"); + var savedState = meshData.patchedConnections[patchKey]; + + // Restore previous exclude state (only add back if they were excluded before patching) + // sorted[0] = first UUID alphabetically, sorted[1] = second + var firstUUID = sorted[0]; + var secondUUID = sorted[1]; + + if (session.mixMinusState[secondUUID] && savedState && savedState.wasFirstExcludedFromSecond) { + // First was excluded from second's mix before - restore that + if (!session.mixMinusState[secondUUID].excludeSources.includes(firstUUID)) { + session.mixMinusState[secondUUID].excludeSources.push(firstUUID); + } + updateMixMinusForGuest(secondUUID); + } + + if (session.mixMinusState[firstUUID] && savedState && savedState.wasSecondExcludedFromFirst) { + // Second was excluded from first's mix before - restore that + if (!session.mixMinusState[firstUUID].excludeSources.includes(secondUUID)) { + session.mixMinusState[firstUUID].excludeSources.push(secondUUID); + } + updateMixMinusForGuest(firstUUID); + } + + // Restore previous enabled state for both guests + if (savedState) { + if (session.mixMinusState[firstUUID]) { + session.mixMinusState[firstUUID].enabled = savedState.wasFirstEnabled; + updateMixMinusForGuest(firstUUID); + } + if (session.mixMinusState[secondUUID]) { + session.mixMinusState[secondUUID].enabled = savedState.wasSecondEnabled; + updateMixMinusForGuest(secondUUID); + } + } + + // Remove from patched tracking + delete meshData.patchedConnections[patchKey]; + + log("Unpatched connection: " + uuidA + " <-> " + uuidB); +} + +// Auto-patch all failed connections in the mesh +function autoPatchAllFailed() { + var patchCount = 0; + meshData.edges.forEach(function(edge) { + if (edge.state === "failed" || edge.state === "disconnected") { + // Skip edges involving director or viewers - patching only makes sense for guest↔guest + var sourceNode = meshData.nodes[edge.source]; + var targetNode = meshData.nodes[edge.target]; + if (sourceNode && (sourceNode.isDirector || sourceNode.isViewer)) return; + if (targetNode && (targetNode.isDirector || targetNode.isViewer)) return; + + var patchKey = [edge.source, edge.target].sort().join("-"); + if (!meshData.patchedConnections[patchKey]) { + patchConnectionViaMixMinus(edge.source, edge.target); + patchCount++; + } + } + }); + log("Auto-patched " + patchCount + " failed connections via mix-minus"); + if (meshData.modalOpen) { + renderMeshVisualization(); + } + return patchCount; +} + +// Auto-unpatch connections that have recovered +function autoUnpatchRecovered() { + var unpatchCount = 0; + // Collect keys to unpatch first (avoid modifying while iterating) + var toUnpatch = []; + for (var patchKey in meshData.patchedConnections) { + // Find the corresponding edge + var edge = meshData.edges.find(function(e) { return e.id === patchKey; }); + if (edge && edge.state === "connected") { + toUnpatch.push(patchKey); + } + } + // Now unpatch collected connections + toUnpatch.forEach(function(patchKey) { + var uuids = patchKey.split("-"); + unpatchConnection(uuids[0], uuids[1]); + unpatchCount++; + }); + if (unpatchCount > 0) { + log("Auto-unpatched " + unpatchCount + " recovered connections"); + if (meshData.modalOpen) { + renderMeshVisualization(); + } + } + return unpatchCount; +} + +// Check if a connection is currently patched +function isConnectionPatched(uuidA, uuidB) { + var patchKey = [uuidA, uuidB].sort().join("-"); + return !!meshData.patchedConnections[patchKey]; +} + +// UI wrapper for patching from mesh diagram +function meshPatchConnection(uuidA, uuidB) { + patchConnectionViaMixMinus(uuidA, uuidB); + // Refresh the edge details panel + var edgeId = [uuidA, uuidB].sort().join("-"); + var edge = meshData.edges.find(function(e) { return e.id === edgeId; }); + if (edge) { + showEdgeDetails(edge); + } + renderMeshVisualization(); +} + +// UI wrapper for unpatching from mesh diagram +function meshUnpatchConnection(uuidA, uuidB) { + unpatchConnection(uuidA, uuidB); + // Refresh the edge details panel + var edgeId = [uuidA, uuidB].sort().join("-"); + var edge = meshData.edges.find(function(e) { return e.id === edgeId; }); + if (edge) { + showEdgeDetails(edge); + } + renderMeshVisualization(); +} + +function requestMeshData() { + // Request connection maps from all connected guests + meshData.nodes = {}; + meshData.edges = []; + meshData.pendingResponses = 0; + + // Collect WHIP outbound status + meshData.whipStatus = null; + if (session.whipOut) { + meshData.whipStatus = { + state: session.whipOut.connectionState || session.whipOut.iceConnectionState || "unknown", + url: session.whipOutput || "", + connected: session.whipOut.connectionState === 'connected' || + session.whipOut.iceConnectionState === 'connected' || + session.whipOut.iceConnectionState === 'completed', + reconnectAttempts: session.getWhipReconnectAttempts ? session.getWhipReconnectAttempts() : 0 + }; + } + + // Collect WHEP inbound connection statuses + meshData.whepConnections = {}; + for (var uuid in session.rpcs) { + if (session.rpcs[uuid] && session.rpcs[uuid].whep) { + meshData.whepConnections[uuid] = { + state: session.rpcs[uuid].whep.connectionState || session.rpcs[uuid].whep.iceConnectionState || "unknown", + connected: session.rpcs[uuid].whep.connectionState === 'connected' || + session.rpcs[uuid].whep.iceConnectionState === 'connected' || + session.rpcs[uuid].whep.iceConnectionState === 'completed' + }; + } + } + + // Add director as a node - include its connections from rpcs and pcs + var directorConnections = []; + + // Director's rpcs = guests publishing TO director = director receives from them + for (var uuid in session.rpcs) { + if (session.rpcs[uuid]) { + var rpc = session.rpcs[uuid]; + directorConnections.push({ + peerUUID: uuid, + peerStreamID: rpc.streamID || uuid, + direction: "incoming", // Director receives from guest + state: rpc.connectionState || "connected", + bandwidth: rpc.bandwidth || -1, + audioEnabled: true, + videoEnabled: true + }); + } + } + + // Director's pcs = director publishing TO peers (data channels, etc.) + for (var uuid in session.pcs) { + if (session.pcs[uuid]) { + var pc = session.pcs[uuid]; + directorConnections.push({ + peerUUID: uuid, + peerStreamID: pc.streamID || uuid, + direction: "outgoing", // Director sends to guest/scene + state: pc.connectionState || "connected", + bandwidth: -1, + audioEnabled: true, + videoEnabled: true + }); + } + } + + meshData.nodes[session.UUID] = { + uuid: session.UUID, + streamID: session.streamID, + label: "Director", + isDirector: true, + connections: directorConnections, + health: "healthy" + }; + + // Request from all rpcs (guests we're receiving from) + for (var uuid in session.rpcs) { + if (session.rpcs[uuid]) { + var data = { getConnectionMap: true, UUID: uuid }; + session.sendRequest(data, uuid); + meshData.pendingResponses++; + + // Add node placeholder + meshData.nodes[uuid] = { + uuid: uuid, + streamID: session.rpcs[uuid].streamID || uuid, + label: session.rpcs[uuid].label || session.rpcs[uuid].streamID || "Guest", + isDirector: false, + connections: [], + health: "pending" + }; + } + } + + log("Requested mesh data from " + meshData.pendingResponses + " guests"); + + // Set timeout to process after responses come in + setTimeout(function() { + aggregateMeshData(); + renderMeshVisualization(); + }, 2000); +} + +function handleConnectionMapResponse(msg, UUID) { + // Called when a guest responds with their connection map + // UUID = the key from director's rpcs (how director identifies this guest) + // msg.connectionMap.uuid = guest's session.UUID (might differ!) + if (msg.connectionMap) { + var map = msg.connectionMap; + + // Use the UUID parameter (director's key) for node matching, not map.uuid + // This ensures we update the correct placeholder node + var nodeKey = UUID; + + // Store the director's external UUID (as known by this guest) + // This lets us map guest connections to director correctly + if (map.requesterUUID) { + meshData.directorExternalUUID = map.requesterUUID; + } + + // Check if any connections use TURN (relay) + var usingTurn = false; + if (map.connections) { + for (var i = 0; i < map.connections.length; i++) { + if (map.connections[i].candidateType === "relay") { + usingTurn = true; + break; + } + } + } + + // Update node info using director's UUID key + if (meshData.nodes[nodeKey]) { + meshData.nodes[nodeKey].streamID = map.streamID; + meshData.nodes[nodeKey].label = map.label; + meshData.nodes[nodeKey].guestUUID = map.uuid; // Store guest's self-reported UUID + meshData.nodes[nodeKey].connections = map.connections; + meshData.nodes[nodeKey].browser = map.browser || "Unknown"; + meshData.nodes[nodeKey].usingTurn = usingTurn; + } else { + meshData.nodes[nodeKey] = { + uuid: nodeKey, + guestUUID: map.uuid, + streamID: map.streamID, + label: map.label, + isDirector: false, + connections: map.connections, + browser: map.browser || "Unknown", + usingTurn: usingTurn, + health: "healthy" + }; + } + + meshData.pendingResponses--; + log("Received connection map from " + map.label + " (UUID: " + nodeKey + ", " + map.connections.length + " connections)"); + + // Re-render whenever a response arrives and modal is open + // This handles late responses even if some peers never respond + if (meshData.modalOpen) { + aggregateMeshData(); + renderMeshVisualization(); + } + } +} + +function aggregateMeshData() { + // Build edges from all node connections + meshData.edges = []; + var edgeMap = {}; // edgeId -> edge object (to track bidirectionality) + + // Build a lookup from streamID to node UUID for edge matching + var streamIdToUuid = {}; + var directorNodeKey = null; + for (var uuid in meshData.nodes) { + var node = meshData.nodes[uuid]; + if (node.streamID) { + streamIdToUuid[node.streamID] = uuid; + } + if (node.isDirector) { + directorNodeKey = uuid; + } + } + + for (var uuid in meshData.nodes) { + var node = meshData.nodes[uuid]; + var failedCount = 0; + var degradedCount = 0; + + if (node.connections) { + for (var i = 0; i < node.connections.length; i++) { + var conn = node.connections[i]; + + // Try to resolve peer by streamID first (more reliable), then UUID + var peerNodeUuid = conn.peerUUID; + if (conn.peerStreamID && streamIdToUuid[conn.peerStreamID]) { + peerNodeUuid = streamIdToUuid[conn.peerStreamID]; + } + + // If peer doesn't exist as a node and this is an outgoing connection, + // check if it's actually the director (using directorExternalUUID) + if (!meshData.nodes[peerNodeUuid] && conn.direction === "outgoing") { + // Check if this is a connection to the director + if (meshData.directorExternalUUID && conn.peerUUID === meshData.directorExternalUUID) { + // Map to director node instead of creating phantom + peerNodeUuid = directorNodeKey; + } else { + // Create viewer/scene node for other unknown peers + meshData.nodes[peerNodeUuid] = { + uuid: peerNodeUuid, + streamID: conn.peerStreamID || peerNodeUuid, + label: conn.peerStreamID || "Viewer", + isDirector: false, + isViewer: true, + connections: [], + health: "healthy" + }; + if (conn.peerStreamID) { + streamIdToUuid[conn.peerStreamID] = peerNodeUuid; + } + } + } + + // Create unique edge ID (sorted UUIDs for deduplication) + var edgeId = [uuid, peerNodeUuid].sort().join("-"); + + // Track direction: outgoing = publishing TO peer, incoming = receiving FROM peer + var directionKey = conn.direction === "outgoing" ? "hasOutgoing" : "hasIncoming"; + + if (!edgeMap[edgeId]) { + edgeMap[edgeId] = { + id: edgeId, + source: uuid, + target: peerNodeUuid, + sourceStreamID: node.streamID, + targetStreamID: conn.peerStreamID, + state: conn.state, + bandwidth: conn.bandwidth, + candidateType: conn.candidateType, + nackCount: conn.nackCount, + pliCount: conn.pliCount, + hasOutgoing: false, + hasIncoming: false, + bidirectional: false + }; + } + + // Mark this direction as present + edgeMap[edgeId][directionKey] = true; + + // Update bidirectional flag + if (edgeMap[edgeId].hasOutgoing && edgeMap[edgeId].hasIncoming) { + edgeMap[edgeId].bidirectional = true; + } + + // Merge states - keep the worse one + var stateRank = { "failed": 0, "disconnected": 1, "new": 1, "connecting": 1, "closed": 1, "connected": 2 }; + var existingRank = stateRank[edgeMap[edgeId].state] !== undefined ? stateRank[edgeMap[edgeId].state] : 1; + var newRank = stateRank[conn.state] !== undefined ? stateRank[conn.state] : 1; + if (newRank < existingRank) { + edgeMap[edgeId].state = conn.state; + } + + // Track health based on RTCPeerConnection.connectionState + if (conn.state === "failed") { + failedCount++; + } else if (conn.state === "disconnected" || conn.state === "new" || conn.state === "connecting" || conn.state === "closed") { + degradedCount++; + } + } + } + + // Update node health + if (failedCount > 0) { + node.health = "failed"; + } else if (degradedCount > 0) { + node.health = "degraded"; + } else if (node.connections && node.connections.length > 0) { + node.health = "healthy"; + } else if (node.isViewer || node.isDirector) { + // Viewers/scenes and director don't report connections, so no connections is expected + node.health = "healthy"; + } else { + node.health = "isolated"; + } + } + + // Convert edgeMap to array + meshData.edges = Object.values(edgeMap); + + // Calculate summary stats + var totalConnections = meshData.edges.length; + var failedConnections = meshData.edges.filter(e => e.state === "failed").length; + var healthyConnections = meshData.edges.filter(e => e.state === "connected").length; + var bidirectionalCount = meshData.edges.filter(e => e.bidirectional).length; + var onewayCount = totalConnections - bidirectionalCount; + + var viewerCount = Object.values(meshData.nodes).filter(n => n.isViewer).length; + + meshData.summary = { + totalNodes: Object.keys(meshData.nodes).length, + viewerNodes: viewerCount, + totalConnections: totalConnections, + bidirectionalConnections: bidirectionalCount, + onewayConnections: onewayCount, + healthyConnections: healthyConnections, + failedConnections: failedConnections, + degradedConnections: totalConnections - healthyConnections - failedConnections + }; + + meshData.lastRefresh = Date.now(); + log("Aggregated mesh data: " + meshData.summary.totalNodes + " nodes, " + meshData.summary.totalConnections + " connections"); +} + +function getMeshHealthBadge() { + // Returns HTML for the health badge to show in director controls + var s = meshData.summary; + if (!s) return ""; + + var color = "#4CAF50"; // green + var text = s.healthyConnections + "/" + s.totalConnections; + + if (s.failedConnections > 0) { + color = "#F44336"; // red + text = s.failedConnections + " failed"; + } else if (s.degradedConnections > 0) { + color = "#FF9800"; // orange + } + + return '' + text + ''; +} + +function openMeshVisualization() { + if (meshData.modalOpen) return; + meshData.modalOpen = true; + + // Create modal overlay + var modal = document.createElement("div"); + modal.id = "meshModal"; + modal.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:10000;display:flex;flex-direction:column;"; + + // Toolbar + var toolbar = document.createElement("div"); + toolbar.style.cssText = "padding:10px 20px;background:#222;display:flex;align-items:center;gap:10px;border-bottom:1px solid #444;flex-wrap:wrap;"; + toolbar.innerHTML = ` +

    Mesh Network Debug

    + + + + + + + `; + + // SVG container + var svgContainer = document.createElement("div"); + svgContainer.id = "meshSvgContainer"; + svgContainer.style.cssText = "flex:1;overflow:hidden;position:relative;"; + + // Detail panel (hidden by default) + var detailPanel = document.createElement("div"); + detailPanel.id = "meshDetailPanel"; + detailPanel.style.cssText = "position:absolute;right:0;top:0;width:300px;height:100%;background:#1a1a1a;border-left:1px solid #444;padding:20px;display:none;overflow-y:auto;color:#fff;"; + + svgContainer.appendChild(detailPanel); + modal.appendChild(toolbar); + modal.appendChild(svgContainer); + document.body.appendChild(modal); + + // Initialize layout button text to match current mode + var layoutBtn = document.getElementById("meshLayoutBtn"); + if (layoutBtn) { + layoutBtn.textContent = "Layout: " + meshLayoutMode.charAt(0).toUpperCase() + meshLayoutMode.slice(1); + } + + // Add keyboard shortcuts + document.addEventListener("keydown", meshKeyHandler); + + // Request fresh data and render + requestMeshData(); +} + +function closeMeshVisualization() { + var modal = document.getElementById("meshModal"); + if (modal) { + modal.remove(); + } + meshData.modalOpen = false; + document.removeEventListener("keydown", meshKeyHandler); +} + +function meshKeyHandler(e) { + if (!meshData.modalOpen) return; + + switch(e.key) { + case "Escape": + closeMeshVisualization(); + break; + case "r": + case "R": + requestMeshData(); + break; + case "f": + case "F": + var cb = document.getElementById("meshFilterProblems"); + if (cb) cb.checked = !cb.checked; + renderMeshVisualization(); + break; + } +} + +var meshLayoutMode = "circular"; // circular, grid, force + +function cycleMeshLayout() { + if (meshLayoutMode === "force") { + meshLayoutMode = "circular"; + } else if (meshLayoutMode === "circular") { + meshLayoutMode = "grid"; + } else { + meshLayoutMode = "force"; + } + + var btn = document.getElementById("meshLayoutBtn"); + if (btn) { + btn.textContent = "Layout: " + meshLayoutMode.charAt(0).toUpperCase() + meshLayoutMode.slice(1); + } + + renderMeshVisualization(); +} + +function renderMeshVisualization() { + var container = document.getElementById("meshSvgContainer"); + if (!container) return; + + var existingSvg = container.querySelector("svg"); + if (existingSvg) existingSvg.remove(); + + var width = container.clientWidth; + var height = container.clientHeight - 50; // Leave room for status bar + + // Create SVG + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", width); + svg.setAttribute("height", height); + svg.style.display = "block"; + + // Filter problems only? + var filterProblems = document.getElementById("meshFilterProblems")?.checked || false; + + // Calculate node positions based on layout mode + var nodePositions = {}; + var nodeArray = Object.values(meshData.nodes); + var filteredNodes = filterProblems ? nodeArray.filter(n => n.health !== "healthy") : nodeArray; + + if (filteredNodes.length === 0 && filterProblems) { + // Show message + var text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.setAttribute("x", width / 2); + text.setAttribute("y", height / 2); + text.setAttribute("fill", "#4CAF50"); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("font-size", "24"); + text.textContent = "All connections healthy!"; + svg.appendChild(text); + container.insertBefore(svg, container.firstChild); + return; + } + + // Calculate positions + var centerX = width / 2; + var centerY = height / 2; + var radius = Math.min(width, height) / 2 - 100; + + if (meshLayoutMode === "circular") { + // Director in center, guests in a ring + var guestNodes = filteredNodes.filter(n => !n.isDirector); + var directorNode = filteredNodes.find(n => n.isDirector); + + // Place director in center + if (directorNode) { + nodePositions[directorNode.uuid] = { x: centerX, y: centerY }; + } + + // Place guests in a ring around director + guestNodes.forEach(function(node, i) { + var angle = (2 * Math.PI * i) / guestNodes.length - Math.PI / 2; + nodePositions[node.uuid] = { + x: centerX + radius * Math.cos(angle), + y: centerY + radius * Math.sin(angle) + }; + }); + } else if (meshLayoutMode === "grid") { + var cols = Math.ceil(Math.sqrt(filteredNodes.length)); + var spacing = Math.min(width, height) / (cols + 1); + filteredNodes.forEach(function(node, i) { + var col = i % cols; + var row = Math.floor(i / cols); + nodePositions[node.uuid] = { + x: spacing + col * spacing, + y: spacing + row * spacing + }; + }); + } else { + // Force-directed: simple random for now + filteredNodes.forEach(function(node) { + nodePositions[node.uuid] = { + x: 100 + Math.random() * (width - 200), + y: 100 + Math.random() * (height - 200) + }; + }); + } + + // Add arrow marker definitions for one-way connections + var defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); + + // Arrow markers for different colors + ["#4CAF50", "#FF9800", "#F44336"].forEach(function(color, idx) { + var marker = document.createElementNS("http://www.w3.org/2000/svg", "marker"); + marker.setAttribute("id", "arrow-" + idx); + marker.setAttribute("markerWidth", "10"); + marker.setAttribute("markerHeight", "10"); + marker.setAttribute("refX", "35"); + marker.setAttribute("refY", "3"); + marker.setAttribute("orient", "auto"); + marker.setAttribute("markerUnits", "strokeWidth"); + + var path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", "M0,0 L0,6 L9,3 z"); + path.setAttribute("fill", color); + marker.appendChild(path); + defs.appendChild(marker); + }); + svg.appendChild(defs); + + // Draw edges first (so they appear behind nodes) + meshData.edges.forEach(function(edge) { + var sourcePos = nodePositions[edge.source]; + var targetPos = nodePositions[edge.target]; + + if (!sourcePos || !targetPos) return; + + // Filter if needed + if (filterProblems && edge.state === "connected") return; + + // For one-way connections, determine correct arrow direction + // hasOutgoing means source publishes TO target (arrow: source→target) + // hasIncoming only means target publishes TO source (arrow: target→source) + var drawFromPos = sourcePos; + var drawToPos = targetPos; + if (!edge.bidirectional && edge.hasIncoming && !edge.hasOutgoing) { + // Swap direction - target is actually the publisher + drawFromPos = targetPos; + drawToPos = sourcePos; + } + + var line = document.createElementNS("http://www.w3.org/2000/svg", "line"); + line.setAttribute("x1", drawFromPos.x); + line.setAttribute("y1", drawFromPos.y); + line.setAttribute("x2", drawToPos.x); + line.setAttribute("y2", drawToPos.y); + + // Check if this edge is patched via mix-minus + var isPatched = isConnectionPatched(edge.source, edge.target); + + // Style based on state + var strokeColor = "#4CAF50"; // green + var strokeWidth = 2; + var dashArray = ""; + var arrowIdx = 0; + + if (edge.state === "failed") { + strokeColor = "#F44336"; + strokeWidth = 3; + dashArray = "5,5"; + arrowIdx = 2; + } else if (edge.state === "disconnected" || edge.state === "new" || edge.state === "connecting" || edge.state === "closed") { + strokeColor = "#FF9800"; + dashArray = "10,5"; + arrowIdx = 1; + } + + // Override style for patched connections - show as cyan with double-dash + if (isPatched) { + strokeColor = "#00BCD4"; // cyan - matches patch button + strokeWidth = 3; + dashArray = "8,3,2,3"; // distinctive double-dash pattern + } + + line.setAttribute("stroke", strokeColor); + line.setAttribute("stroke-width", strokeWidth); + if (dashArray) line.setAttribute("stroke-dasharray", dashArray); + line.setAttribute("data-edge-id", edge.id); + line.style.cursor = "pointer"; + + // Add arrow for one-way connections (not bidirectional) + if (!edge.bidirectional) { + line.setAttribute("marker-end", "url(#arrow-" + arrowIdx + ")"); + } + + // Click handler for edge + line.onclick = function() { + showEdgeDetails(edge); + }; + + // Hover effect + line.onmouseenter = function() { + this.setAttribute("stroke-width", parseInt(strokeWidth) + 2); + }; + line.onmouseleave = function() { + this.setAttribute("stroke-width", strokeWidth); + }; + + svg.appendChild(line); + }); + + // Draw nodes + filteredNodes.forEach(function(node) { + var pos = nodePositions[node.uuid]; + if (!pos) return; + + var g = document.createElementNS("http://www.w3.org/2000/svg", "g"); + g.setAttribute("transform", "translate(" + pos.x + "," + pos.y + ")"); + g.style.cursor = "pointer"; + + // Node shape - circle for publishers, square for viewers/scenes + var shape; + if (node.isViewer) { + // Square for viewers/scenes + shape = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + shape.setAttribute("x", -25); + shape.setAttribute("y", -25); + shape.setAttribute("width", 50); + shape.setAttribute("height", 50); + shape.setAttribute("rx", 5); + shape.setAttribute("fill", "#222"); + } else { + // Circle for publishers + shape = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + shape.setAttribute("r", 30); + shape.setAttribute("fill", "#333"); + } + + // Border color based on health/type + var borderColor = "#4CAF50"; + if (node.health === "failed") borderColor = "#F44336"; + else if (node.health === "degraded") borderColor = "#FF9800"; + else if (node.health === "isolated") borderColor = "#9E9E9E"; + else if (node.isDirector) borderColor = "#2196F3"; + else if (node.isViewer) borderColor = "#9C27B0"; // Purple for viewers/scenes + + shape.setAttribute("stroke", borderColor); + shape.setAttribute("stroke-width", 3); + + // Label + var text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("dy", 5); + text.setAttribute("fill", "#fff"); + text.setAttribute("font-size", "12"); + text.textContent = (node.label || "?").substring(0, 8); + + // Badge for special node types + if (node.isDirector) { + var badge = document.createElementNS("http://www.w3.org/2000/svg", "text"); + badge.setAttribute("text-anchor", "middle"); + badge.setAttribute("y", -35); + badge.setAttribute("fill", "#2196F3"); + badge.setAttribute("font-size", "10"); + badge.textContent = "DIRECTOR"; + g.appendChild(badge); + } else if (node.isViewer) { + var badge = document.createElementNS("http://www.w3.org/2000/svg", "text"); + badge.setAttribute("text-anchor", "middle"); + badge.setAttribute("y", -30); + badge.setAttribute("fill", "#9C27B0"); + badge.setAttribute("font-size", "10"); + badge.textContent = "SCENE/VIEW"; + g.appendChild(badge); + } + + g.appendChild(shape); + g.appendChild(text); + + // Click handler + g.onclick = function() { + showNodeDetails(node); + }; + + svg.appendChild(g); + }); + + container.insertBefore(svg, container.firstChild); + + // Add status bar + var statusBar = container.querySelector(".meshStatusBar"); + if (!statusBar) { + statusBar = document.createElement("div"); + statusBar.className = "meshStatusBar"; + statusBar.style.cssText = "position:absolute;bottom:0;left:0;right:0;padding:10px 20px;background:#222;color:#fff;font-size:14px;"; + container.appendChild(statusBar); + } + + var s = meshData.summary || {}; + var nodeInfo = s.totalNodes || 0; + if (s.viewerNodes > 0) { + nodeInfo += " (" + s.viewerNodes + " scenes/viewers)"; + } + var connInfo = ""; + if (s.bidirectionalConnections > 0) { + connInfo += s.bidirectionalConnections + " bidirectional"; + } + if (s.onewayConnections > 0) { + if (connInfo) connInfo += ", "; + connInfo += s.onewayConnections + " one-way→"; + } + + // Build WHIP/WHEP status display + var whipWhepStatus = ""; + if (meshData.whipStatus) { + var whipColor = meshData.whipStatus.connected ? "#4CAF50" : "#F44336"; + var whipIcon = meshData.whipStatus.connected ? "la-broadcast-tower" : "la-exclamation-triangle"; + whipWhepStatus += ''; + whipWhepStatus += ' WHIP: ' + meshData.whipStatus.state; + if (!meshData.whipStatus.connected) { + whipWhepStatus += ' '; + } + if (meshData.whipStatus.reconnectAttempts > 0) { + whipWhepStatus += ' (' + meshData.whipStatus.reconnectAttempts + ' retries)'; + } + whipWhepStatus += ''; + } + + // Count WHEP connection issues + var whepTotal = Object.keys(meshData.whepConnections).length; + var whepFailed = Object.values(meshData.whepConnections).filter(function(w) { return !w.connected; }).length; + if (whepTotal > 0) { + var whepColor = whepFailed === 0 ? "#4CAF50" : "#F44336"; + whipWhepStatus += ''; + whipWhepStatus += ' WHEP: ' + (whepTotal - whepFailed) + '/' + whepTotal + ' connected'; + whipWhepStatus += ''; + } + + statusBar.innerHTML = ` + ${s.healthyConnections || 0} healthy | + ${s.degradedConnections || 0} degraded | + ${s.failedConnections || 0} failed | + ${nodeInfo} nodes | ${connInfo || "0 connections"} + ${whipWhepStatus} + Last refresh: ${meshData.lastRefresh ? new Date(meshData.lastRefresh).toLocaleTimeString() : "Never"} + `; +} + +function showNodeDetails(node) { + var panel = document.getElementById("meshDetailPanel"); + if (!panel) return; + + panel.style.display = "block"; + + var connections = node.connections || []; + var connHtml = connections.map(function(c) { + var stateColor = c.state === "connected" ? "#4CAF50" : c.state === "failed" ? "#F44336" : "#FF9800"; + var turnBadge = c.candidateType === "relay" ? ' TURN' : ''; + return `
    + ${c.peerStreamID || c.peerUUID.substring(0,8)}${turnBadge} + ${c.state} + ${c.bandwidth > 0 ? '
    ' + c.bandwidth + ' kbps' : ''} +
    `; + }).join(""); + + // Action buttons for guest nodes (not director) - recovery focused + var actionButtons = ''; + if (!node.isDirector && !node.isViewer) { + actionButtons = ` +

    Recovery Actions

    +
    + + +
    + + + + `; + } + + // Build browser/TURN info line + var infoLine = ''; + if (node.browser && node.browser !== "Unknown") { + infoLine += '' + node.browser + ''; + } + if (node.usingTurn) { + infoLine += 'TURN'; + } + // Add WHEP status if this node has a WHEP inbound connection + if (meshData.whepConnections && meshData.whepConnections[node.uuid]) { + var whepInfo = meshData.whepConnections[node.uuid]; + var whepColor = whepInfo.connected ? "#4CAF50" : "#F44336"; + var whepBg = whepInfo.connected ? "#1B5E20" : "#B71C1C"; + infoLine += 'WHEP: ' + whepInfo.state + ''; + } + + panel.innerHTML = ` +

    ${node.label}

    +

    UUID: ${node.uuid.substring(0, 12)}...

    +

    Stream ID: ${node.streamID}

    + ${infoLine ? '

    ' + infoLine + '

    ' : ''} +

    Health: ${node.health}

    +

    Connections (${connections.length})

    + ${connHtml || '

    No connections

    '} + ${actionButtons} +
    + +
    + `; +} + +function showEdgeDetails(edge) { + var panel = document.getElementById("meshDetailPanel"); + if (!panel) return; + + panel.style.display = "block"; + + var stateColor = edge.state === "connected" ? "#4CAF50" : edge.state === "failed" ? "#F44336" : "#FF9800"; + + // Describe directionality + var directionText = ""; + if (edge.bidirectional) { + directionText = "↔ Bidirectional (both publish to each other)"; + } else if (edge.hasOutgoing && !edge.hasIncoming) { + directionText = "→ One-way (source publishes to target)"; + } else if (edge.hasIncoming && !edge.hasOutgoing) { + directionText = "← One-way (target publishes to source)"; + } else { + directionText = "Unknown"; + } + + // Check if this connection is patched via mix-minus + var isPatched = isConnectionPatched(edge.source, edge.target); + var patchedStatus = isPatched ? '

    Status: 🔊 Patched via Mix-Minus

    ' : ''; + + // Check if patching is applicable (only guest↔guest, not director/viewer edges) + var sourceNode = meshData.nodes[edge.source]; + var targetNode = meshData.nodes[edge.target]; + var canPatch = !(sourceNode && (sourceNode.isDirector || sourceNode.isViewer)) && + !(targetNode && (targetNode.isDirector || targetNode.isViewer)); + + // Build action buttons based on state + var actionButtons = ''; + if (edge.state === 'failed' || edge.state === 'disconnected') { + actionButtons += ``; + if (canPatch && !isPatched) { + actionButtons += ``; + } else if (isPatched) { + actionButtons += ``; + } + } else if (isPatched) { + // Connection is healthy but still patched - offer to unpatch + actionButtons += ``; + } + + panel.innerHTML = ` +

    Connection Details

    +

    From: ${edge.sourceStreamID || edge.source.substring(0,12)}

    +

    To: ${edge.targetStreamID || edge.target.substring(0,12)}

    +

    State: ${edge.state}

    + ${patchedStatus} +

    Direction: ${directionText}

    + ${edge.bandwidth > 0 ? '

    Bandwidth: ' + edge.bandwidth + ' kbps

    ' : ''} + ${edge.candidateType !== 'unknown' ? '

    Type: ' + edge.candidateType + '

    ' : ''} +

    NACK count: ${edge.nackCount}

    +

    PLI count: ${edge.pliCount}

    +
    + ${actionButtons} + +
    + `; +} + +function reconnectEdge(sourceUUID, targetUUID) { + // Find the edge to determine direction + var edgeId = [sourceUUID, targetUUID].sort().join("-"); + var edgeData = meshData.edges.find(function(e) { return e.id === edgeId; }); + + // Determine who should initiate the reconnect + // For outgoing edges: source publishes to target, so source reconnects + // For incoming-only edges: target publishes to source, so target reconnects + var initiator = sourceUUID; + var peer = targetUUID; + if (edgeData && edgeData.hasIncoming && !edgeData.hasOutgoing) { + // Swap - target is the publisher + initiator = targetUUID; + peer = sourceUUID; + } + + directReconnectPeer(initiator, peer); + + // Visual feedback + var edge = document.querySelector('[data-edge-id="' + edgeId + '"]'); + if (edge) { + edge.setAttribute("stroke", "#FF9800"); + edge.setAttribute("stroke-dasharray", "10,5"); + } + + // Close detail panel + var panel = document.getElementById("meshDetailPanel"); + if (panel) panel.style.display = "none"; + + // Refresh after delay + setTimeout(requestMeshData, 3000); +} + +// ============================================ +// END MESH NETWORK VISUALIZATION +// ============================================ + +function directRefreshMicrophone(ele) { + var UUID = ele.dataset.UUID; + if (!UUID) { return; } + // remoteAudioLabel_ + // '[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]' + + if (getById("remoteAudioLabel_" + UUID).dataset.nomic) { + warnUser("No microphone is enabled for this user\n\nNothing to reresh."); + } + var data = {}; + data.refreshMicrophone = true; + data.UUID = UUID; + if (session.sendRequest(data, UUID)) { // Viewer is requesting the PUBLISHER + ele.classList.add("pressed"); + setTimeout((ele) => { + if (ele) { + ele.classList.remove("pressed"); + } + }, 400, ele); + } +} + +function gotDevicesRemote(deviceInfos, UUID) { + try { + if (document.getElementById("remoteVideoSelect_" + UUID)) { + var videoSelect = document.getElementById("remoteVideoSelect_" + UUID); + var length = videoSelect.options.length; + for (i = length - 1; i >= 0; i--) { + videoSelect.options[i] = null; + } + } else { + var videoSelect = document.createElement("select"); + videoSelect.id = "remoteVideoSelect_" + UUID; + + videoSelect.onchange = function () { + if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) { + getById("requestVideoDevice_" + UUID).innerHTML = ' apply'; + getById("requestVideoDevice_" + UUID).title = "This will update the remote device to the selected one"; + } else { + getById("requestVideoDevice_" + UUID).innerHTML = ' request'; + getById("requestVideoDevice_" + UUID).title = "This will ask the remote guest for permission to change"; + } + }; + + var buttonGO = document.createElement("button"); + buttonGO.innerHTML = ' refresh'; + buttonGO.title = "This will refresh the current device"; + buttonGO.id = "requestVideoDevice_" + UUID; + buttonGO.onclick = function () { + var data = {}; + data.changeCamera = videoSelect.value; + data.UUID = UUID; + session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER + }; + + var videoSelectDiv = document.createElement("div"); + query("#container_" + UUID + " .advancedVideoSettings").appendChild(videoSelectDiv); + videoSelectDiv.appendChild(videoSelect); + videoSelectDiv.appendChild(buttonGO); + } + + if (document.getElementById("remoteAudioSelect_" + UUID)) { + log("remoteAudioSelect_ "); + var audioSelect = document.getElementById("remoteAudioSelect_" + UUID); + var length = audioSelect.options.length; + for (i = length - 1; i >= 0; i--) { + audioSelect.options[i] = null; + } + } else { + var audioSelect = document.createElement("select"); + audioSelect.id = "remoteAudioSelect_" + UUID; + + audioSelect.onchange = function () { + log("ON CHANGE"); + if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) { + getById("requestAudioDevice_" + UUID).innerHTML = ' apply'; + getById("requestAudioDevice_" + UUID).title = "This will update the remote device to the selected one"; + } else { + getById("requestAudioDevice_" + UUID).innerHTML = ' request'; + getById("requestAudioDevice_" + UUID).title = "This will ask the remote guest for permission to change"; + } + }; + + var buttonGO = document.createElement("button"); + buttonGO.innerHTML = ' refresh'; + // buttonGO.style = "padding: 5px;"; + buttonGO.title = "This will refresh the current device"; + buttonGO.id = "requestAudioDevice_" + UUID; + + buttonGO.onclick = function () { + var data = {}; + data.changeMicrophone = audioSelect.value; + data.UUID = UUID; + session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER + }; + var audioSelectDiv = document.createElement("div"); + query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioSelectDiv); + audioSelectDiv.appendChild(audioSelect); + audioSelectDiv.appendChild(buttonGO); + } + + if (document.getElementById("remoteAudioOutputSelect_" + UUID)) { + var audioOutputSelect = document.getElementById("remoteAudioOutputSelect_" + UUID); + var length = audioOutputSelect.options.length; + for (i = length - 1; i >= 0; i--) { + audioOutputSelect.options[i] = null; + } + } else { + var audioOutputSelect = document.createElement("select"); + audioOutputSelect.id = "remoteAudioOutputSelect_" + UUID; + + audioOutputSelect.onchange = function () { + if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.consent) { + getById("requestAudioOutputDevice_" + UUID).innerHTML = ' apply'; + getById("requestAudioOutputDevice_" + UUID).title = "This will update the remote device to the selected one"; + } else { + getById("requestAudioOutputDevice_" + UUID).innerHTML = ' request'; + getById("requestAudioOutputDevice_" + UUID).title = "This will ask the remote guest for permission to change"; + } + }; + + var buttonGO = document.createElement("button"); + buttonGO.innerHTML = ' refresh'; + buttonGO.title = "This will refresh the current device"; + buttonGO.id = "requestAudioOutputDevice_" + UUID; + buttonGO.onclick = function () { + var data = {}; + data.changeSpeaker = audioOutputSelect.value; + data.UUID = UUID; + session.sendRequest(data, UUID); // Viewer is requesting the PUBLISHER + }; + + var audioOutputSelectContainer = document.createElement("div"); + query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioOutputSelectContainer); + audioOutputSelectContainer.appendChild(audioOutputSelect); + audioOutputSelectContainer.appendChild(buttonGO); + } + + var matched = false; + var audiomatched = false; + for (let i = 0; i !== deviceInfos.length; ++i) { + const deviceInfo = deviceInfos[i]; + if (deviceInfo == null) { + continue; + } + if (deviceInfo.kind === "videoinput") { + const option = document.createElement("option"); + option.value = deviceInfo.deviceId || "default"; + option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`; + if (getById("remoteVideoLabel_" + UUID).innerText == option.text) { + option.selected = "true"; + matched = true; + } + videoSelect.appendChild(option); + } else if (deviceInfo.kind === "audioinput") { + const option = document.createElement("option"); + option.value = deviceInfo.deviceId || "default"; + option.text = deviceInfo.label || `microphone ${audioSelect.length + 1}`; + if (getById("remoteAudioLabel_" + UUID).innerText == option.text) { + option.selected = "true"; + audiomatched = true; + } + audioSelect.appendChild(option); + } else if (deviceInfo.kind === "audiooutput") { + const option = document.createElement("option"); + option.value = deviceInfo.deviceId || "default"; + option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`; + if (getById("remoteAudioOutputSelect_" + UUID).innerText == option.text) { + option.selected = "true"; + } + audioOutputSelect.appendChild(option); + } + } + + if (!matched) { + getById("requestVideoDevice_" + UUID).innerHTML = ' request'; + getById("requestVideoDevice_" + UUID).title = "This will ask the remote guest for permission to change"; + } + + if (!audiomatched) { + getById("requestAudioDevice_" + UUID).innerHTML = ' request'; + getById("requestAudioDevice_" + UUID).title = "This will ask the remote guest for permission to change"; + } + } catch (e) { + errorlog(e); + } + + pokeIframeAPI("remote-devices-info", deviceInfos, UUID); +} + +var timeoutTone = false; +function playtone(screen = false, tonename = "testtone") { + if (timeoutTone) return; + + if (session.beepToNotify && tonename) { + if (session.beepToNotify !== true) { + let val = tonename.split("tone")[0]; + if (!session.beepToNotify.includes(val)) { + return; + } + } + } + + const type = tonename.replace("tone", ""); + if (session.notifyTypes && !session.notifyTypes.includes(type)) return; + + timeoutTone = true; + setTimeout(() => timeoutTone = false, 500); + + const toneEle = document.getElementById(tonename); + if (!toneEle) return; + + if (iOS || iPad) { + toneEle.muted = false; + toneEle.play().catch(e => console.error('Playback failed:', e)); + return; + } + + if (screen && getById("outputSourceScreenshare")) { + session.sink = getById("outputSourceScreenshare").value; + saveSettings(); + } + + if (session.sink) { + toneEle.setSinkId(session.sink) + .then(() => toneEle.play()) + .catch(() => toneEle.play()); + } else { + toneEle.play(); + } +} + +function updateAudioSource(newUrl, name = "testtone") { + var audioElement = document.getElementById(name); + + if (!audioElement) { + audioElement = createAudioElement(); + audioElement.id = name; + getById("testtone").parentNode.insertBefore(audioElement, getById("testtone").nextSibling); + } + + audioElement.src = newUrl; + + var sources = audioElement.getElementsByTagName("source"); + var extension = newUrl.split(".").pop().toLowerCase(); + var mimeType; + + switch (extension) { + case "mp3": + mimeType = "audio/mpeg"; + break; + case "wav": + mimeType = "audio/wav"; + break; + case "ogg": + mimeType = "audio/ogg"; + break; + case "aac": + case "m4a": + mimeType = "audio/aac"; + break; + case "opus": + mimeType = "audio/opus"; + break; + case "flac": + mimeType = "audio/flac"; + break; + case "webm": + mimeType = "audio/webm"; + break; + default: + console.error("Unsupported file type:", extension); + return; + } + if (sources && sources.length === 1) { + sources[0].src = newUrl; + sources[0].type = mimeType; + } else { + audioElement.innerHTML = ""; + var newSource = document.createElement("source"); + newSource.src = newUrl; + newSource.type = mimeType; + audioElement.appendChild(newSource); + } + audioElement.load(); +} + +function showNotification(title, body = "") { + if (!Notification) { + return; + } + if (Notification.permission !== "granted") { + Notification.requestPermission(); + } else { + let icon = "/media/old_icon.png"; //this is a large image may take more time to show notifiction, replace with small size icon + + let notification = new Notification(title, { body, icon }); + + notification.onclick = () => { + notification.close(); + window.parent.focus(); + }; + } +} + +function toFirefoxConstraint(chromeConstraint) { + if (!chromeConstraint || typeof chromeConstraint !== 'object') { + return chromeConstraint; + } + + const result = { ...chromeConstraint }; + + if (result.audio && typeof result.audio === 'object') { + result.audio = flattenConstraints(result.audio); + } + + if (result.video && typeof result.video === 'object') { + result.video = flattenConstraints(result.video); + } + + return result; +} + +function flattenConstraints(constraints) { + const result = { ...constraints }; + + for (const [key, value] of Object.entries(constraints)) { + if (value && typeof value === 'object') { + if ('exact' in value) { + result[key] = value.exact; + } else if ('ideal' in value) { + result[key] = value.ideal; + } + } + } + + return result; +} + +async function getAudioOnly(selector, trackid = null, override = false, requestToken = null) { + var audioSelect = document.querySelector(selector).querySelectorAll("input,option"); + var audioList = []; + var streams = []; + log("getAudioOnly()"); + + // Fast-path: if override includes a specific deviceId, use it directly + if (override && override.audio && (override.audio.deviceId || (override.audio.deviceId && override.audio.deviceId.exact))) { + let o = JSON.parse(JSON.stringify(override)); + if (typeof o.audio.deviceId === "string") { + o.audio.deviceId = { exact: o.audio.deviceId }; + } + o.video = false; + if (Firefox) { + o = toFirefoxConstraint(o); + } + warnlog("navigator.mediaDevices.getUserMedia starting (override)..."); + if (navigator.mediaDevices) { + var stream = await navigator.mediaDevices + .getUserMedia(o) + .then(function (stream2) { + log("get audio sucecss"); + pokeIframeAPI("local-microphone-event"); + return stream2; + }) + .catch(function (err) { + warnlog(err); + return false; + }); + if (stream) { + if (requestToken !== null && requestToken !== getAudioUserMediaRequestID) { + stream.getTracks().forEach(track => track.stop()); + } else { + streams.push(stream); + } + } + } + return streams; + } + for (var i = 0; i < audioSelect.length; i++) { + if (audioSelect[i].value == "ZZZ") { + continue; + } else if (trackid == audioSelect[i].value) { + // skip already excluded + continue; + } else if ("screen" == audioSelect[i].dataset.type) { + // skip already excluded ---------- !!!!!! DOES THIS MAKE SENSE? TODO: CHECK + continue; + } else if (audioSelect[i].checked) { + log(audioSelect[i]); + audioList.push(audioSelect[i]); + } else if (audioSelect[i].selected) { + log(audioSelect[i]); + audioList.push(audioSelect[i]); + } + } + + // Deduplicate selections so the same physical mic is not captured multiple times. + try { + const uniqueSeen = new Set(); + const groupedByBase = new Map(); + const orderedEntries = []; + + for (var i = 0; i < audioList.length; i++) { + const el = audioList[i]; + const rawLabel = (el.dataset && typeof el.dataset.label !== "undefined" && el.dataset.label) ? el.dataset.label : (el.label || el.text || el.textContent || ""); + let normLabel = normalizeAudioAliasLabel(rawLabel); + const deviceIdLower = (el.value || "").toLowerCase(); + const groupId = (el.dataset && el.dataset.groupId) || ""; + const hasGroupId = !!groupId; + const isDefaultish = (deviceIdLower === "default" || deviceIdLower === "communications"); + + if (!normLabel) { + if (!isDefaultish) { + normLabel = rawLabel ? rawLabel.toLowerCase() : (groupId || deviceIdLower); + } + } + + const baseKey = groupId || normLabel || deviceIdLower || rawLabel; + const groupKey = groupId || normLabel || deviceIdLower || baseKey; + const uniqueKey = (isDefaultish && !groupId) + ? (groupKey + "|alias") + : (groupKey + "|" + ((el.value || "").toLowerCase() || normLabel || rawLabel || "")); + + if (uniqueSeen.has(uniqueKey)) { + continue; + } + uniqueSeen.add(uniqueKey); + + const entry = { + element: el, + isDefaultish, + hasGroupId, + groupKey, + baseKey, + index: null + }; + + let groupMap = groupedByBase.get(baseKey); + if (!groupMap) { + groupMap = new Map(); + groupedByBase.set(baseKey, groupMap); + } + + const existing = groupMap.get(groupKey); + if (!existing) { + entry.index = orderedEntries.length; + orderedEntries.push(entry); + groupMap.set(groupKey, entry); + continue; + } + + let replace = false; + if (existing.isDefaultish && !entry.isDefaultish) { + replace = true; + } else if (!existing.isDefaultish && entry.isDefaultish) { + replace = false; + } else if (!existing.hasGroupId && entry.hasGroupId) { + replace = true; + } + + if (replace) { + entry.index = existing.index; + orderedEntries[existing.index] = entry; + groupMap.set(groupKey, entry); + } + } + + audioList = orderedEntries.map(entry => entry.element); + } catch (e) { /* non-fatal */ } + + for (var i = 0; i < audioList.length; i++) { + if (session.echoCancellation !== false && session.autoGainControl !== false && session.noiseSuppression !== false && (session.voiceIsolation !== true)) { + var constraint = { + audio: { + deviceId: { + exact: audioList[i].value + } + } + }; + } else { + // Just trying to avoid problems with some systems that don't support these features + var constraint = { + audio: { + deviceId: { + exact: audioList[i].value + } + } + }; + if (session.echoCancellation === false) { + constraint.audio.echoCancellation = false; + } else { + constraint.audio.echoCancellation = true; + } + if (session.autoGainControl === false) { + constraint.audio.autoGainControl = false; + } else { + constraint.audio.autoGainControl = true; + } + if (session.noiseSuppression === false) { + constraint.audio.noiseSuppression = false; + } else { + constraint.audio.noiseSuppression = true; + } + if (session.voiceIsolation === true) { + constraint.audio.voiceIsolation = true; + } + } + constraint.video = false; + if (override !== false) { + log("Override true"); + constraint = override; + if (constraint.audio && typeof constraint.audio.deviceId === "string") { + constraint.audio.deviceId = { exact: constraint.audio.deviceId }; + } + } + + if (audioList[i].value && SelectedAudioInputDevices) { + if (SelectedAudioInputDevices.indexOf(audioList[i].value) === -1) { + if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices.push(audioList[i].value); + } + } + + if (session.audioInputChannels) { + if (constraint.audio === true) { + constraint.audio = {}; + constraint.audio.channelCount = session.audioInputChannels; + } else if (constraint.audio) { + constraint.audio.channelCount = session.audioInputChannels; + } + } + + if (session.micSampleRate) { + if (constraint.audio === true) { + constraint.audio = {}; + constraint.audio.sampleRate = parseInt(session.micSampleRate); + } else if (constraint.audio) { + constraint.audio.sampleRate = parseInt(session.micSampleRate); + } + } + + if (session.micSampleSize) { + if (constraint.audio === true) { + constraint.audio = {}; + constraint.audio.sampleSize = parseInt(session.micSampleSize); + } else if (constraint.audio) { + constraint.audio.sampleSize = parseInt(session.micSampleSize); + } + } + + log("CONSTRAINT"); + log(constraint); + + if (Firefox) { + constraint = toFirefoxConstraint(constraint); + } + + warnlog("navigator.mediaDevices.getUserMedia starting..."); + if (navigator.mediaDevices) { + var stream = await navigator.mediaDevices + .getUserMedia(constraint) + .then(function (stream2) { + log("get audio sucecss"); + pokeIframeAPI("local-microphone-event"); + return stream2; + }) + .catch(function (err) { + warnlog(err); + if (!session.cleanOutput) { + if (override !== false) { + if (err.name) { + if (err.constraint) { + warnUser(err["name"] + ": " + err["constraint"]); + } + } + } + } + return false; + }); // More error reporting maybe? + if (stream) { + if (requestToken !== null && requestToken !== getAudioUserMediaRequestID) { + stream.getTracks().forEach(track => track.stop()); + } else { + streams.push(stream); + } + } + } else { + console.warn("navigator.mediaDevices was not found; try a different browser or check your settings"); + } + } + + if (requestToken !== null && requestToken !== getAudioUserMediaRequestID) { + try { + streams.forEach(stream => { + if (!stream) { + return; + } + stream.getTracks().forEach(track => track.stop()); + }); + } catch (e) { } + return []; + } + + return streams; +} + +function applyMirror(mirror) { + // true unmirrors as its already mirrored + if (!session.videoElement) { + return; + } + try { + var transFlip = ""; + var transNorm = ""; + if (document.getElementById("videosource") && session.windowed) { + transFlip = " translate(0, 50%)"; + transNorm = " translate(0, -50%)"; + } + + if (session.mirrored == 2) { + mirror = true; + } else if (session.mirrored === 0) { + mirror = true; + } + + if (!session.videoElement.style) { + session.videoElement.style = ""; + } + + if (session.permaMirrored) { + mirror = !mirror; + } + + if (mirror) { + if (session.mirrored && session.flipped) { + session.videoElement.style.transform = "scaleX(-1) scaleY(-1)" + transFlip; + session.videoElement.classList.add("mirrorControl"); + session.videoElement.dataset.transform = "scaleX(-1) scaleY(-1)"; + } else if (session.mirrored) { + session.videoElement.style.transform = "scaleX(-1)" + transNorm; + session.videoElement.classList.add("mirrorControl"); + session.videoElement.dataset.transform = "scaleX(-1)"; + } else if (session.flipped) { + session.videoElement.style.transform = "scaleY(-1) scaleX(1)" + transFlip; + session.videoElement.classList.remove("mirrorControl"); + session.videoElement.dataset.transform = "scaleX(1) scaleY(-11)"; + } else { + session.videoElement.style.transform = "scaleX(1)" + transNorm; + session.videoElement.classList.remove("mirrorControl"); + session.videoElement.dataset.transform = "scaleX(1)"; + } + } else { + if (session.mirrored && session.flipped) { + session.videoElement.style.transform = "scaleX(1) scaleY(-1)" + transFlip; + session.videoElement.classList.remove("mirrorControl"); + session.videoElement.dataset.transform = "scaleX(1) scaleY(-1)"; + } else if (session.mirrored) { + session.videoElement.style.transform = "scaleX(1)" + transNorm; + session.videoElement.classList.remove("mirrorControl"); + session.videoElement.dataset.transform = "scaleX(1)"; + } else if (session.flipped) { + session.videoElement.style.transform = "scaleY(-1) scaleX(-1)" + transFlip; + session.videoElement.classList.add("mirrorControl"); + session.videoElement.dataset.transform = "scaleX(-1) scaleY(-1)"; + } else { + session.videoElement.style.transform = "scaleX(-1)" + transNorm; + session.videoElement.classList.add("mirrorControl"); + session.videoElement.dataset.transform = "scaleX(-1)"; + } + } + var rotate = 0; + if (session.forceRotate !== false) { + if (session.rotate) { + rotate = session.forceRotate * -1 + parseInt(session.rotate); + } else { + rotate = session.forceRotate * -1; + } + if (session.forceRotate) { + rotate += 180; + } + } else { + rotate = session.rotate; + } + + if (rotate && rotate >= 360) { + rotate -= 360; + } + + session.videoElement.rotated = rotate; + session.videoElement.dataset.rotated = rotate; + + if (document.getElementById("previewWebcam") || document.getElementById("videosource")) { + var eleName = document.getElementById("previewWebcam") || document.getElementById("videosource"); + if (rotate) { + if (eleName.style.transform) { + eleName.style.transform += " rotate(" + rotate + "deg)"; + } else { + eleName.style.transform = "rotate(" + rotate + "deg)"; + } + eleName.classList.add("rotate"); + } else { + eleName.classList.remove("rotate"); + } + } else if (document.getElementById("container")) { + if (rotate == 0) { + document.getElementById("container").classList.remove("rotate"); + document.getElementById("container").style.transform = "unset"; + document.getElementById("container").style.transformOrigin = "unset"; + } else { + document.getElementById("container").style.transform = "rotate(" + rotate + "deg)"; + } + } else if (document.getElementById("minipreview")) { + var eleName = document.getElementById("minipreview"); + if (rotate == 90) { + eleName.style.transform = "rotate(90deg)"; + eleName.style.transformOrigin = "50% 100%"; + eleName.style.height = eleName.style.width; + eleName.style.width = "unset"; + } else if (session.videoElement.rotated == 270) { + eleName.style.transform = "rotate(270deg)"; + eleName.style.transformOrigin = "50% 100%"; + eleName.style.width = "unset"; + eleName.style.height = eleName.style.width; + } else if (session.videoElement.rotated == 180) { + eleName.style.transform = "rotate(180deg)"; + eleName.style.transformOrigin = "unset"; + } else { + eleName.classList.remove("rotate"); + eleName.style.transform = "unset"; + eleName.style.transformOrigin = "unset"; + } + } + // if not one of these, then it's going to be handled by the automixer automatically for us. + } catch (e) { + errorlog(e); + } +} + +function applyMirrorGuest(mirror, videoElement, flip = undefined) { + // true unmirrors as it's already mirrored + try { + const mirrored = !!mirror; + let flipped; + if (flip == null) { // Treats both null and undefined as "preserve existing" + flipped = videoElement.dataset && videoElement.dataset.flipGuest === "true"; + } else { + flipped = !!flip; + } + + if (videoElement.dataset) { + videoElement.dataset.mirrorGuest = mirrored ? "true" : "false"; + videoElement.dataset.flipGuest = flipped ? "true" : "false"; + } + + updateVideoTransform(videoElement); + + if (mirrored) { + videoElement.classList.add("mirrorControl"); + } else { + videoElement.classList.remove("mirrorControl"); + } + } catch (e) { + errorlog(e); + } +} + +// Applies transform (mirror, flip, rotation) to video elements +// Used for BOTH local preview (session.videoElement) AND remote guest videos (session.rpcs[UUID].videoElement) +function updateVideoTransform(videoElement) { + try { + if (!videoElement || !videoElement.style) { + return; + } + const dataset = videoElement.dataset || {}; + let mirrored = false; + let flipped = false; + let hasMirrorControl = false; + let hasFlipControl = false; + + // First priority: explicit dataset flags (from applyMirrorGuest or director controls) + if (dataset.mirrorGuest) { + mirrored = dataset.mirrorGuest === "true"; + hasMirrorControl = true; + } else { + // Second priority: preserve existing inline transform (from applyMirror) + const inlineTransform = videoElement.style.transform || ""; + if (inlineTransform.includes("scaleX(-1)")) { + mirrored = true; + hasMirrorControl = true; + } else if (inlineTransform.includes("scaleX(1)")) { + mirrored = false; + hasMirrorControl = true; + } else { + // Third priority: check computed style to preserve global CSS mirror (from &mirror parameter) + // This ensures rotation doesn't strip CSS-only mirrors + // Uses proper matrix decomposition to handle combined rotate+mirror + try { + const computed = window.getComputedStyle(videoElement); + const matrix = computed.transform; + if (matrix && matrix !== "none") { + const values = matrix.match(/matrix.*\((.+)\)/); + if (values) { + const parts = values[1].split(",").map(parseFloat); + const SCALE_EPS = 0.01; + + if (parts.length === 6) { + // 2D matrix(a, b, c, d, tx, ty) + // Decompose to extract scale with sign, accounting for rotation + const [a, b] = parts; + const scaleX = Math.sign(a || 1) * Math.hypot(a, b); + // Set control if scale differs from 1, or is explicitly -1 + const notOne = Math.abs(Math.abs(scaleX) - 1) > SCALE_EPS; + if (notOne || Math.abs(scaleX + 1) < SCALE_EPS) { + mirrored = scaleX < 0; + hasMirrorControl = true; + } + } else if (parts.length === 16) { + // 3D matrix3d(m00, m01, ..., m15) + // For 3D, scaleX is first column magnitude: sqrt(m00² + m01² + m02²) + const [m00, m01, m02] = parts; + const scaleX = Math.sign(m00 || 1) * Math.hypot(m00, m01, m02); + const notOne = Math.abs(Math.abs(scaleX) - 1) > SCALE_EPS; + if (notOne || Math.abs(scaleX + 1) < SCALE_EPS) { + mirrored = scaleX < 0; + hasMirrorControl = true; + } + } + } + } + } catch (e) { + // Fallback: if computed style fails, leave hasMirrorControl=false + } + } + } + + if (dataset.flipGuest) { + flipped = dataset.flipGuest === "true"; + hasFlipControl = true; + } else { + const inlineTransform = videoElement.style.transform || ""; + if (inlineTransform.includes("scaleY(-1)")) { + flipped = true; + hasFlipControl = true; + } else if (inlineTransform.includes("scaleY(1)")) { + flipped = false; + hasFlipControl = true; + } else { + // Check computed style for flip (scaleY), accounting for rotation + try { + const computed = window.getComputedStyle(videoElement); + const matrix = computed.transform; + if (matrix && matrix !== "none") { + const values = matrix.match(/matrix.*\((.+)\)/); + if (values) { + const parts = values[1].split(",").map(parseFloat); + const SCALE_EPS = 0.01; + + if (parts.length === 6) { + // 2D matrix(a, b, c, d, tx, ty) + // Decompose to extract scaleY with sign, accounting for rotation + const [, , c, d] = parts; + const scaleY = Math.sign(d || 1) * Math.hypot(c, d); + // Set control if scale differs from 1, or is explicitly -1 + const notOne = Math.abs(Math.abs(scaleY) - 1) > SCALE_EPS; + if (notOne || Math.abs(scaleY + 1) < SCALE_EPS) { + flipped = scaleY < 0; + hasFlipControl = true; + } + } else if (parts.length === 16) { + // 3D matrix3d(m00, m01, ..., m15) + // For 3D, scaleY is second column magnitude: sqrt(m04² + m05² + m06²) + const [, , , , m04, m05, m06] = parts; + const scaleY = Math.sign(m05 || 1) * Math.hypot(m04, m05, m06); + const notOne = Math.abs(Math.abs(scaleY) - 1) > SCALE_EPS; + if (notOne || Math.abs(scaleY + 1) < SCALE_EPS) { + flipped = scaleY < 0; + hasFlipControl = true; + } + } + } + } + } catch (e) { + // Fallback: leave hasFlipControl=false + } + } + } + + let rotated = 0; + if (dataset.rotated) { + rotated = parseInt(dataset.rotated) || 0; + } else if (typeof videoElement.rotated !== "undefined" && videoElement.rotated !== false) { + rotated = parseInt(videoElement.rotated) || 0; + } + + const transforms = []; + // Include explicit scaleX/scaleY to preserve: + // 1. Dataset flags (director controls) + // 2. Inline transforms (from applyMirror) + // 3. Computed CSS transforms (from global &mirror) + if (hasMirrorControl) { + if (mirrored) { + transforms.push("scaleX(-1)"); + } else { + transforms.push("scaleX(1)"); + } + } + if (hasFlipControl) { + if (flipped) { + transforms.push("scaleY(-1)"); + } else { + transforms.push("scaleY(1)"); + } + } + if (rotated) { + transforms.push("rotate(" + rotated + "deg)"); + } + + videoElement.style.transform = transforms.join(" "); + } catch (e) { + errorlog(e); + } +} + +function cleanupMediaTracks() { + getUserMediaRequestID += 1; + try { + if (session.streamSrcClone) { + session.streamSrcClone.getTracks().forEach(function (track) { + session.streamSrcClone.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } + if (session.streamSrc) { + session.streamSrc.getTracks().forEach(function (track) { + session.streamSrc.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } else { + checkBasicStreamsExist(); + } + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getTracks().forEach(function (track) { + session.videoElement.srcObject.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } else { + session.videoElement.srcObject = createMediaStream(); + } + activatedPreview = false; + } catch (e) { + errorlog(e); + } + getById("gowebcam").dataset.ready = "false"; + getById("gowebcam").dataset.audioready = "false"; + getById("gowebcam").disabled = true; +} + +/// Detect system changes; handle change or use for debugging +var lastAudioDevice = null; +var lastVideoDevice = null; +var lastPlaybackDevice = null; + +var audioReconnectTimeout = null; +var videoReconnectTimeout = null; +var grabDevicesTimeout = null; +var playbackReconnectTimeout = null; + +function reconnectDevices(event) { + /// TODO: Perhaps change this to only if there is a DISCONNECT; rather than ON NEW DEVICE? + + try { + if (session.firstPlayTriggered && session.audioCtx.state == "suspended") { + session.audioCtx.resume(); + } + } catch (e) { + warnlog("session.audioCtx.resume(); failed"); + } + + warnlog("A media device has changed"); + + if (iOS || iPad) { + // consider adding this back, but if no problem, whatever. + return; + } + + if (document.getElementById("previewWebcam")) { + // rest of the code isn't setup to support pre-connection setup. + clearTimeout(playbackReconnectTimeout); + playbackReconnectTimeout = setTimeout(function () { + if (document.getElementById("previewWebcam")) { + enumerateDevices() + .then(gotDevices) + .then(function () { + if (document.getElementById("previewWebcam")) { + resetupAudioOut(document.getElementById("previewWebcam")); + } + }); + } + }, 1000); + return; + } + + try { + if (!session.streamSrc) { + checkBasicStreamsExist(); + } else { + session.streamSrc.getTracks().forEach(function (track) { + if (track.readyState == "ended") { + if (track.kind == "audio") { + lastAudioDevice = track.label; + } else if (track.kind == "video") { + lastVideoDevice = track.label; + } + session.streamSrc.removeTrack(track); + log("remove ended old track"); + } + }); + + if (session.streamSrcClone) { + session.streamSrcClone.getTracks().forEach(function (track) { + if (track.readyState == "ended") { + log("remove track4"); + session.streamSrcClone.removeTrack(track); + track.stop(); + } + }); + } + + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getTracks().forEach(function (track) { + if (track.readyState == "ended") { + session.videoElement.srcObject.removeTrack(track); + log("remove ended old track"); + } + }); + } + } + /* } else { + clearTimeout(playbackReconnectTimeout); + playbackReconnectTimeout = setTimeout(function() { + enumerateDevices().then(gotDevices2).then(function() { + resetupAudioOut(); + }); + }, 1000); + return; + } */ + } catch (e) { + errorlog(e); + } + + clearTimeout(audioReconnectTimeout); + audioReconnectTimeout = null; + if (lastAudioDevice) { + audioReconnectTimeout = setTimeout(function () { + // only reconnect same audio device. If reconnected, clear the disconnected flag. + enumerateDevices() + .then(gotDevices2) + .then(function () { + // TODO: check to see if any audio is connected? + var streamConnected = false; + var audioSelect = getById("audioSource3").querySelectorAll("input"); + for (var i = 0; i < audioSelect.length; i++) { + if (audioSelect[i].value == "ZZZ") { + continue; + } else if (audioSelect[i].checked) { + log("checked"); + streamConnected = true; + break; + } + } + + if (!streamConnected) { + for (var i = 0; i < audioSelect.length; i++) { + if (audioSelect[i].value == "ZZZ") { + continue; + } + const lastNorm = normalizeAudioAliasLabel(lastAudioDevice || ""); + const currNorm = normalizeAudioAliasLabel(audioSelect[i].dataset.label || ""); + if (lastNorm && lastNorm === currNorm) { + // if the last disconnected device matches. + audioSelect[i].checked = true; + streamConnected = true; + lastAudioDevice = null; + warnlog("DISCONNECTED AUDIO DEVICE RECONNECTED"); + break; + } + } + } + activatedPreview = false; + grabAudio("#audioSource3"); + setTimeout(function () { + enumerateDevices() + .then(gotDevices2) + .then(function () { }); + }, 1000); + }); + }, 2000); + } + + clearTimeout(videoReconnectTimeout); // only reconnect same video device. + videoReconnectTimeout = null; + if (lastVideoDevice) { + videoReconnectTimeout = setTimeout(function () { + enumerateDevices() + .then(gotDevices2) + .then(function () { + var streamConnected = false; + var videoSelect = getById("videoSource3"); + errorlog(videoSelect.value); + + if (videoSelect.value == "ZZZ") { + for (var i = 0; i < videoSelect.options.length; i++) { + try { + if (videoSelect.options[i].innerHTML == lastVideoDevice) { + videoSelect.options[i].selected = "true"; + streamConnected = true; + lastVideoDevice = null; + break; + } + } catch (e) { + errorlog(e); + } + } + } + + if (streamConnected) { + activatedPreview = false; + grabVideo(session.quality, "videosource", "select#videoSource3"); + setTimeout(function () { + enumerateDevices() + .then(gotDevices2) + .then(function () { }); + }, 1000); + } + }); + }, 2000); + } + clearTimeout(playbackReconnectTimeout); + playbackReconnectTimeout = setTimeout(function () { + enumerateDevices() + .then(gotDevices2) + .then(function () { + resetupAudioOut(); + }); + }, 1000); +} + +function handleAudioTrackEnded(event) { + errorlog("Audio track ended unexpectedly"); + + // If there's already a reconnection attempt in progress, don't start another one + if (session.audioReconnectInProgress) { + return; + } + + let modalID = null; + if (!session.cleanOutput) { + warnUser("Your microphone disconnected. Attempting to reconnect...", 3200); + } + + session.audioReconnectInProgress = true; + + // Wait a brief moment to ensure the device has time to be recognized again if it was unplugged/replugged + setTimeout(function () { + activatedPreview = false; + grabAudio("#audioSource3", null, false); + + // Check if reconnection was successful after a delay + setTimeout(function () { + session.audioReconnectInProgress = false; + + closeModal(false, modalID); + + // Check if there are any active audio tracks after reconnection attempt + const hasAudioTracks = session.streamSrc && + session.streamSrc.getAudioTracks && + session.streamSrc.getAudioTracks().length > 0; + + if (!hasAudioTracks) { + // Reconnection failed, open settings menu + if (!session.cleanOutput) { + warnUser("Failed to reconnect your microphone. Please select a different device.", 5000); + } + + // Open the settings menu + if (typeof toggleSettings === 'function') { + toggleSettings(true); // force show the settings + } + } + }, 2000); + }, 1000); +} + +var vingesterFixed = false; +function resetupAudioOut(ele = false, forceReset = false) { + // this re-sets ALL output devices / sources + log("resetupAudioOut"); + if (iOS || iPad || SafariVersion || (ChromiumVersion && session.mobile)) { + // TODO : TEST TO SEE IF THIS WORKS WITH SAFARI? it might. + if (ele) { + return; + } + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement) { + try { + session.rpcs[UUID].videoElement + .pause() + .then(() => { + setTimeout( + function (uuid) { + log("win"); + try { + session.rpcs[uuid].videoElement.play().then(() => { + // updateIncomingAudioElement(uuid); // DO NOT DO THIS; it would cause a loop, as this updateIncomingAudioElement calls reseet + log("toggle pause/play"); + }); + } catch (e) { + errorlog(e); + } + }, + 0, + UUID + ); + }) + .catch(errorlog); + } catch (e) { + warnlog(e); + } + } + } + return; + } + + //if (isVingester){return;} + + var outputSelect = document.getElementById("outputSource") || document.getElementById("outputSource3") || false; + var sinkSet = false; + + try { + // function tries to get vingester working, by listening for its first tweak. + if (!session.sink && !vingesterFixed && document.getElementById("testtone") && document.getElementById("testtone").sinkId && isVingester) { + vingesterFixed = true; // only set the session.sink one time, as vingester on should be needing to set it one time. + session.sink = document.getElementById("testtone").sinkId; + } + } catch (e) { + errorlog(e); + } + + if (outputSelect && outputSelect.options && outputSelect.options.length) { + for (var i = 0; i < outputSelect.options.length; i++) { + if (outputSelect.options[i].value == session.sink) { + outputSelect.options[i].selected = "true"; + sinkSet = true; + } + } + if (sinkSet == false) { + if (outputSelect.options[0]) { + outputSelect.options[0].selected = "true"; + sinkSet = outputSelect.value; + } + } else { + sinkSet = session.sink; + } + } else { + sinkSet = session.sink; + } + + if (ele) { + try { + var eleSink = sinkSet; + if (ele.manualSink) { + eleSink = ele.manualSink; + log("Manual Sink Identified"); + } + if (eleSink) { + if (forceReset) { + ele.setSinkId("default") + .then(() => { + ele.setSinkId(eleSink) + .then(() => { + log("New Output Device"); + }) + .catch(error => { + if (!Firefox) { + warnlog(error); + } + // TODO: If error, then see if I need to add mic support, and grab it if needed. + }); + }) + .catch(error => { + if (!Firefox) { + errorlog(error); + } + ele.setSinkId(eleSink) + .then(() => { + log("New Output Device"); + }) + .catch(error => { + if (!Firefox) { + warnlog(error); + } + // TODO: If error, then see if I need to add mic support, and grab it if needed. + }); + }); + } + + ele.setSinkId(eleSink) + .then(() => { + log("New Output Device for self-preview"); + }) + .catch(error => { + if (!Firefox) { + warnlog("Need to add mic support, and grab it if needed."); + warnlog(error); + } + // TODO: If error, then see if I need to add mic support, and grab it if needed. + }); + } + } catch (e) { + warnlog("can't use setsink"); + } + log("audio sink: " + eleSink); + return; + } + + if (session.videoElement) { + // this would be a preview or videosource + try { + var eleSink = sinkSet; + if (session.videoElement.manualSink) { + eleSink = session.videoElement.manualSink; + } + if (eleSink) { + session.videoElement + .setSinkId(eleSink) + .then(() => { + log("New Output Device for self-preview"); + }) + .catch(error => { + if (!Firefox) { + warnlog(error); + } + // TODO: If error, then see if I need to add mic support, and grab it if needed. + }); + } + } catch (e) { + warnlog("can't use setsink"); + } + } + + for (UUID in session.rpcs) { + try { + if (session.rpcs[UUID].videoElement) { + var eleSink = sinkSet; + if (session.rpcs[UUID].videoElement.manualSink) { + eleSink = session.rpcs[UUID].videoElement.manualSink; + } + if (eleSink) { + session.rpcs[UUID].videoElement + .setSinkId(eleSink) + .then(() => { + log("New Output Device for: " + UUID); + }) + .catch(error => { + if (!Firefox) { + warnlog(error); + } + // TODO: If error, then see if I need to add mic support, and grab it if needed. + }); + } + } + } catch (e) { + warnlog(e); + } + } + log("audio sink 2: " + eleSink); +} + +function obfuscateURL(input) { + if (input.startsWith("https://obs.ninja/")) { + input = input.replace("https://obs.ninja/", "obs.ninja/"); + } else if (input.startsWith("http://obs.ninja/")) { + input = input.replace("http://obs.ninja/", "obs.ninja/"); + } else if (input.startsWith("obs.ninja/")) { + input = input.replace("obs.ninja/", "obs.ninja/"); + } else if (input.startsWith("https://vdo.ninja/")) { + input = input.replace("https://vdo.ninja/", "vdo.ninja/"); + } else if (input.startsWith("http://vdo.ninja/")) { + input = input.replace("http://vdo.ninja/", "vdo.ninja/"); + } else if (input.startsWith("vdo.ninja/")) { + input = input.replace("vdo.ninja/", "vdo.ninja/"); + } + + input = input.replace("&view=", "&v="); + input = input.replace("&view&", "&v&"); + input = input.replace("?view&", "?v&"); + input = input.replace("?view=", "?v="); + + input = input.replace("&videobitrate=", "&vb="); + input = input.replace("?videobitrate=", "?vb="); + input = input.replace("&bitrate=", "&vb="); + input = input.replace("?bitrate=", "?vb="); + + input = input.replace("?audiodevice=", "?ad="); + input = input.replace("&audiodevice=", "&ad="); + + input = input.replace("?label=", "?l="); + input = input.replace("&label=", "&l="); + + input = input.replace("?stereo=", "?s="); + input = input.replace("&stereo=", "&s="); + input = input.replace("&stereo&", "&s&"); + input = input.replace("?stereo&", "?s&"); + + input = input.replace("?webcam&", "?wc&"); + input = input.replace("&webcam&", "&wc&"); + + input = input.replace("?remote=", "?rm="); + input = input.replace("&remote=", "&rm="); + + input = input.replace("?password=", "?p="); + input = input.replace("&password=", "&p="); + + input = input.replace("&maxvideobitrate=", "&mvb="); + input = input.replace("?maxvideobitrate=", "?mvb="); + + input = input.replace("&maxbitrate=", "&mvb="); + input = input.replace("?maxbitrate=", "?mvb="); + + input = input.replace("&height=", "&h="); + input = input.replace("?height=", "?h="); + + input = input.replace("&width=", "&w="); + input = input.replace("?width=", "?w="); + + input = input.replace("&quality=", "&q="); + input = input.replace("?quality=", "?q="); + + input = input.replace("&cleanoutput=", "&clean="); + input = input.replace("?cleanoutput=", "?clean="); + + input = input.replace("&maxviewers=", "&clean="); + input = input.replace("?maxviewers=", "?clean="); + + input = input.replace("&frameRate=", "&fr="); + input = input.replace("?frameRate=", "?fr="); + + input = input.replace("&fps=", "&fr="); + input = input.replace("?fps=", "?fr="); + + input = input.replace("&roomid=", "&r="); + input = input.replace("?roomid=", "?r="); + + input = input.replace("&room=", "&r="); + input = input.replace("?room=", "?r="); + + log(input); + var key = "OBSNINJAFORLIFE"; + var encrypted = CryptoJS.AES.encrypt(input, key); + var output = "https://invite.cam/" + encrypted.toString(); + return output; +} + +function notifyOfScreenShare() { + if (session.notifyScreenShare) { + var data = {}; + data.screenShareState = session.screenShareState; + session.sendMessage(data); + } +} + +var beforeScreenShare = null; // video +var screenShareAudioTrack = null; +async function toggleScreenShare(reload = false) { + /// &sstype=1 + + var quality = session.quality_ss; + + if (quality === false) { + quality = session.roomid ? session.quality_room : session.quality_wb; + } + + if (session.quality !== false) { + quality = session.quality; + } + if (session.screensharequality !== false) { + quality = session.screensharequality; + } + + if (reload) { + // quality = 0, audio = true, videoOnEnd = false) { + await grabScreen(quality, true, true).then(res => { + if (res != false) { + session.screenShareState = true; + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + notifyOfScreenShare(); + + getById("screensharebutton").classList.add("green"); + getById("screensharebutton").ariaPressed = "true"; + enumerateDevices() + .then(gotDevices2) + .then(function () { }); + } + }); + return; + } + if (session.screenShareState == false) { + // adding a screen + await grabScreen(quality, true, true).then(res => { + if (res != false) { + session.screenShareState = true; + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + notifyOfScreenShare(); + + //session.refreshScale(); + + getById("screensharebutton").classList.add("green"); + getById("screensharebutton").ariaPressed = "true"; + enumerateDevices() + .then(gotDevices2) + .then(function () { }); + + //if (session.videoElement.readyState!==4){ + session.videoElement.play().then(() => { + log("start play doublecheck"); + }); + //} + updateMixer(); + pokeIframeAPI("screen-share-state", true); + } + }); + } else { + // removing a screen . session.screenShareState already true true ///////////////////////////////// + + session.screenShareState = false; + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + notifyOfScreenShare(); + + if (!session.streamSrc) { + checkBasicStreamsExist(); + } + + if (screenShareAudioTrack) { + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getAudioTracks().forEach(function (track) { + // previous video track; saving it. Must remove the track at some point. + if (screenShareAudioTrack.id == track.id) { + // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. + log("remove ss track"); + session.videoElement.srcObject.removeTrack(track); + track.stop(); + } + }); + } + + if (session.streamSrcClone) { + // + session.streamSrcClone.getAudioTracks().forEach(function (track) { + if (screenShareAudioTrack.id == track.id) { + // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. + log("remove ss track clone"); + session.streamSrcClone.removeTrack(track); + track.stop(); + } + }); + } + if (session.streamSrc) { + session.streamSrc.getAudioTracks().forEach(function (track) { + // previous video track; saving it. Must remove the track at some point. + if (screenShareAudioTrack.id == track.id) { + // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. + log("remove ss track audio"); + session.streamSrc.removeTrack(track); + track.stop(); + } + }); + } + + session.videoElement.srcObject = outboundAudioPipeline(); // updateREnderOoutput is just for video if videoElement is already activated. + screenShareAudioTrack = null; + senderAudioUpdate(); + } + + var addedAlready = false; + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(function (track) { + if (beforeScreenShare && track.id == beforeScreenShare.id) { + addedAlready = true; + } else { + log("remove ss track 44"); + session.streamSrc.removeTrack(track); + track.stop(); + } + }); + } + + if (session.streamSrcClone) { + session.streamSrcClone.getVideoTracks().forEach(function (track) { + if (beforeScreenShare && track.id == beforeScreenShare.id) { + // + } else { + log("remove ss track 45"); + session.streamSrcClone.removeTrack(track); + track.stop(); + } + }); + } + + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getVideoTracks().forEach(function (track) { + if (beforeScreenShare && track.id == beforeScreenShare.id) { + addedAlready = true; + } else { + log("remove ss track 46"); + session.videoElement.srcObject.removeTrack(track); + track.stop(); + } + }); + } + + getById("screensharebutton").classList.remove("green"); + getById("screensharebutton").ariaPressed = "false"; + + if (beforeScreenShare) { + if (addedAlready == false) { + session.streamSrc.addTrack(beforeScreenShare); // add back in the video track we had before we started screen sharing. It should be NULL if we changed the video track else where (such as via the settings). #TODO: + } + } + + beforeScreenShare = null; + updateRenderOutpipe(); // this syncs the video + + toggleSettings(true); // forceShow + updateMixer(); + } +} + +var ipcRenderer = false; +var ElectronDesktopCapture = false; +var electronAppAudioInstance = null; +var electronAppAudioSupportChecked = false; +var electronAppAudioSupported = false; + +function electronSupportsApplicationAudio() { + if (electronAppAudioSupportChecked) { + return electronAppAudioSupported; + } + electronAppAudioSupportChecked = true; + try { + if (typeof window !== "undefined") { + if (window.WindowAudioStream) { + electronAppAudioSupported = true; + } else if (window.electronApi && typeof window.electronApi.isWindowAudioCaptureAvailable === "function") { + electronAppAudioSupported = !!window.electronApi.isWindowAudioCaptureAvailable(); + } else { + electronAppAudioSupported = false; + } + } + } catch (err) { + console.warn("Failed to determine application audio support:", err); + electronAppAudioSupported = false; + } + return electronAppAudioSupported; +} + +function ensureElectronAppAudioInstance() { + if (!electronSupportsApplicationAudio()) { + return null; + } + if (!electronAppAudioInstance) { + try { + electronAppAudioInstance = new window.WindowAudioStream(); + } catch (err) { + console.error("Failed to create WindowAudioStream instance:", err); + electronAppAudioSupported = false; + electronAppAudioInstance = null; + } + } + return electronAppAudioInstance; +} + +// ASIO Audio Capture Support (Windows only via Electron Capture) +var electronAsioSupportChecked = false; +var electronAsioSupported = false; +var electronAsioDevicesCache = null; +var activeAsioStreams = new Map(); + +function electronSupportsAsio() { + if (electronAsioSupportChecked) { + return electronAsioSupported; + } + electronAsioSupportChecked = true; + try { + if (typeof window !== "undefined" && window.electronApi) { + // Sync API (works with --node flag) + if (typeof window.electronApi.isAsioAvailable === "function") { + try { + electronAsioSupported = !!window.electronApi.isAsioAvailable(); + } catch (e) { + // Sync may fail in sandbox mode, will use async + } + } + } + } catch (err) { + console.warn("Failed to determine ASIO support:", err); + electronAsioSupported = false; + } + return electronAsioSupported; +} + +// Async version for sandbox mode +async function electronSupportsAsioAsync() { + if (electronAsioSupportChecked && electronAsioSupported) { + return electronAsioSupported; + } + try { + if (typeof window !== "undefined" && window.electronApi) { + // Try async first (works in sandbox mode) + if (typeof window.electronApi.isAsioAvailableAsync === "function") { + electronAsioSupported = !!(await window.electronApi.isAsioAvailableAsync()); + electronAsioSupportChecked = true; + return electronAsioSupported; + } + // Fall back to sync (works with --node flag) + if (typeof window.electronApi.isAsioAvailable === "function") { + electronAsioSupported = !!window.electronApi.isAsioAvailable(); + electronAsioSupportChecked = true; + return electronAsioSupported; + } + } + } catch (err) { + console.warn("Failed to determine ASIO support:", err); + } + return false; +} + +function getAsioDevices() { + if (!electronSupportsAsio()) return []; + if (electronAsioDevicesCache !== null) { + return electronAsioDevicesCache; + } + try { + electronAsioDevicesCache = window.electronApi.getAsioDevices() || []; + return electronAsioDevicesCache; + } catch (err) { + console.warn("Failed to get ASIO devices:", err); + electronAsioDevicesCache = []; + return []; + } +} + +// Async version for sandbox mode +async function getAsioDevicesAsync() { + if (electronAsioDevicesCache !== null) { + return electronAsioDevicesCache; + } + try { + if (typeof window !== "undefined" && window.electronApi) { + // Try async first (works in sandbox mode) + if (typeof window.electronApi.getAsioDevicesAsync === "function") { + electronAsioDevicesCache = await window.electronApi.getAsioDevicesAsync() || []; + return electronAsioDevicesCache; + } + // Fall back to sync (works with --node flag) + if (typeof window.electronApi.getAsioDevices === "function") { + electronAsioDevicesCache = window.electronApi.getAsioDevices() || []; + return electronAsioDevicesCache; + } + } + } catch (err) { + console.warn("Failed to get ASIO devices:", err); + } + electronAsioDevicesCache = []; + return []; +} + +function refreshAsioDevices() { + electronAsioDevicesCache = null; + return getAsioDevices(); +} + +async function refreshAsioDevicesAsync() { + electronAsioDevicesCache = null; + return await getAsioDevicesAsync(); +} + +async function createAsioMediaStream(deviceIndex, options = {}) { + // Check ASIO support (try async first for sandbox mode) + const asioAvailable = await electronSupportsAsioAsync(); + if (!asioAvailable) { + throw new Error("ASIO not available"); + } + + const sampleRate = options.sampleRate || 48000; + const channels = options.channels || 2; + const bufferSize = options.bufferSize || 256; + + const audioContext = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: sampleRate + }); + + const destination = audioContext.createMediaStreamDestination(); + + let asioStream; + let audioDataUnsubscribe = null; + let errorUnsubscribe = null; + let useAsyncMode = false; + + // Try async API first (works in sandbox mode) + if (typeof window.electronApi.createAsioStreamAsync === "function") { + try { + asioStream = await window.electronApi.createAsioStreamAsync({ + deviceIndex: deviceIndex, + sampleRate: sampleRate, + channels: channels, + framesPerBuffer: bufferSize + }); + useAsyncMode = true; + } catch (e) { + console.warn("createAsioStreamAsync failed, trying sync:", e); + } + } + + // Fall back to sync API (works with --node flag) + if (!asioStream && typeof window.electronApi.createAsioStream === "function") { + asioStream = window.electronApi.createAsioStream({ + deviceIndex: deviceIndex, + sampleRate: sampleRate, + channels: channels, + framesPerBuffer: bufferSize + }); + } + + if (!asioStream) { + throw new Error("Failed to create ASIO stream"); + } + + let audioBuffer = []; + const minBufferSize = bufferSize * 4; + + function processAudioData(audioData) { + try { + const float32Data = new Float32Array(audioData.buffer || audioData); + audioBuffer.push(...float32Data); + + while (audioBuffer.length >= minBufferSize) { + const chunk = audioBuffer.splice(0, minBufferSize); + const buffer = audioContext.createBuffer(channels, chunk.length / channels, sampleRate); + + for (let ch = 0; ch < channels; ch++) { + const channelData = buffer.getChannelData(ch); + for (let i = 0; i < channelData.length; i++) { + channelData[i] = chunk[i * channels + ch] || 0; + } + } + + const source = audioContext.createBufferSource(); + source.buffer = buffer; + source.connect(destination); + source.start(); + } + } catch (e) { + console.warn("ASIO audio processing error:", e); + } + } + + if (useAsyncMode) { + // Subscribe to audio data via IPC (sandbox mode) + if (typeof window.electronApi.onAsioAudioData === "function") { + audioDataUnsubscribe = window.electronApi.onAsioAudioData((streamId, buffers) => { + if (streamId === asioStream.streamId && buffers && buffers.length > 0) { + // Interleave channels into single buffer + const totalSamples = buffers[0].length * buffers.length; + const interleaved = new Float32Array(totalSamples); + for (let i = 0; i < buffers[0].length; i++) { + for (let ch = 0; ch < buffers.length; ch++) { + interleaved[i * buffers.length + ch] = buffers[ch][i]; + } + } + processAudioData(interleaved); + } + }); + } + if (typeof window.electronApi.onAsioError === "function") { + errorUnsubscribe = window.electronApi.onAsioError((streamId, error) => { + if (streamId === asioStream.streamId) { + console.error("ASIO stream error:", error); + } + }); + } + await asioStream.start(); + } else { + // Use sync event handlers (--node mode) + asioStream.on('data', processAudioData); + asioStream.on('error', (err) => { + console.error("ASIO stream error:", err); + }); + asioStream.start(); + } + + const streamId = "asio_" + deviceIndex + "_" + Date.now(); + activeAsioStreams.set(streamId, { + asioStream: asioStream, + audioContext: audioContext, + mediaStream: destination.stream, + audioDataUnsubscribe: audioDataUnsubscribe, + errorUnsubscribe: errorUnsubscribe, + useAsyncMode: useAsyncMode + }); + + destination.stream.getAudioTracks().forEach(track => { + track.addEventListener('ended', () => { + stopAsioStream(streamId); + }); + }); + + destination.stream._asioStreamId = streamId; + return destination.stream; +} + +function stopAsioStream(streamId) { + const streamData = activeAsioStreams.get(streamId); + if (streamData) { + try { + // Unsubscribe from IPC events (async mode) + if (streamData.audioDataUnsubscribe) { + streamData.audioDataUnsubscribe(); + } + if (streamData.errorUnsubscribe) { + streamData.errorUnsubscribe(); + } + // Stop the ASIO stream + if (streamData.asioStream) { + if (streamData.useAsyncMode && streamData.asioStream.close) { + streamData.asioStream.close(); + } else if (streamData.asioStream.stop) { + streamData.asioStream.stop(); + } + } + if (streamData.audioContext && streamData.audioContext.state !== 'closed') { + streamData.audioContext.close(); + } + } catch (e) { + console.warn("Error stopping ASIO stream:", e); + } + activeAsioStreams.delete(streamId); + } +} + +function stopAllAsioStreams() { + activeAsioStreams.forEach((_, streamId) => { + stopAsioStream(streamId); + }); +} + +// Add ASIO devices to audio input dropdown (handles both sync and async modes) +function addAsioDevicesToDropdown(audioInputSelect, startCounter) { + if (typeof window === "undefined" || !window.electronApi) return; + + var counter = startCounter || 0; + + function appendAsioDevices(asioDevices) { + if (!asioDevices || asioDevices.length === 0) return; + + asioDevices.forEach(function(device) { + // Check if already added + if (audioInputSelect.querySelector('input[value="asio:' + device.index + '"]')) return; + + counter++; + var listele = document.createElement("li"); + listele.style.display = "none"; + + var option = document.createElement("input"); + option.type = "checkbox"; + option.style.display = "none"; + option.value = "asio:" + device.index; + option.name = "multiselecta" + counter; + option.id = "multiselecta" + counter; + option.dataset.label = device.name; + option.dataset.type = "asio"; + + var label = document.createElement("label"); + label.for = option.name; + label.innerHTML = " ASIO " + device.name + " (" + device.maxInputChannels + "ch)"; + label.title = "Hold Ctrl to select multiple"; + + listele.appendChild(option); + listele.appendChild(label); + audioInputSelect.appendChild(listele); + + option.onchange = function (event) { + log("ASIO device selected: " + event.currentTarget.value); + if (!CtrlPressed) { + document.querySelectorAll("#audioSource3 input[type='checkbox']").forEach(function (item) { + if (event.currentTarget.value !== item.value) { + item.checked = false; + if (item.dataset.type == "screen") { + item.parentElement.parentElement.removeChild(item.parentElement); + } + while (SelectedAudioInputDevices.indexOf(item.value) > -1) { + SelectedAudioInputDevices.splice(SelectedAudioInputDevices.indexOf(item.value), 1); + } + } else { + item.checked = true; + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { + if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices.push(event.currentTarget.value); + } + } + }); + } else { + if (SelectedAudioInputDevices.indexOf(event.currentTarget.value) == -1) { + if (SelectedAudioInputDevices.length && SelectedAudioInputDevices.includes("ZZZ")) { + SelectedAudioInputDevices = []; + } + SelectedAudioInputDevices.push(event.currentTarget.value); + } + getById("audioSourceNoAudio2").checked = false; + } + saveSettings(); + }; + }); + + if (asioDevices.length > 0) { + log("ASIO devices added to dropdown:", asioDevices.map(function(d) { return d.name; })); + } + } + + // Try sync first (works with --node flag) + try { + if (electronSupportsAsio()) { + var asioDevices = getAsioDevices(); + if (asioDevices && asioDevices.length > 0) { + appendAsioDevices(asioDevices); + return; // Got devices via sync, done + } + } + } catch (e) { + // Sync failed, will try async + } + + // Try async (works in sandbox mode) + electronSupportsAsioAsync().then(function(supported) { + if (!supported) return; + getAsioDevicesAsync().then(function(asioDevices) { + appendAsioDevices(asioDevices); + }).catch(function(e) { + console.warn("Failed to get ASIO devices async:", e); + }); + }).catch(function(e) { + console.warn("Failed to check ASIO support async:", e); + }); +} +function extractElectronAudioTargetFromSource(source) { + if (!source || !source.id) { + return null; + } + const id = String(source.id); + if (!id.toLowerCase().startsWith("window:")) { + return null; + } + const match = id.match(/window:(\d+)/i); + if (match && match[1]) { + const numericId = parseInt(match[1], 10); + if (!Number.isNaN(numericId) && numericId > 0) { + return numericId; + } + } + return null; +} + +async function attachElectronApplicationAudio(stream, source) { + const targetId = extractElectronAudioTargetFromSource(source); + if (!targetId) { + console.warn("Application audio capture requires a window source."); + return false; + } + const instance = ensureElectronAppAudioInstance(); + if (!instance) { + return false; + } + + try { + const audioStream = await instance.start(targetId); + if (!audioStream) { + return false; + } + + const clonedTracks = []; + audioStream.getAudioTracks().forEach(track => { + const clone = track.clone(); + clonedTracks.push(clone); + stream.addTrack(clone); + }); + + const cleanup = async () => { + clonedTracks.forEach(track => { + try { + track.stop(); + } catch (err) { + console.warn("Failed to stop application audio track:", err); + } + }); + if (instance.isCapturing()) { + try { + await instance.stop(); + } catch (err) { + console.warn("Failed to stop WindowAudioStream:", err); + } + } + }; + + const onceCleanup = () => { + cleanup().catch(err => console.error("Error cleaning up application audio:", err)); + }; + + if (typeof stream.addEventListener === "function") { + stream.addEventListener("inactive", onceCleanup, { once: true }); + } + if (stream && typeof stream.getVideoTracks === "function") { + stream.getVideoTracks().forEach(track => track.addEventListener("ended", onceCleanup, { once: true })); + } + clonedTracks.forEach(track => track.addEventListener("ended", onceCleanup, { once: true })); + + return true; + } catch (err) { + console.error("Failed to attach application audio:", err); + try { + if (instance.isCapturing()) { + await instance.stop(); + } + } catch (stopErr) { + console.warn("Failed to stop WindowAudioStream after error:", stopErr); + } + return false; + } +} + +async function createElectronDesktopAudioStream() { + const new_constraints = { + audio: { + mandatory: { + chromeMediaSource: "desktop" + } + }, + video: { + mandatory: { + chromeMediaSource: "desktop" + } + } + }; + new_constraints.video.mandatory.maxFrameRate = 1; + + const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); + if (stream && typeof stream.getVideoTracks === "function" && stream.getVideoTracks().length) { + const track = stream.getVideoTracks()[0]; + stream.removeTrack(track); + track.stop(); + } + return stream; +} + +if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + // this enables Screen Capture in Electron + try { + if (!ipcRenderer) { + ipcRenderer = require("electron").ipcRenderer; + } + + window.navigator.mediaDevices.getDisplayMedia = (constraints = false) => { + return new Promise(async (resolve, reject) => { + try { + if (session.autostart) { + session.autostart = false; + if (parseInt(session.screenshare) + "" === session.screenshare) { + var sscid = parseInt(session.screenshare) - 1; + if (sscid < 0) { + sscid = 0; + } + //ipcRenderer.sendSync('prompt', {title, val}); + const sources = await ipcRenderer.sendSync("getSources", { types: ["screen"] }); + var new_constraints = { + audio: { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: sources[sscid].id + } + }, + video: { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: sources[sscid].id + } + } + }; + if (session.audioDevice === 0) { + new_constraints.audio = false; + } + try { + if (constraints.video.width.ideal) { + new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal; + } + } catch (e) { } + try { + if (constraints.video.height.ideal) { + new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal; + } + } catch (e) { } + try { + if (constraints.video.frameRate.ideal) { + new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal; + } + } catch (e) { } + /// + //if (Firefox){ // this is electron + // new_constraints = toFirefoxConstraint(new_constraints); + //} + warnlog("navigator.mediaDevices.getUserMedia starting..."); + const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); + resolve(stream); + } else if (session.screenshare && session.screenshare !== true) { + var sscid = null; + const sources = await ipcRenderer.sendSync("getSources", { types: ["window"] }); + for (var i = 0; i < sources.length; i++) { + if (sources[i].name.toLowerCase().startsWith(session.screenshare.toLowerCase())) { + // check if anythign starts with + sscid = i; + break; + } + } + if (sscid === null) { + sscid = 0; // grab first window if nothing. + for (var i = 0; i < sources.length; i++) { + if (sources[i].name.toLowerCase().includes(session.screenshare.toLowerCase())) { + // check if something includes the string; fallback + sscid = i; + break; + } + } + } + /// + var new_constraints = { + audio: { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: sources[sscid].id + } + }, + video: { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: sources[sscid].id + } + } + }; + if (session.audioDevice === 0) { + new_constraints.audio = false; + } + try { + if (constraints.video.width.ideal) { + new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal; + } + } catch (e) { } + try { + if (constraints.video.height.ideal) { + new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal; + } + } catch (e) { } + try { + if (constraints.video.frameRate.ideal) { + new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal; + } + } catch (e) { } + /// + //if (Firefox){ + // new_constraints = toFirefoxConstraint(new_constraints); + //} + warnlog("navigator.mediaDevices.getUserMedia starting..."); + const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); + resolve(stream); + } else { + var sscid = 0; + const sources = await ipcRenderer.sendSync("getSources", { types: ["screen"] }); + /// + var new_constraints = { + audio: { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: sources[sscid].id + } + }, + video: { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: sources[sscid].id + } + } + }; + if (session.audioDevice === 0) { + new_constraints.audio = false; + } + try { + if (constraints.video.width.ideal) { + new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal; + } + } catch (e) { } + try { + if (constraints.video.height.ideal) { + new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal; + } + } catch (e) { } + try { + if (constraints.video.frameRate.ideal) { + new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal; + } + } catch (e) { } + warnlog(new_constraints); + /// + //if (Firefox){ + // new_constraints = toFirefoxConstraint(new_constraints); + //} + warnlog("navigator.mediaDevices.getUserMedia starting..."); + const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); + resolve(stream); + } + } else { + const sources = await ipcRenderer.sendSync("getSources", { types: ["screen", "window"] }); + const selectionElem = document.createElement("div"); + selectionElem.classList = "desktop-capturer-selection"; + if (session.screenshareVideoOnly) { + selectionElem.innerHTML = ` +
    + +
    + `; + } else { + selectionElem.innerHTML = ` +
    + +
    + `; + } + document.body.appendChild(selectionElem); + const systemAudioCheckbox = getById("alsoCaptureAudio"); + const appAudioOption = getById("captureAppAudioParent"); + const appAudioCheckbox = getById("captureAppAudio"); + if (appAudioOption && appAudioCheckbox) { + const supportsAppAudio = !macOS && electronSupportsApplicationAudio(); + if (supportsAppAudio) { + appAudioOption.style.display = "inline-block"; + appAudioCheckbox.addEventListener("change", () => { + if (!systemAudioCheckbox) { + return; + } + if (appAudioCheckbox.checked) { + systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio = systemAudioCheckbox.checked ? "true" : "false"; + if (systemAudioCheckbox.checked) { + systemAudioCheckbox.checked = false; + } + } else { + if (systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio === "true") { + systemAudioCheckbox.checked = true; + } + delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio; + } + }); + } else { + appAudioOption.style.display = "none"; + appAudioCheckbox.checked = false; + } + } + if (systemAudioCheckbox) { + systemAudioCheckbox.addEventListener("change", () => { + if (systemAudioCheckbox.checked && appAudioCheckbox) { + appAudioCheckbox.checked = false; + } + delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio; + }); + } + if (macOS) { + const captureDesktopButton = getById("captureDesktopAudio"); + if (captureDesktopButton) { + captureDesktopButton.style.display = "none"; + } + const alsoCaptureAudioCheckbox = getById("alsoCaptureAudio"); + if (alsoCaptureAudioCheckbox) { + alsoCaptureAudioCheckbox.checked = false; + } + const alsoCaptureAudioParent1 = getById("alsoCaptureAudioParent1"); + if (alsoCaptureAudioParent1) { + alsoCaptureAudioParent1.style.display = "none"; + } + const alsoCaptureAudioParent2 = getById("alsoCaptureAudioParent2"); + if (alsoCaptureAudioParent2) { + alsoCaptureAudioParent2.style.display = "inline-block"; + } + if (appAudioOption) { + appAudioOption.style.display = "none"; + } + if (appAudioCheckbox) { + appAudioCheckbox.checked = false; + } + } + + document.getElementById("cancelscreenshare").addEventListener("click", async () => { + selectionElem.remove(); + reject(null); + }); + document.querySelectorAll(".desktop-capturer-click").forEach(button => { + button.addEventListener("click", async () => { + try { + if (button.id == "captureDesktopAudio") { + const stream = await createElectronDesktopAudioStream(); + resolve(stream); + selectionElem.remove(); + return; + } + const id = button.getAttribute("data-id"); + const source = sources.find(source => source.id === id); + if (!source) { + throw new Error(`Source with id ${id} does not exist`); + } + const systemAudioCheckbox = getById("alsoCaptureAudio"); + const appAudioCheckbox = getById("captureAppAudio"); + const hadSystemAudioPreference = !!(systemAudioCheckbox && (systemAudioCheckbox.checked || systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio === "true")); + const wantsAppAudio = !!(appAudioCheckbox && appAudioCheckbox.checked && electronSupportsApplicationAudio()); + const wantsSystemAudio = !wantsAppAudio && !!(systemAudioCheckbox && systemAudioCheckbox.checked); + + var audioStream = null; + if (wantsSystemAudio) { + audioStream = await createElectronDesktopAudioStream(); + } + + var new_constraints = { + audio: false, + video: { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: source.id + } + } + }; + try { + if (constraints.video.width.ideal) { + new_constraints.video.mandatory.maxWidth = constraints.video.width.ideal; + } + } catch (e) { } + try { + if (constraints.video.height.ideal) { + new_constraints.video.mandatory.maxHeight = constraints.video.height.ideal; + } + } catch (e) { } + try { + if (constraints.video.frameRate.ideal) { + new_constraints.video.mandatory.maxFrameRate = constraints.video.frameRate.ideal; + } + } catch (e) { } + if (typeof warnlog === "function") { + warnlog(new_constraints); + warnlog("navigator.mediaDevices.getUserMedia starting..."); + } + const stream = await window.navigator.mediaDevices.getUserMedia(new_constraints); + let attachedAppAudio = false; + if (wantsAppAudio) { + attachedAppAudio = await attachElectronApplicationAudio(stream, source); + if (!attachedAppAudio && hadSystemAudioPreference) { + if (!audioStream) { + try { + audioStream = await createElectronDesktopAudioStream(); + } catch (audioErr) { + console.warn("Failed to fallback to desktop audio:", audioErr); + } + } + if (audioStream) { + console.warn("Falling back to desktop audio; application audio capture unavailable for selected source."); + if (systemAudioCheckbox) { + systemAudioCheckbox.checked = true; + delete systemAudioCheckbox.dataset.wasCheckedBeforeAppAudio; + } + if (appAudioCheckbox) { + appAudioCheckbox.checked = false; + } + } + } + } + if (!attachedAppAudio && audioStream && typeof audioStream.getAudioTracks === "function") { + const audioTracks = audioStream.getAudioTracks(); + if (audioTracks.length) { + stream.addTrack(audioTracks[0]); + } + } + resolve(stream); + selectionElem.remove(); + } catch (err) { + errorlog("Error selecting desktop capture source:", err); + reject(err); + } + }); + }); + } + } catch (err) { + errorlog("Error displaying desktop capture sources:", err); + reject(err); + } + }); + }; + ElectronDesktopCapture = true; + } catch (e) { + warnlog("Couldn't load electron's screen capture. Elevate the app's permission to allow it (right-click?)"); + } +} + +async function grabScreen(quality = 0, audio = true, videoOnEnd = false) { + if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) { + if (!session.cleanOutput) { + setTimeout(function () { + if (iOS || iPad) { + warnUser(getTranslation("ios-no-screen-share"), false, false); + } else if (session.mobile) { + warnUser(getTranslation("mobile-no-screen-share"), false, false); + } else if (Firefox && !session.mobile) { + warnUser(getTranslation("no-screen-share-supported-firefox"), false, false); + } else { + warnUser(getTranslation("no-screen-share-supported"), false, false); + } + }, 1); + } + return false; + } + + if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + if (!ElectronDesktopCapture) { + if (!session.cleanOutput) { + warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)"); + } + return false; + } + } + + var video = {}; + + if (quality == -1) { + // unlocked capture resolution + } else if (quality == 0) { + video.width = { + ideal: 1920 + }; + video.height = { + ideal: 1080 + }; + } else if (quality == 1) { + video.width = { + ideal: 1280 + }; + video.height = { + ideal: 720 + }; + } else if (quality == 2) { + video.width = { + ideal: 640 + }; + video.height = { + ideal: 360 + }; + } else if (quality >= 3) { + // lowest + video.width = { + ideal: 320 + }; + video.height = { + ideal: 180 + }; + } + + if (session.width) { + video.width = { + ideal: session.width + }; + } + if (session.height) { + video.height = { + ideal: session.height + }; + } + + var constraints = { + // this part is a bit annoying. Do I use the same settings? I can add custom setting controls here later + audio: { + echoCancellation: false, // For screen sharing, we want it off by default. + autoGainControl: false, + noiseSuppression: false + }, + video: video + //,cursor: {exact: "none"} + }; + + try { + let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + if (supportedConstraints.cursor) { + if (session.screensharecursor) { + constraints.video.cursor = ["always", "motion"]; + } else { + constraints.video.cursor = "never"; + } + } + if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) { + constraints.audio.suppressLocalAudioPlayback = true; + } + if (session.preferCurrentTab) { + constraints.preferCurrentTab = true; + } + if (session.selfBrowserSurface) { + constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include + } + if (session.surfaceSwitching) { + constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include + } + if (session.systemAudio) { + constraints.systemAudio = session.systemAudio; // exclude or include + } + if (session.displaySurface && supportedConstraints.displaySurface) { + constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser + } + } catch (e) { + warnlog("navigator.mediaDevices.getSupportedConstraints() not supported"); + } + + if (session.echoCancellation === true) { + constraints.audio.echoCancellation = true; + } + if (session.autoGainControl === true) { + constraints.audio.autoGainControl = true; + } + if (session.noiseSuppression === true) { + constraints.audio.noiseSuppression = true; + } + if (session.voiceIsolation === true) { + constraint.audio.voiceIsolation = true; + } + if (audio == false) { + constraints.audio = false; + } + + var overrideFramerate = false; + if (session.screensharefps !== false) { + constraints.video.frameRate = { + ideal: session.screensharefps, + max: session.screensharefps + }; + } else if (session.frameRate !== false && session.maxframeRate != false) { + overrideFramerate = session.frameRate; + constraints.video.frameRate = { + ideal: session.maxframeRate, + max: session.maxframeRate + }; + } else if (session.frameRate !== false) { + constraints.video.frameRate = session.frameRate; + } else if (session.maxframeRate != false) { + constraints.video.frameRate = { + ideal: session.maxframeRate, + max: session.maxframeRate + }; + } else { + constraints.video.frameRate = { + ideal: 60 + }; + } + + if (session.screenshareVideoOnly) { + constraints.audio = false; + } + + if (session.forceAspectRatio) { + // await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + if (constraints.video && constraints.video !== true) { + constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) }; + + if (constraints.video.width && !session.width) { + delete constraints.video.width; + } else if (constraints.video.height && !session.height) { + delete constraints.video.height; + } + } + } + + if (constraints.video !== false && Object.keys(constraints.video).length == 0) { + constraints.video = true; + } + + var wasDisabled = true; + + return navigator.mediaDevices + .getDisplayMedia(constraints) + .then(async function (stream) { + log("adding video tracks 2245"); + + try { + var constraint = {}; + if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) { + constraint.aspectRatio = parseFloat(session.forceAspectRatio); + } else if (session.forceScreenShareAspectRatio) { + constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio); + } + if (overrideFramerate) { + constraint.frameRate = overrideFramerate; + } + if (Object.keys(constraint).length) { + await stream.getVideoTracks()[0].applyConstraints({ + advanced: [constraint] + }); + log({ + advanced: [constraint] + }); + } + } catch (e) { + errorlog(e); + } + + try { + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(function (track) { + //track.stop(); + beforeScreenShare = track; + session.streamSrc.removeTrack(track); + wasDisabled = false; // + log("stopping video track"); + }); + if (session.streamSrcClone) { + session.streamSrcClone.getVideoTracks().forEach(function (track) { + log("remove ss track clone 11"); + session.streamSrcClone.removeTrack(track); + track.stop(); + }); + } + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getVideoTracks().forEach(function (track) { + //track.stop(); + wasDisabled = false; + session.videoElement.srcObject.removeTrack(track); + log("stopping video track 2"); + }); + } else { + checkBasicStreamsExist(); + } + } else { + checkBasicStreamsExist(); // create srcObject + videoElement + } + } catch (e) { + warnlog(e); + } + + try { + stream.getVideoTracks()[0].onended = function (e) { + // if screen share stops, + warnlog(e); + + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(function (track) { + session.streamSrc.removeTrack(track); + track.stop(); + log("stopping video track 3"); + if (beforeScreenShare && beforeScreenShare.id == track.id) { + beforeScreenShare.stop(); + beforeScreenShare = null; + } + }); + } + + if (session.streamSrcClone) { + session.streamSrcClone.getVideoTracks().forEach(function (track) { + session.streamSrcClone.removeTrack(track); + log("remove ss track clone 14"); + track.stop(); + }); + } + + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getVideoTracks().forEach(function (track) { + session.videoElement.srcObject.removeTrack(track); + track.stop(); + log("stopping video track 4"); + }); + } else { + //session.videoElement.srcObject = createMediaStream(); + session.videoElement.srcObject = outboundAudioPipeline(); + } + + if (screenShareAudioTrack) { + if (session.streamSrc) { + session.streamSrc.getAudioTracks().forEach(function (track) { + // previous video track; saving it. Must remove the track at some point. + if (screenShareAudioTrack.id == track.id) { + // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. + session.streamSrc.removeTrack(track); + track.stop(); + } + }); + } + if (session.streamSrcClone) { + session.streamSrcClone.getAudioTracks().forEach(function (track) { + // previous video track; saving it. Must remove the track at some point. + if (screenShareAudioTrack.id == track.id) { + // since there are more than one audio track, lets see if we can remove JUST the audio track for the screen share. + session.streamSrcClone.removeTrack(track); + log("remove ss track 21"); + track.stop(); + } + }); + } + screenShareAudioTrack = null; + senderAudioUpdate(); + } + + session.screenShareState = false; + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + notifyOfScreenShare(); + + getById("screensharebutton").classList.remove("green"); + getById("screensharebutton").ariaPressed = "false"; + + if (videoOnEnd == true) { + if (beforeScreenShare) { + session.streamSrc.addTrack(beforeScreenShare); // updateRenderOutpipe + beforeScreenShare = null; + } + updateRenderOutpipe(); + toggleSettings(true); // forceshow + } else { + //session.refreshScale(); // since updateREnderOutput already has htis. + } + updateMixer(); + }; + } catch (e) { + log("No Video selected; screensharing?"); + } + + stream.getTracks().forEach(function (track) { + addScreenDevices(track); + session.streamSrc.addTrack(track, stream); // Lets not add the audio to this preview; echo can be annoying + }); + updateRenderOutpipe(); + + if (wasDisabled && stream.getVideoTracks().length && !session.videoMuted) { + var msg = {}; + msg.videoMuted = session.videoMuted; + session.sendMessage(msg); + } + + if (stream.getAudioTracks().length) { + screenShareAudioTrack = stream.getAudioTracks()[0]; + senderAudioUpdate(); + } + + session.applySoloChat(); // mute streams that should be muted if a director + session.applyIsolatedChat(); + + applyMirror(true); + + return true; + }) + .catch(function (err) { + errorlog(err); + errorlog(err.name); + if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") { + // User Stopped it. + if (macOS) { + warnUser(getTranslation("screen-permissions-denied"), false, false); + } + } else { + if (audio == true) { + if (err.name == "NotReadableError") { + if (!session.cleanOutput) { + warnUser(getTranslation("change-audio-output-device"), false, false); + } + setTimeout(function () { + grabScreen(quality, false); + }, 1); + return false; + } else { + setTimeout(function () { + grabScreen(quality, false); + }, 1); + } + } + if (!session.cleanOutput) { + setTimeout( + function (e) { + errorlog(e); + }, + 1, + err + ); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported + } + } + return false; + }); +} +function toggleBufferSettings(UUID) { + getById("bufferSettings").dataset.UUID = UUID; + toggle(getById("bufferSettings")); + if (getById("bufferSettings").style.display == "none") { + getById("modalBackdrop").innerHTML = ""; // Delete modal + getById("modalBackdrop").remove(); + } else { + getById("modalBackdrop").innerHTML = ""; // Delete modal + getById("modalBackdrop").remove(); + zindex = 25; + getById("bufferSettings").style.zIndex = 25; + var modalTemplate = `
    `; + document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + document.getElementById("modalBackdrop").addEventListener("click", toggleBufferSettings); + + var buffer = session.rpcs[UUID].buffer; + if (buffer === false) { + buffer = session.buffer || 0; + } + getById("bufferSettings") + .querySelectorAll("input") + .forEach(ele => { + ele.value = parseInt(buffer); + ele.title = ele.value + " ms"; + //getById("bufferSliderValue").innerText = ele.title + ele.onchange = function (e) { + session.rpcs[UUID].buffer = parseInt(e.target.value); + //getById("bufferSliderValue").innerText = session.rpcs[UUID].buffer + " ms"; + getById("bufferSettings") + .querySelectorAll("input") + .forEach(ele2 => { + if (ele2 !== e.target) { + ele2.value = parseInt(e.target.value); + } + ele2.title = parseInt(e.target.value) + " ms"; + }); + playoutdelay(UUID); // trigger + }; + ele.onkeyup = function (e) { + if (e.key === "Enter") { + session.rpcs[UUID].buffer = parseInt(e.target.value); + //getById("bufferSliderValue").innerText = session.rpcs[UUID].buffer + " ms"; + getById("bufferSettings") + .querySelectorAll("input") + .forEach(ele2 => { + if (ele2 !== e.target) { + ele2.value = parseInt(e.target.value); + } + ele2.title = parseInt(e.target.value) + " ms"; + }); + playoutdelay(UUID); // trigger + } + }; + ele.oninput = function (e) { + getById("bufferSettings") + .querySelectorAll("input") + .forEach(ele2 => { + if (ele2 !== e.target) { + ele2.value = parseInt(e.target.value); + } + ele2.title = parseInt(e.target.value) + " ms"; + }); + }; + }); + } +} + +function togglePTZControls(UUID) { + var modal = getById("ptzControlsModal"); + if (UUID) { + modal.dataset.UUID = UUID; + } + toggle(modal); + + if (modal.style.display == "none") { + if (getById("modalBackdrop")) { + getById("modalBackdrop").innerHTML = ""; + getById("modalBackdrop").remove(); + } + } else { + if (getById("modalBackdrop")) { + getById("modalBackdrop").innerHTML = ""; + getById("modalBackdrop").remove(); + } + zindex = 25; + modal.style.zIndex = 25; + var modalTemplate = `
    `; + document.body.insertAdjacentHTML("beforeend", modalTemplate); + document.getElementById("modalBackdrop").addEventListener("click", function() { + togglePTZControls(); + }); + + var targetUUID = modal.dataset.UUID; + + // Reset sliders to neutral positions + getById("ptzPanSlider").value = 0; + getById("ptzPanValue").innerText = "0"; + getById("ptzTiltSlider").value = 0; + getById("ptzTiltValue").innerText = "0"; + getById("ptzZoomSlider").value = 50; + getById("ptzZoomValue").innerText = "50"; + getById("ptzFocusSlider").value = 0; + getById("ptzFocusValue").innerText = "0"; + + // Pan slider handlers + getById("ptzPanSlider").oninput = function(e) { + getById("ptzPanValue").innerText = e.target.value; + }; + getById("ptzPanSlider").onchange = function(e) { + var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1 + session.requestPanChange(normalizedValue, targetUUID, session.remote, true); + }; + + // Tilt slider handlers + getById("ptzTiltSlider").oninput = function(e) { + getById("ptzTiltValue").innerText = e.target.value; + }; + getById("ptzTiltSlider").onchange = function(e) { + var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1 + session.requestTiltChange(normalizedValue, targetUUID, session.remote, true); + }; + + // Zoom slider handlers + getById("ptzZoomSlider").oninput = function(e) { + getById("ptzZoomValue").innerText = e.target.value; + }; + getById("ptzZoomSlider").onchange = function(e) { + var normalizedValue = parseInt(e.target.value) / 100; // Convert 0..100 to 0..1 + session.requestZoomChange(normalizedValue, targetUUID, session.remote, true); + }; + + // Focus slider handlers + getById("ptzFocusSlider").oninput = function(e) { + getById("ptzFocusValue").innerText = e.target.value; + }; + getById("ptzFocusSlider").onchange = function(e) { + var normalizedValue = parseInt(e.target.value) / 100; // Convert -100..100 to -1..1 + session.requestFocusChange(normalizedValue, targetUUID, session.remote, true); + }; + + // Reset Autofocus button handler + getById("ptzResetAutofocusBtn").onclick = function() { + session.requestAutofocusChange(true, targetUUID, session.remote); + }; + } +} + +function toggleRoomSettings() { + toggle(getById("roomSettings")); + if (getById("roomSettings").style.display == "none") { + //getById("modalBackdrop").innerHTML = ''; // Delete modal + //getById("modalBackdrop").remove(); + } else { + //getById("modalBackdrop").innerHTML = ''; // Delete modal + //getById("modalBackdrop").remove(); + zindex = 25; + getById("roomSettings").style.zIndex = 25; + var modalTemplate = `
    `; + // document.body.insertAdjacentHTML("beforeend", modalTemplate); // Insert modal at body end + // document.getElementById("modalBackdrop").addEventListener("click", toggleRoomSettings); + getById("trbSettingInput").value = session.totalRoomBitrate; + getById("trbSettingInputManual").value = session.totalRoomBitrate; + getById("trbSettingInputFeedback").innerHTML = session.totalRoomBitrate; + + if (session.limitTotalBitrate !== false) { + getById("ltbSettingInputManual").value = session.limitTotalBitrate; + getById("ltbSettingInput").value = session.limitTotalBitrate; + getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled"; + } + + // Show auth access control if in auth mode and user is director + if (session.authMode && session.director && window.vdoAuth) { + getById("authAccessControl").style.display = "block"; + loadRoomAccessSettings(); + } + } +} + +function changeLTB(ele) { + session.limitTotalBitrate = parseInt(ele.value); + + getById("ltbSettingInputManual").value = session.limitTotalBitrate; + getById("ltbSettingInput").value = session.limitTotalBitrate; + getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled"; + + pokeIframeAPI("limit-total-bitrate", session.limitTotalBitrate); + session.limitTotalBitrateGuests(); +} + +function changeTRB(ele) { + session.totalRoomBitrate = parseInt(ele.value); + var msg = {}; + msg.directorSettings = {}; + msg.directorSettings.totalRoomBitrate = session.totalRoomBitrate; + session.sendMessage(msg); + pokeIframeAPI("total-room-bitrate", session.totalRoomBitrate); +} + +function sendMediaDevices(UUID) { + enumerateDevices().then(async function (deviceInfos) { + // Add ASIO devices if available (Windows only via Electron Capture) + try { + var asioAvailable = await electronSupportsAsioAsync(); + if (asioAvailable) { + var asioDevices = await getAsioDevicesAsync(); + if (asioDevices && asioDevices.length > 0) { + asioDevices.forEach(function(device) { + deviceInfos.push({ + deviceId: "asio:" + device.index, + kind: "audioinput", + label: "ASIO: " + device.name + " (" + device.maxInputChannels + "ch)", + groupId: "asio" + }); + }); + } + } + } catch (e) { + // ASIO not available, continue with standard devices + } + var data = {}; + data.UUID = UUID; + data.mediaDevices = deviceInfos; + session.sendMessage(data, data.UUID); + }); +} + +function changeVideoDevice(index, quality = 0) { + enumerateDevices() + .then(gotDevices2) + .then(function () { + activatedPreview = false; + document.getElementById("videoSource3").selectedIndex = index + ""; + grabVideo(quality, "videosource", "#videoSource3"); + }); +} + +function changeAudioDevice(index) { + enumerateDevices() + .then(gotDevices2) + .then(function () { + activatedPreview = false; + var audioSelect = document.getElementById("audioSource3").querySelectorAll("input"); + for (var i = 0; i < audioSelect.length; i++) { + audioSelect[i].checked = false; + } + audioSelect[index - 1].checked = true; + grabAudio("#audioSource3"); + }); +} + +function changeVideoDeviceById(deviceId, UUID = false) { + enumerateDevices() + .then(gotDevices2) + .then(function () { + var opts = document.getElementById("videoSource3").options; + var index = false; + for (var opt, j = 0; (opt = opts[j]); j++) { + if (opt.value == deviceId) { + index = j; + break; + } + } + if (index !== false) { + if (document.getElementById("videoSource3").selectedIndex === index) { + // this is just refreshing the device. + activatedPreview = false; + grabVideo(0, "videosource", "#videoSource3", UUID); + } else if (UUID && !session.consent) { + window.focus(); + confirmAlt("Allow the director to change your video device to:\n\n" + opts[index].text + " ?").then(res => { + if (res) { + document.getElementById("videoSource3").selectedIndex = index; + activatedPreview = false; + grabVideo(0, "videosource", "#videoSource3", UUID); + } else { + try { + var data = {}; + data.UUID = UUID; + data.rejected = "changeCamera"; + session.sendMessage(data, data.UUID); + } catch (e) { } + } + }); + } else { + document.getElementById("videoSource3").selectedIndex = index; + activatedPreview = false; + grabVideo(0, "videosource", "#videoSource3", UUID); + } + } + }); +} + +function changeAudioDeviceById(deviceId, UUID = false) { + enumerateDevices() + .then(gotDevices2) + .then(function () { + var audioSelect = document.getElementById("audioSource3").querySelectorAll("input"); + var matched = false; + var exists = false; + for (var i = 0; i < audioSelect.length; i++) { + if (audioSelect[i].value == deviceId) { + exists = true; + if (audioSelect[i].checked) { + matched = true; + } + } + } + + if (exists) { + if (matched) { + // this is just refreshing the device. + activatedPreview = false; + //grabAudio("#audioSource3", UUID); + grabAudio("#audioSource3", null, false, UUID); + } else if (UUID && !session.consent) { + window.focus(); + confirmAlt("Allow the director to change your audio mic source?").then(res => { + if (res) { + // enumerateDevices().then(gotDevices2).then(function() { + var audioSelect = document.getElementById("audioSource3").querySelectorAll("input"); + for (var i = 0; i < audioSelect.length; i++) { + if (audioSelect[i].value == deviceId) { + audioSelect[i].checked = true; + } else { + audioSelect[i].checked = false; + } + } + activatedPreview = false; + grabAudio("#audioSource3", null, false, UUID); + // }); + } else { + try { + var data = {}; + data.UUID = UUID; + data.rejected = "changeMicrophone"; + session.sendMessage(data, data.UUID); + } catch (e) { } + } + }); + } else { + //enumerateDevices().then(gotDevices2).then(function() { + var audioSelect = document.getElementById("audioSource3").querySelectorAll("input"); + for (var i = 0; i < audioSelect.length; i++) { + if (audioSelect[i].value == deviceId) { + audioSelect[i].checked = true; + } else { + audioSelect[i].checked = false; + } + } + activatedPreview = false; + grabAudio("#audioSource3", null, false, UUID); + // }); + } + } + }); +} + +function changeAudioOutputDeviceById(deviceId, UUID = false) { + // remote control of the speaker output. + warnlog(deviceId); + if (document.getElementById("outputSource3")) { + enumerateDevices() + .then(gotDevices2) + .then(function () { + var index = false; + if (document.getElementById("outputSource3")) { + var opts = document.getElementById("outputSource3").options; + for (var opt, j = 0; (opt = opts[j]); j++) { + if (opt.value == deviceId) { + index = j; + break; + } + } + } + if (index !== false) { + if (document.getElementById("outputSource3").selectedIndex === index) { + // this is just refreshing the device. + session.sink = deviceId; + saveSettings(); + resetupAudioOut(); + } else if (UUID && !session.consent) { + // UUID just lets us inform the requester + window.focus(); + confirmAlt("Allow the director to change your audio's speaker to:\n\n" + opts[index].text + " ?").then(res => { + if (res) { + if (index !== false) { + document.getElementById("outputSource3").selectedIndex = index; + } + session.sink = deviceId; + saveSettings(); + resetupAudioOut(); + var data = {}; + data.UUID = UUID; + sendMediaDevices(data.UUID); + session.sendMessage(data, data.UUID); + } else { + try { + var data = {}; + data.UUID = UUID; + data.rejected = "changeSpeaker"; + session.sendMessage(data, data.UUID); + } catch (e) { } + } + }); + } else { + if (index !== false) { + document.getElementById("outputSource3").selectedIndex = index; + } + session.sink = deviceId; + saveSettings(); + resetupAudioOut(); + } + } + }); + } else { + session.sink = deviceId; + saveSettings(); + resetupAudioOut(); + } +} + +function checkBasicStreamsExist() { + log("checkBasicStreamsExist()"); + if (!session.streamSrc) { + session.streamSrc = createMediaStream(); + } + if (!session.videoElement) { + if (document.getElementById("videosource")) { + session.videoElement = document.getElementById("videosource"); + } else if (document.getElementById("previewWebcam")) { + session.videoElement = document.getElementById("previewWebcam"); + } else { + session.videoElement = createVideoElement(); + } + + session.videoElement.addEventListener( + "playing", + e => { + resetupAudioOut(session.videoElement, true); + }, + { once: true } + ); + + session.videoElement.onpause = event => { + // prevent things from pausing; human or other + if (!(event.ctrlKey || event.metaKey)) { + log("Video paused; auto playing"); + event.currentTarget + .play() + .then(_ => { + log("playing 10"); + }) + .catch(warnlog); + } + }; + + session.videoElement.addEventListener("error", function (event) { + errorlog("video error"); + errorlog(event); + setTimeout(function () { + if (session.videoElement) { + log("Trying to re-load local preview, as it may have crashed"); + session.videoElement.load(); + } + }, 1200); + }); + + //session.videoElement.addEventListener('loadedmetadata', function(event) { + // log("loadedmetadata"); + // log(event); + //}); + } + session.videoElement.srcObject = outboundAudioPipeline(); + toggleMute(true); + return session.videoElement; +} + +var getUserMediaRequestID = 0; +var getAudioUserMediaRequestID = 0; +var grabVideoUserMediaTimeout = null; +var grabVideoTimer = null; + +async function grabVideo(quality = 0, eleName = "previewWebcam", selector = "select#videoSourceSelect", callback = false) { + if (activatedPreview == true) { + log("activated preview return 2"); + return; + } + + if (session.miconly) { + return; + } + + activatedPreview = true; + log("Grabbing video: " + quality); + if (grabVideoTimer) { + clearTimeout(grabVideoTimer); + } + log("element:" + eleName); + + var wasDisabled = true; + try { + if (session.streamSrc) { + if (session.canvasWebGL) { + session.canvasWebGL.remove(); + session.canvasWebGL = null; + } + + if (session.canvasSource) { + session.canvasSource.srcObject.getTracks().forEach(function (trk) { + session.canvasSource.srcObject.removeTrack(trk); + trk.stop(); + wasDisabled = false; + }); + } + if (session.streamSrc) { + session.streamSrc.getVideoTracks().forEach(function (track) { + session.streamSrc.removeTrack(track); + log("remove ss track 9"); + track.stop(); + wasDisabled = false; + }); + } + if (session.streamSrcClone) { + session.streamSrcClone.getVideoTracks().forEach(function (track) { + session.streamSrcClone.removeTrack(track); + log("remove ss track s9"); + track.stop(); + }); + } + } else { + checkBasicStreamsExist(); + log("CREATE NEW STREAM"); + } + + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getVideoTracks().forEach(function (track) { + session.videoElement.srcObject.removeTrack(track); + log("remove ss track 98"); + track.stop(); + session.videoElement.load(); + wasDisabled = false; + }); + } else { + checkBasicStreamsExist(); + } + } catch (e) { + errorlog(e); + } + + session.videoElement.controls = session.showControls || false; + + log("selector: " + selector); + var videoSelect = document.querySelector(selector); // document.querySelector("videoSource3").value == "ZZZ" + log(videoSelect); + var mirror = false; + getById("cameraTip1").classList.add("hidden"); + + if (!videoSelect || videoSelect.value == "ZZZ") { + // if there is no video, or if manually set to audio ready, then do this step. + + clearTimeout(grabVideoUserMediaTimeout); + getUserMediaRequestID += 1; + + warnlog("ZZZ SET - so no VIDEO"); + SelectedVideoInputDevices = []; + saveSettings(); + + if (session.avatar && session.avatar.ready) { + updateRenderOutpipe(); + } else if (session.mobile && needsLegacyWakeLock()) { // OBSOLETE since we now have "WAKE LOCK" API used. + startLegacyKeepAliveLoop(); + } + + if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) { + if (session.autostart) { + publishWebcam(); // no need to mirror as there is no video... + return; + } else { + log("4462"); + updateStats(); + if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").dataset.ready = "true"; + if (document.getElementById("gowebcam").dataset.audioready == "true") { + document.getElementById("gowebcam").disabled = false; + //document.getElementById("gowebcam").innerHTML = getTranslation("start"); + miniTranslate(document.getElementById("gowebcam"), "start"); + document.getElementById("gowebcam").focus(); + } + } + } + } else { + // If they disabled the video but not in preview mode; but actualy live. We will want to remove the stream from the publishing + // we don't want to do this otherwise, as we are "replacing" the track in other cases. + // this does cause a problem, as previous bitrate settings & resolutions might not be applied if switched back.... must test + + if (session.avatar && session.avatar.ready) { + updateRenderOutpipe(); + return; + } + + if (session.chunked) { + for (UUID in session.pcs) { + session.chunkedStream(UUID); // make sure we check that this connection allows video / audio + } + // return; + } + try { + var miscSenders = []; + + if (session.whipOut && session.whipOut.getSenders) { + miscSenders.push(session.whipOut); + } + + miscSenders.forEach(dataRTC => { + if (dataRTC && dataRTC.getSenders) { + dataRTC.getSenders().forEach(sender => { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track && sender.track.kind == "video") { + var trk = getWhipOutCanvasTrack(dataRTC); + if (session.screenShareState && session.screenshareContentHint && trk.kind === "video") { + try { + trk.contentHint = session.screenshareContentHint; + } catch (e) { + errorlog(e); + } + } else if (session.contentHint && trk.kind === "video") { + try { + trk.contentHint = session.contentHint; + } catch (e) { + errorlog(e); + } + } + try { + sender.replaceTrack(trk); // replace may not be supported by all browsers. eek. + } catch (e) { + errorlog(e); + } + } + }); + } + }); + } catch (e) { + errorlog(e); + } + + for (UUID in session.pcs) { + if ("realUUID" in session.pcs[UUID]) { + continue; + } // do not apply to screen shares. + + if (session.chunked && session.pcs[UUID].allowChunked) { + continue; + } + + // for any connected peer, update the video they have if connected with a video already. + var senders = getSenders2(UUID); + senders.forEach(sender => { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track && sender.track.kind == "video") { + sender.track.enabled = false; // I'm not entirely sure if I shoudl be doing this to a video stream... but I suppose new connections won't get a stream, and old connections will just replace it? + getById("mutevideobutton").classList.add("hidden"); // hide the mute button, so they can't unmute while no video. + //session.pcs[UUID].removeTrack(sender); // replace may not be supported by all browsers. eek. + //errorlog("DELETED SENDER"); + } + }); + + } + + var msg = {}; + msg.videoMuted = true; // doesn;t matter if video is actually muted or not; no video is being sent + session.sendMessage(msg); + } + return; + } else { + if (videoSelect && videoSelect.value) { + SelectedVideoInputDevices = [videoSelect.value]; + saveSettings(); + } + + if (session.avatar && session.avatar.timer) { + clearInterval(session.avatar.timer); + session.avatar.timer = null; + } + + var sq = 0; + if (session.quality === false) { + sq = session.roomid ? session.quality_room : session.quality_wb; + } else if (session.quality > 2) { + // 1080, 720, and 360p + sq = 2; // hacking my own code. TODO: ugly, so I need to revisit this. + } else { + sq = session.quality; + } + + if (session.director && quality !== false) { + // URL-based quality won't matter if DIRECTOR; + // quality = quality; + } else if (quality === false || quality < sq) { + quality = sq; // override the user's setting + } + + if ((iOS || iPad) && SafariVersion < 15) { + // iOS will not work correctly at 1080p; likely a h264 codec issue. + if (quality == 0) { + quality = 1; + } + } + + var constraints = { + audio: false, + video: getUserMediaVideoParams(quality, iOS || iPad) + }; + + //if (Firefox){ + // constraints.video.height = constraints.video.height.ideal; + // constraints.video.width = constraints.video.height.ideal; + //} + + log("Quality selected:" + quality); + + if (session.outboundVideoBitrate_userSet === false) { + // default is 2500 + if (session.quality == 0) { // 1080p + session.outboundVideoBitrate = 4000; + } else if (session.quality == -1) { // unlocked + session.outboundVideoBitrate = 4000; + } else if (session.quality == -2) { // 4k + session.outboundVideoBitrate = 8000; + } else if (session.quality == -3) { // 2k + session.outboundVideoBitrate = 6000; + } else { + session.outboundVideoBitrate = false; + } + } + + if (session.facingMode) { + constraints.video.facingMode = { exact: session.facingMode }; // user or environment + } else if (iOS || iPad) { + constraints.video.deviceId = { + exact: videoSelect.value + }; // iPhone 6s compatible ? Needs to be exact for iPhone 6s + } else if (Firefox) { + // is firefox. + constraints.video.deviceId = { + exact: videoSelect.value + }; // Firefox is a dick. Needs it to be exact. + const selectedLabel = videoSelect.options[videoSelect.selectedIndex] ? videoSelect.options[videoSelect.selectedIndex].text : ""; + const isObsCam = selectedLabel.startsWith("OBS-Camera") || selectedLabel.startsWith("OBS Virtual Camera") || selectedLabel.startsWith("Streamlabs "); + if (isObsCam && !session.frameRate && session.maxframeRate == false) { + // Firefox + OBS Virtual Camera can fail or stick on device switches unless a 30fps cap is applied. + // Scope the cap to OBS only so other cameras (eg. Cam Link) retain their native fps/resolution behavior. + constraints.video.frameRate = { + ideal: 30, + max: 30 + }; + } + } else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) { + + // NDI does not like "EXACT" + constraints.video.deviceId = videoSelect.value; // NDI is fucked up + } else { + constraints.video.deviceId = { + exact: videoSelect.value + }; // Default. Should work for Logitech, etc. + } + + if (session.width) { + constraints.video.width = { + exact: session.width + }; // manually specified - so must be exact + } + if (session.height) { + constraints.video.height = { + exact: session.height + }; + } + + if (session.frameRate) { + constraints.video.frameRate = { + exact: session.frameRate + }; + } else if (session.maxframeRate != false) { + constraints.video.frameRate = { + ideal: session.maxframeRate, + max: session.maxframeRate + }; + } else if ((iOS || iPad) && SafariVersion > 15) { + // iOS supports 720p60, but just 1080p30 : iphone 11 on march 2023 + if (quality === 1) { + // iphone 11 and older + if (!constraints.video.frameRate) { + constraints.video.frameRate = { + ideal: 60, + max: 60 + }; + } + } else if (iPhone12Up && quality < 1) { + // iphone 12 and up? + if (!constraints.video.frameRate) { + try { + if (videoSelect.options[videoSelect.selectedIndex].innerText.startsWith("Back ")) { + // front seems to be limited to 720p60 / 1080p30 + constraints.video.frameRate = { + ideal: 60, + max: 60 + }; + } + } catch (e) { + errorlog(e); + } + } + } + } + + if (session.ptz) { + if (constraints.video && constraints.video !== true) { + if (ChromiumVersion && ChromiumVersion > 80) { + constraints.video.pan = true; + constraints.video.tilt = true; + constraints.video.zoom = true; + } + } + } + + if (session.forceAspectRatio) { + // await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + if (constraints.video && constraints.video !== true) { + constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) }; + + if (constraints.video.width && !session.width) { + delete constraints.video.width; + } else if (constraints.video.height && !session.height) { + delete constraints.video.height; + } + } + } + + var obscam = false; + var mirrorcheck = false; + log(videoSelect.options[videoSelect.selectedIndex].text); + + if (!videoSelect.options[videoSelect.selectedIndex]) { + if (session.mobile) { + mirrorcheck = true; + mirror = false; + } else { + mirror = false; + } + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS-Camera")) { + // OBS Virtualcam + mirror = true; + obscam = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("OBS Virtual Camera")) { + // OBS Virtualcam + mirror = true; + obscam = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Streamlabs ")) { + // OBS Virtualcam + mirror = true; + obscam = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Dummy video device")) { + // Linuxv + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("vMix Video")) { + // vMix + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Blackmagic")) { + // Blackmagic devices + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("screen-capture-recorder")) { + // screen-capture-recorder + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" back")) { + // Android + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.includes(" rear")) { + // Android + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.includes("NDI Video")) { + // NDI Virtualcam + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.startsWith("Back Camera")) { + // iPhone and iOS + mirror = true; + } else if (videoSelect.options[videoSelect.selectedIndex].text.toLowerCase().includes("c922")) { + if (session.quality !== 2 && !session.cleanOutput) { + //getById("cameraTipContext1").innerHTML = getTranslation("camera-tip-c922"); + miniTranslate(getById("cameraTipContext1"), "camera-tip-c922"); + getById("cameraTip1").classList.remove("hidden"); + } + } else if (videoSelect.options[videoSelect.selectedIndex].text.toLowerCase().includes("cam link")) { + if (!session.cleanOutput) { + //getById("cameraTipContext1").innerHTML = getTranslation("camera-tip-camlink"); + miniTranslate(getById("cameraTipContext1"), "camera-tip-camlink"); + getById("cameraTip1").classList.remove("hidden"); + } + } else if (session.mobile) { + mirrorcheck = true; + mirror = false; + } else { + mirror = false; + } + + if (SamsungASeries && ChromiumVersion) { + if (!session.cleanOutput) { + //getById("cameraTipContext1").innerHTML = getTranslation("samsung-a-series"); + miniTranslate(getById("cameraTipContext1"), "samsung-a-series"); + getById("cameraTip1").classList.remove("hidden"); + } + } + if (session.nomirror) { + // do not have the camera be mirrored by default, unless using &mirror + session.mirrorExclude = true; + } else { + session.mirrorExclude = mirror; + } + + if (constraints.video && constraints.video !== true && Object.keys(constraints.video).length == 0) { + constraints.video = true; + } else if (constraints.video && constraints.video !== true && Object.keys(constraints.video).length == 1 && "deviceId" in constraints.video && "exact" in constraints.video.deviceId && constraints.video.deviceId.exact === "default") { + constraints.video = true; // solves issues with IOS, where no permission yet given - can't request device ID it seems until permissions is given. + } + + log(constraints); + clearTimeout(grabVideoUserMediaTimeout); + getUserMediaRequestID += 1; + var gumMediaID = getUserMediaRequestID; + var delayStart = 100; + if (ChromiumVersion > 110) { + // aded july 16th; speed up camera switching. + delayStart = 20; + } else if (Firefox) { + delayStart = 500; // cause firefox is buggy as crap + } + grabVideoUserMediaTimeout = setTimeout( + function (gumID, callback2) { + if (getUserMediaRequestID !== gumID) { + return; + } // cancel + + if (Firefox) { + constraints = toFirefoxConstraint(constraints); + } + + warnlog("navigator.mediaDevices.getUserMedia starting..."); + navigator.mediaDevices + .getUserMedia(constraints) + .then(function (stream) { + if (getUserMediaRequestID !== gumID) { + warnlog("GET USER MEDIA CALL HAS EXPIRED"); + stream.getTracks().forEach(function (track) { + stream.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + return; + } + log("adding video tracks 2412"); + stream.getVideoTracks().forEach(async function (track) { + try { + if (mirrorcheck) { + try { + var capabilities = track.getCapabilities(); + } catch (e) { + var capabilities = {}; + } + if ("facingMode" in capabilities) { + if (capabilities.facingMode == "environment") { + session.mirrorExclude = true; + } + } + if ("backgroundBlur" in capabilities) { + // Chrome original trial, until v117, and then??? + query('#effectSelector option[value="13"]').classList.remove("hidden"); + query('#effectSelector option[value="13"]').disabled = null; + query('#effectSelector3 option[value="13"]').classList.remove("hidden"); + query('#effectSelector3 option[value="13"]').disabled = null; + } else { + query('#effectSelector option[value="13"]').disabled = true; + query('#effectSelector3 option[value="13"]').disabled = true; + } + } + } catch (e) { } + session.streamSrc.addTrack(track); // tracks previously removed. + try { + track.onended = function (e) { + // hurrah! + warnlog(e); + refreshVideoDevice(); + }; + } catch (e) { + errorlog(e); + } + if (session.whiteBalance !== false) { + try { + await track.applyConstraints({ advanced: [{ whiteBalanceMode: "manual", colorTemperature: parseInt(session.whiteBalance) }] }); + } catch (e) { + errorlog(e); + try { + await track.applyConstraints({ advanced: [{ whiteBalanceMode: "manual" }] }); + } catch (e) { + warnlog(e); + } + } + } + if (session.exposure !== false) { + try { + await track.applyConstraints({ advanced: [{ exposureMode: "manual", exposureTime: parseInt(session.exposure) }] }); + } catch (e) { + errorlog(e); + try { + await track.applyConstraints({ advanced: [{ exposureMode: "manual" }] }); + } catch (e) { + warnlog(e); + } + } + } + if (session.zoom !== false) { + try { + await track.applyConstraints({ advanced: [{ zoom: parseFloat(session.zoom) }] }); + } catch (e) { + errorlog(e); + } + } + if (session.saturation !== false) { + try { + await track.applyConstraints({ advanced: [{ saturation: parseInt(session.saturation) }] }); + } catch (e) { + errorlog(e); + } + } + if (session.sharpness !== false) { + try { + await track.applyConstraints({ advanced: [{ sharpness: parseInt(session.sharpness) }] }); + } catch (e) { + errorlog(e); + } + } + if (session.contrast !== false) { + try { + await track.applyConstraints({ advanced: [{ contrast: parseInt(session.contrast) }] }); + } catch (e) { + errorlog(e); + } + } + if (session.brightness !== false) { + try { + await track.applyConstraints({ advanced: [{ brightness: parseInt(session.brightness) }] }); + } catch (e) { + errorlog(e); + } + } + if (session.focusDistance !== false) { + try { + await track.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: parseInt(session.focusDistance) }] }); + } catch (e) { + errorlog(e); + try { + await track.applyConstraints({ advanced: [{ focusMode: "manual" }] }); + } catch (e) { + warnlog(e); + } + } + } + if (session.mobile) { + if (!(iPad || iOS || Firefox)) { + try { + applySavedVideoSettings(track); + } catch (e) { + errorlog(e); + } + } + } + }); + if (Firefox && !FirefoxEnumerated) { + if (session.streamSrc && session.streamSrc.getTracks().length) { + FirefoxEnumerated = true; + enumerateDevices().then(gotDevices); + } + } + updateRenderOutpipe(); + // senderAudioUpdate + if (wasDisabled && !session.videoMuted) { + var msg = {}; + msg.videoMuted = session.videoMuted; + session.sendMessage(msg); + } + applyMirror(session.mirrorExclude); + session.videoElement.play().then(() => { + log("play doublecheck completed"); + }); + if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) { + if (session.autostart) { + publishWebcam(); + } else { + log("4620"); + if (document.getElementById("gear_webcam")) { + updateStats(obscam); + } + if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").dataset.ready = "true"; + if (document.getElementById("gowebcam").dataset.audioready == "true") { + document.getElementById("gowebcam").disabled = false; + //document.getElementById("gowebcam").innerHTML = getTranslation("start"); + miniTranslate(document.getElementById("gowebcam"), "start"); + document.getElementById("gowebcam").focus(); + } + } + } + } else if (getById("gear_webcam3").style.display === "inline-block") { + updateStats(obscam); + } + // Once crbug.com/711524 is fixed, we won't need to wait anymore. This is + // currently needed because capabilities can only be retrieved after the + // device starts streaming. This happens after and asynchronously w.r.t. + // getUserMedia() returns. + if (grabVideoTimer) { + clearTimeout(grabVideoTimer); + if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) { + session.videoElement.controls = true; + } + } + if (getById("popupSelector_constraints_video")) { + getById("popupSelector_constraints_video").innerHTML = ""; + } + if (getById("popupSelector_constraints_audio")) { + getById("popupSelector_constraints_audio").innerHTML = ""; + } + if (getById("popupSelector_constraints_loading")) { + getById("popupSelector_constraints_loading").style.display = ""; + } + if (iOS || iPad) { + // TEMPORARY: iOS 15.3 beta fix + toggleSpeakerMute(true); + } + if (!(eleName == "previewWebcam" || document.getElementById("previewWebcam"))) { + updateMixer(); // not with the preview, but after. + } + pokeIframeAPI("local-camera-event"); + let grabVideoPostTimeoutValue = 1000; + if (Firefox || session.mobile) { + // wait longer for these; they are more likely to crash if too quick. + grabVideoPostTimeoutValue = 2000; + } + grabVideoTimer = setTimeout( + async function (callback3, gumid) { + if (getUserMediaRequestID !== gumid) { + // new camera selected in this time. + return; + } + makeImages(true); + if (getById("popupSelector_constraints_loading")) { + getById("popupSelector_constraints_loading").style.display = "none"; + } + if (eleName == "previewWebcam" && document.getElementById("previewWebcam")) { + session.videoElement.controls = true; + try { + var track0 = session.streamSrc.getVideoTracks(); + if (track0.length) { + track0 = track0[0]; + if (track0.getCapabilities) { + session.cameraConstraints = track0.getCapabilities(); + } else { + session.cameraConstraints = {}; + } + log(session.cameraConstraints); + if (track0.getSettings) { + session.currentCameraConstraints = track0.getSettings(); + if (screen && screen.orientation && screen.orientation.type) { + if (screen.orientation.type.includes("portrait")) { + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + } else if (window.matchMedia("(orientation: portrait)").matches) { + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + } else { + session.currentCameraConstraints = {}; + } + log(session.currentCameraConstraints); + } + } catch (e) { + errorlog(e); + } + } else if (toggleSettingsState) { + log("16047"); + updateConstraintSliders(); //listCameraSettings(); + } + if (callback3) { + try { + var data = {}; + data.UUID = callback3; + data.videoOptions = listVideoSettingsPrep(); + sendMediaDevices(data.UUID); + session.sendMessage(data, data.UUID); + } catch (e) { } + } + if (iOS || iPad) { + // TEMPORARY: iOS 15.3 beta fix + toggleSpeakerMute(true); + } + if (session.forceAspectRatio) { + await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + } + updateForceRotate(); // this contains session.setResolution(); + if (iOS || iPad) { + // if we don't do this, portrait videos may be detected as horizontal + if (!document.getElementById("previewWebcam")) { + updateMixer(); // not with the preview, but after. + } + } + try { + if (session.pip3) { + if (!eleName.pip) { + eleName.pip = true; + toggleSystemPip(session.videoElement, true); + } + } + } catch (e) { } + // this will reset scaling for all viewers of this stream. I also call it when aspect ratio, width, or height is changed via applyConstraints + dragElement(session.videoElement); + }, + grabVideoPostTimeoutValue, + callback2, + gumID + ); // focus + log("DONE - found stream"); + }) + .catch(function (e) { + if (getUserMediaRequestID !== gumID) { + warnlog("the previously selected camera attempted failed, but not a big deal, since its now void"); + return; + } + warnlog(e); + if (e.name === "OverconstrainedError") { + warnlog(e.message || e); + log("Resolution or frameRate didn't work"); + } else if (e.name === "NotReadableError") { + if (quality <= 10) { + activatedPreview = false; + grabVideo(quality + 1, eleName, selector); + } else if (session.facingMode) { + session.facingMode = false; + activatedPreview = false; + grabVideo(false, eleName, selector); // restart. + } else { + if (!session.cleanOutput) { + if (iOS) { + warnUser("An error occured. Closing existing tabs in Safari may solve this issue."); + } else { + warnUser("Error: Could not start video source.\n\nTypically this means the Camera is already be in use elsewhere. Most webcams can only be accessed by one program at a time.\n\nTry a different camera or perhaps try re-plugging in the device."); + } + } + activatedPreview = true; + if (getById("gowebcam")) { + getById("gowebcam").innerHTML = "Problem with Camera"; + } + } + return; + } else if (e.name === "NavigatorUserMediaError") { + if (getById("gowebcam")) { + getById("gowebcam").innerHTML = "Problem with Camera"; + } + if (!session.cleanOutput) { + warnUser("Unknown error: 'NavigatorUserMediaError'"); + } + return; + } else if (e.name === "timedOut") { + activatedPreview = true; + if (getById("gowebcam")) { + getById("gowebcam").innerHTML = "Problem with Camera"; + } + if (!session.cleanOutput) { + warnUser(e.message); + } + return; + } else { + errorlog("An unknown camera error occured"); + } + if (quality <= 10) { + activatedPreview = false; + grabVideo(quality + 1, eleName, selector); + } else if (session.facingMode) { + session.facingMode = false; + activatedPreview = false; + grabVideo(false, eleName, selector); // restart. + } else { + errorlog("********Camera failed to work"); + activatedPreview = true; + if (getById("gowebcam")) { + getById("gowebcam").innerHTML = "Problem with Camera"; + } + if (!session.cleanOutput) { + if (session.width || session.height || session.frameRate) { + warnUser(" Camera failed to load.\n\nPlease ensure your camera supports the resolution and frameRate that has been manually specified. Perhaps use &quality=0 instead.", false, false); + } else { + warnUser(" Camera failed to load.\n\nPlease make sure it is not already in use by another application.\n\nPlease make sure you have accepted the camera permissions.", false, false); + } + } + } + }); + }, + delayStart, + gumMediaID, + callback + ); + } +} + +function updateRenderOutpipe() { + // video only. + log("updateRenderOutpipe()"); + + if (session.canvasWebGL) { + session.canvasWebGL.remove(); + session.canvasWebGL = null; + } + + if (session.canvasSource) { + session.canvasSource.srcObject.getTracks().forEach(function (trk) { + session.canvasSource.srcObject.removeTrack(trk); + //trk.stop(); + }); + } + + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getVideoTracks().forEach(function (track) { + session.videoElement.srcObject.removeTrack(track); + log("remove ss track 84"); + //track.stop(); + //session.videoElement.load(); + }); + } else { + checkBasicStreamsExist(); + } + + if (session.streamSrc) { + var tracks = session.streamSrc.getVideoTracks(); + + if (!tracks.length || session.videoMuted) { + tracks = setAvatarImage(tracks); + if (tracks.length) { + if (tracks.length && !session.cleanOutput && !session.cleanish) { + getById("mutevideobutton").classList.remove("hidden"); + } + + tracks.forEach(function (track) { + session.videoElement.srcObject.addTrack(track); + if (session.avatar && session.avatar.tracks) { + var msg = {}; + msg.videoMuted = false; // doesn't matter actual mute state, since its the avatar + session.sendMessage(msg); + } else { + toggleVideoMute(true); + } + pushOutVideoTrack(track); // video only + }); + } else { + var msg = {}; + msg.videoMuted = true; + session.sendMessage(msg); + session.videoElement.load(); + getById("mutevideobutton").classList.add("hidden"); + } + } else if (tracks.length) { + applyMirror(session.mirrorExclude || session.screenShareState); + tracks.forEach(function (track) { + track = applyEffects(track); // updates with the correct track session.streamSrc + session.videoElement.srcObject.addTrack(track); + toggleVideoMute(true); + pushOutVideoTrack(track); // video only + }); + + if (tracks.length && !session.cleanOutput && !session.cleanish) { + getById("mutevideobutton").classList.remove("hidden"); + } + } + } +} + +function pushOutVideoTrack(track) { + log("pushOutVideoTrack"); + + pokeIframeAPI("push-video-track", track.id, false, session.streamID); // (action, value = null, UUID = null, SID=null) + + if (session.chunked) { + for (UUID in session.pcs) { + session.chunkedStream(UUID); // I need to update chunkedStream with the current track? If sstype=3, then skip this + } + } + + if (session.audioContentHint && track.kind === "audio") { + // why am I pushing an audio track? + errorlog("this shouldn't occur, since only video tracks are expected"); + try { + track.contentHint = session.audioContentHint; + } catch (e) { + errorlog(e); + } + } + if (session.screenShareState && session.screenshareContentHint && track.kind === "video") { + // I need to check if this is actually a screenshare before setting the hint (sstype=3) + try { + track.contentHint = session.screenshareContentHint; + } catch (e) { + errorlog(e); + } + } else if (session.contentHint && track.kind === "video") { + try { + track.contentHint = session.contentHint; + } catch (e) { + errorlog(e); + } + } + + if (session.whipOut && session.whipOut.getSenders) { + // should only be 0 or 1 video sender, ever. + //var added = false; + session.whipOut.getSenders().forEach(sender => { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track && sender.track.kind == "video") { + warnlog("Replacing track"); + sender.replaceTrack(track); // replace may not be supported by all browsers. eek. + //sender.track.enabled = true; + //added = true; + } + }); + } + + for (UUID in session.pcs) { + var videoAdded = false; + try { + if ("realUUID" in session.pcs[UUID]) { + continue; + } + if (session.chunked && session.pcs[UUID].allowChunked) { + continue; + } + + if (session.pcs[UUID].guest == true && session.roombitrate === 0) { + log("room rate restriction detected. No videos will be published to other guests"); + } else if (session.pcs[UUID].allowVideo == true) { + // allow + + // for any connected peer, update the video they have if connected with a video already. + var added = false; + var senders = getSenders2(UUID); + senders.forEach(sender => { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (added) { + return; + } + if (sender.track && sender.track.kind == "video") { + sender.replaceTrack(track); // replace may not be supported by all browsers. eek. + log("Track replaced"); + log(track); + sender.track.enabled = true; + added = true; + } + }); + if (added == false) { + videoAdded = true; + session.pcs[UUID].addTrack(track, session.videoElement.srcObject); // can't replace, so adding + setTimeout( + function (uuid) { + session.optimizeBitrate(uuid); + }, + session.rampUpTime, + UUID + ); // 3 seconds lets us ramp up the quality a bit and figure out the total bandwidth quicker + } + } + } catch (e) { + errorlog(e); + } + + if (iOS || iPad) { + ///////// THIS IS A FIX FOR iOS 15.4. When a video is loaded (view/push), the bitrate from iOS devices is stuck low, and resolution needs toggle to fix. + // videoAdded value needs to be deleted from above also + if (SafariVersion && SafariVersion <= 13) { + // + } else if (videoAdded) { + setTimeout( + function (uuid) { + session.setScale(uuid, null); + }, + 2000, + UUID + ); + setTimeout( + function (uuid) { + var scale = 100; + session.setScale; + if (session.pcs[uuid].scale) { + scale = session.pcs[uuid].scale; + } + session.setScale(uuid, scale); + }, + 5000, + UUID + ); + } + } + } + if (track.kind === "audio") { + session.applyIsolatedChat(); + } + session.refreshScale(); +} + +async function grabAudio(selector = "#audioSource", trackid = null, override = false, callbackUUID = false, callback = false) { + // trackid is the excluded track , callback is UUID + + if (activatedPreview == true) { + log("activated preview return 2"); + return; + } + activatedPreview = true; + getAudioUserMediaRequestID += 1; + var gumAudioID = getAudioUserMediaRequestID; + log("TRACK EXCLUDED:" + trackid); + + try { + var baseTest = document.querySelector(selector); + if (!baseTest) { + errorlog("No audio source menu"); + return; + } + if (baseTest && baseTest.tagName == "UL") { + var audioSelect = baseTest.querySelectorAll("input"); + var audioExcludeList = []; + for (var i = 0; i < audioSelect.length; i++) { + try { + if ("screen" == audioSelect[i].dataset.type) { + // skip already excluded ---------- !!!!!! DOES THIS MAKE SENSE? TODO: CHECK + if (audioSelect[i].checked) { + audioExcludeList.push(audioSelect[i]); + } + } + } catch (e) { + errorlog(e); + } + } + } else if (baseTest && baseTest.tagName == "SELECT") { + var audioExcludeList = []; + var audioSelect = baseTest.options; + for (var i = 0; i < audioSelect.length; i++) { + try { + if ("screen" == audioSelect[i].dataset.type) { + // skip already excluded ---------- !!!!!! DOES THIS MAKE SENSE? TODO: CHECK + if (audioSelect[i].selected) { + audioExcludeList.push(audioSelect[i]); + } + } + } catch (e) { + errorlog(e); + } + } + } + } catch (e) { + errorlog(e); + } + + try { + if (session.videoElement && session.videoElement.srcObject) { + session.videoElement.srcObject.getAudioTracks().forEach(function (track) { + // TODO: Confirm that I even need this? + for (var i = 0; i < audioExcludeList.length; i++) { + try { + if (audioExcludeList[i].label == track.label) { + warnlog("DONE"); + return; + } + } catch (e) { + errorlog(e); + } + } + if (trackid && track.id == trackid) { + warnlog("SKIPPED EXCLUDED TRACK?"); + return; + } + session.videoElement.srcObject.removeTrack(track); + log("remove ss track67"); + track.stop(); // remove then stop. + }); + } else { + // if no stream exists + checkBasicStreamsExist(); + } + } catch (e) { + errorlog(e); + } + + try { + if (session.streamSrc) { + session.streamSrc.getAudioTracks().forEach(function (track) { + for (var i = 0; i < audioExcludeList.length; i++) { + try { + if (audioExcludeList[i].label == track.label) { + warnlog("EXCLUDING TRACK; PROBABLY SCREEN SHARE"); + return; + } + } catch (e) { + errorlog(e); + } + } + if (trackid && track.id == trackid) { + warnlog("SKIPPED EXCLUDED TRACK?"); + return; + } + session.streamSrc.removeTrack(track); + track.stop(); + }); + } else { + // if no stream exists + checkBasicStreamsExist(); + } + } catch (e) { + errorlog(e); + } + + try { + if (session.streamSrcClone) { + session.streamSrcClone.getAudioTracks().forEach(function (track) { + for (var i = 0; i < audioExcludeList.length; i++) { + try { + if (audioExcludeList[i].label == track.label) { + warnlog("EXCLUDING TRACK; PROBABLY SCREEN SHARE"); + return; + } + } catch (e) { + errorlog(e); + } + } + if (trackid && track.id == trackid) { + warnlog("SKIPPED EXCLUDED TRACK?"); + return; + } + log("remove ss track 55"); + session.streamSrcClone.removeTrack(track); + track.stop(); + }); + } + } catch (e) { + errorlog(e); + } + + var streams = await getAudioOnly(selector, trackid, override, gumAudioID); // Get audio streams + + if (gumAudioID !== getAudioUserMediaRequestID) { + try { + streams.forEach(stream => { + if (!stream) { + return; + } + stream.getTracks().forEach(track => track.stop()); + }); + } catch (e) { } + activatedPreview = false; + return; + } + + try { + log("STREAMS: " + streams.length); + + for (var i = 0; i < streams.length; i++) { + streams[i].getAudioTracks().forEach(function (track) { + try { + if (gumAudioID !== getAudioUserMediaRequestID) { + track.stop(); + return; + } + session.streamSrc.addTrack(track); // add video track to the preview video + + track.onended = handleAudioTrackEnded; // Add event listener for track end + + log("ok?"); + // applySavedAudioSettings(track); ## this doesn't work as echo-cancellation(+) needs to be applied via getuserMedia only. + } catch (e) { + errorlog(e); + } + }); + } + } catch (e) { + errorlog(e); + } + if (Firefox && !FirefoxEnumerated) { + if (session.streamSrc && session.streamSrc.getTracks().length) { + FirefoxEnumerated = true; + enumerateDevices().then(gotDevices); + } + } + + if (callback) { + callback(); + } + + senderAudioUpdate(callbackUUID); + + try { + if (session.streamSrc && session.streamSrc.getVideoTracks && session.streamSrc.getVideoTracks().length) { + var previewVideoCount = 0; + if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getVideoTracks) { + previewVideoCount = session.videoElement.srcObject.getVideoTracks().length; + } + if (!previewVideoCount && typeof updateRenderOutpipe === "function") { + updateRenderOutpipe(); + } + } + } catch (e) { + errorlog(e); + } +} + +session.toggleSoloChat = function (UUID, event = false) { + // ==> applyIsolatedChat -- this should be trigger by the director only I think + + if (session.director) { + if (!session.directorEnabledPPT) { + warnUser("Enable the director's microphone first.", 2000); + return false; + } + } + + if (Firefox) { + warnlog("Solo talk support for Firefox is currently experimental"); + } + + var msg = {}; + msg.micIsolate = false; + + if (session.soloChatUUID.includes(UUID)) { + // already added, so lets toggle off + session.soloChatUUID.splice(session.soloChatUUID.indexOf(UUID), 1); // Toggles. Adds target to soloChatUUID list + msg.lowerVolume = false; + } else { + session.soloChatUUID.push(UUID); //not added, so lets toggle on + msg.lowerVolume = true; + } + + if (event) { + if (event.ctrlKey || event.metaKey) { + if (session.soloChatUUID.includes(UUID)) { + msg.micIsolate = 1; + } + } + } + + session.sendRequest(msg, UUID); + + log(session.soloChatUUID); + + var ele = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + UUID + '"]'); // [data--u-u-i-d="'+UUID+'"] // this all just updates the buttons + log(ele); + + var ret = 0; + + if (session.soloChatUUID.includes(UUID)) { + if (msg.micIsolate) { + ret = 2; + ele.classList.add("altpress"); // we will do this later. + ele.value = 2; + } else { + ret = 1; + ele.value = 1; + } + } else { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + ele.classList.remove("altpress"); + ele.value = 0; + } + + session.applySoloChat(false); + return ret; +}; +/////////////////////// + +session.togglePrivateChat = function (ele) { + var msg = {}; + warnlog(ele); + if (ele.value == 0) { + msg.micIsolate = true; + ele.value = 1; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } else { + msg.micIsolate = false; + ele.value = 0; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } + session.sendRequest(msg, ele.dataset.UUID); + warnlog(msg); +}; + +// we call this via session.applyIsolatedChat, just in case +session.applyIsolatedVolume = function () { + // mutes outbound mic audio; for guests, and not the director + + var i = session.lowerVolume.length; + while (i--) { + if (!(session.lowerVolume[i] in session.rpcs)) { + // clean up dead connections + session.lowerVolume.splice(i, 1); + } + } + + var soloMode = false; + + /* if (!(session.cleanOutput)){ + if (session.lowerVolume.length){ + getById("header").classList.add('orange'); + getById("head6").classList.remove('hidden'); + } else if (session.audioGain === 0){ + // do nothing. + } else { + getById("header").classList.remove('orange'); + getById("head6").classList.add('hidden'); + } + } */ + + if (session.lowerVolume.length) { + soloMode = true; + } + + if (soloMode) { + for (var UUID in session.rpcs) { + if (session.lowerVolume.includes(UUID)) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume !== false) { + // isolated + session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume; + session.rpcs[UUID].savedVolume = false; + } + continue; + } + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume == false) { + // not isolated + session.rpcs[UUID].savedVolume = session.rpcs[UUID].videoElement.volume; + session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume * 0.25; + } + } + } else { + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].videoElement && session.rpcs[UUID].savedVolume !== false) { + // isolated + session.rpcs[UUID].videoElement.volume = session.rpcs[UUID].savedVolume; + session.rpcs[UUID].savedVolume = false; + } + } + } +}; + +session.applyIsolatedChat = function (UUID = false) { + // mutes outbound mic audio; for guests, and not the director + log("applyIsolatedChat"); + session.applyIsolatedVolume(); // this toggle the speaker output + + var i = session.micIsolated.length; + while (i--) { + if (!(session.micIsolated[i] in session.pcs) && !(session.micIsolated[i] in session.rpcs)) { + session.micIsolated.splice(i, 1); + } + } + + var muteList = [...session.micIsolated]; // one thing I hate about Javascript. Doesn't actually copy arrays. + var soloMode = false; + + if (session.micIsolatedAutoMute) { + // session.micIsolatedAutoMute + soloMode = true; + session.micIsolatedAutoMute.forEach(uid => { + if (!muteList.includes(uid) && (uid in session.rpcs || uid in session.pcs)) { + muteList.push(uid); + } + }); + } + + if (muteList.length) { + soloMode = true; + } + + if (!session.cleanOutput) { + if (soloMode) { + getById("header").classList.add("orange"); + getById("head6").classList.remove("hidden"); + } else if (session.audioGain === 0) { + // do nothing. + } else { + getById("header").classList.remove("orange"); + getById("head6").classList.add("hidden"); + } + } + + ///// + if (session.directorSpeakerMuted !== null) { + for (var uuid in session.rpcs) { + try { + var receivers = getReceivers2(uuid); //session.rpcs[uuid].getReceivers(); + for (var i = 0; i < receivers.length; i++) { + if (receivers[i].track.kind == "audio") { + receivers[i].track.enabled = true; // Chrome 133+ fix: must enable before disabling + receivers[i].track.enabled = !session.directorSpeakerMuted; + } + } + } catch (e) { + errorlog(e); + } + } + if (session.directorSpeakerMuted) { + getById("videosource").muted = true; + } + } + ////////////// + + if (UUID) { + try { + var senders = getSenders2(UUID); + senders.forEach(sender => { + if (!sender.track) { + return; + } + if (sender.track.kind !== "audio") { + return; + } + + var settings = {}; + if (!soloMode) { + settings.active = true; + session.pcs[UUID].audioMutedOverride = false; + } else if (muteList.indexOf(UUID) >= 0) { + settings.active = true; + session.pcs[UUID].audioMutedOverride = false; + } else { + log("MUTING via session.applyIsolatedChat"); + settings.active = false; + session.pcs[UUID].audioMutedOverride = true; + } + setEncodings(sender, settings); + }); + } catch (e) { + errorlog(e); + } + } else { + for (var UUID in session.pcs) { + try { + var senders = getSenders2(UUID); + senders.forEach(sender => { + if (!sender.track) { + return; + } + if (sender.track.kind !== "audio") { + return; + } + + var settings = {}; + if (!soloMode) { + settings.active = true; + session.pcs[UUID].audioMutedOverride = false; + } else if (muteList.indexOf(UUID) >= 0) { + settings.active = true; + session.pcs[UUID].audioMutedOverride = false; + } else { + log("MUTING via session.applyIsolatedChat"); + settings.active = false; + session.pcs[UUID].audioMutedOverride = true; + } + setEncodings(sender, settings); + }); + } catch (e) { + errorlog(e); + } + } + } +}; + +var FirefoxSenders = {}; + +function setEncodings(sender, settings = null, callback = null, cbarg = null) { + if (!settings) { + if (!sender.encodingsQueue) { + // not set + return; + } else if (!sender.encodingsQueue.length) { + // none left + return; + } + } else if (!("encodingsQueue" in sender)) { + sender.encodingsQueue = [[settings, callback, cbarg]]; + } else { + sender.encodingsQueue.push([settings, callback, cbarg]); + } + + if (sender.encodingsQueueActive) { + return; + } + + try { + sender.encodingsQueueActive = true; // we're now busy. + + var options = sender.encodingsQueue.shift(); + settings = options[0]; + callback = options[1]; + cbarg = options[2]; + + const params = sender.getParameters(); + if (!params.encodings || params.encodings.length == 0) { + params.encodings = [{}]; + } + var changed = false; + for (var setting in settings) { + if (settings[setting] === null) { + if (setting in params.encodings[0]) { + delete params.encodings[0][setting]; + changed = true; + } + } else { + if (setting in params.encodings[0]) { + if (params.encodings[0][setting] !== settings[setting]) { + changed = true; + } + } else { + changed = true; + } + params.encodings[0][setting] = settings[setting]; + } + } + + log(settings); + + // if old Firefox, see if I can do something other than Active? + + if (!changed && !Firefox && !SafariVersion) { + log("SET ENCODINGS MATCH INPUT; skipping"); + if (callback) { + if (cbarg) { + setTimeout(function () { + callback(cbarg); + }, 0); + } else { + setTimeout(function () { + callback(); + }, 0); + } + } + sender.encodingsQueueActive = false; + setEncodings(sender); + return; + } + + if (Firefox && !(Firefox >= 110)) { + // https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpEncodingParameters now supported in v110, but old versions will need this function still + if ("active" in settings) { + warnlog("Firefox does not support track active state. We will use enable/disable for that instead."); + if (FirefoxSenders.sender) { + if (FirefoxSenders.sender.lastState === false) { + FirefoxSenders.sender.activeState = settings.active; + // already set to false, so should stay disabled + } else { + FirefoxSenders.sender.activeState = settings.active; + sender.track.enabled = settings.active; // either true or false + } + } else { + FirefoxSenders.sender = { lastState: sender.track.enabled, activeState: settings.active }; + sender.track.enabled = settings.active; + } + + delete settings.active; + if (!Object.keys(settings).length) { + if (callback) { + if (cbarg) { + setTimeout(function () { + callback(cbarg); + }, 0); + } else { + setTimeout(function () { + callback(); + }, 0); + } + } + log("COMPELTED FIREFOX SET ENCODINGS"); + sender.encodingsQueueActive = false; + setEncodings(sender); + return; + } + } + } else if (Firefox) { + // Firefox , all versions, don't support active state with audio yet?? GAhhhhhhhh! + if ("track" in sender && "kind" in sender.track && sender.track.kind == "audio") { + if ("active" in settings) { + warnlog("Firefox does not support track active state with AUDIO yet... We will use enable/disable for that instead."); + if (FirefoxSenders.sender) { + if (FirefoxSenders.sender.lastState === false) { + FirefoxSenders.sender.activeState = settings.active; + // already set to false, so should stay disabled + } else { + FirefoxSenders.sender.activeState = settings.active; + sender.track.enabled = settings.active; // either true or false + } + } else { + FirefoxSenders.sender = { lastState: sender.track.enabled, activeState: settings.active }; + sender.track.enabled = settings.active; + } + + delete settings.active; + if (!Object.keys(settings).length) { + if (callback) { + if (cbarg) { + setTimeout(function () { + callback(cbarg); + }, 0); + } else { + setTimeout(function () { + callback(); + }, 0); + } + } + log("COMPELTED FIREFOX SET ENCODINGS"); + sender.encodingsQueueActive = false; + setEncodings(sender); + return; + } + } + } + } + + sender + .setParameters(params) + .then(() => { + if (callback) { + if (cbarg) { + setTimeout(function () { + callback(cbarg); + }, 0); + } else { + setTimeout(function () { + callback(); + }, 0); + } + } + sender.encodingsQueueActive = false; + setEncodings(sender); + }) + .catch(e => { + errorlog(e); + sender.encodingsQueueActive = false; + setEncodings(sender); + }); + } catch (e) { + errorlog(e); + sender.encodingsQueueActive = false; + } +} + +session.applySoloChat = function (apply = true) { + // mutes outbound mic audio; ;; does the actual solo chat muting for the director + if (session.director === false) { + session.applyIsolatedChat(); + return; + } else if (!session.directorEnabledPPT) { + return; + } + + log("applySoloChat()"); + + var i = session.soloChatUUID.length; + while (i--) { + if (!(session.soloChatUUID[i] in session.pcs)) { + session.soloChatUUID.splice(i, 1); + log("splicing out: " + i); + } + } + + for (var uuid in session.pcs) { + // not sure what to do here wrt to screen tracks + try { + var senders = getSenders2(uuid); + senders.forEach(sender => { + if (!sender.track) { + return; + } + if (sender.track.kind !== "audio") { + return; + } + + var settings = {}; + + if (session.soloChatUUID.length && session.soloChatUUID.includes(uuid)) { + settings.active = true; + setEncodings( + sender, + settings, + function (uid) { + log("2: " + uid); + var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]'); + if (button) { + button.classList.add("pressed"); + button.ariaPressed = "true"; + button.classList.remove("hint"); + } + }, + uuid + ); + } else if (session.soloChatUUID.length == 0) { + settings.active = true; + setEncodings( + sender, + settings, + function (uid) { + log(uid); + var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]'); + if (button) { + button.classList.remove("pressed"); + button.ariaPressed = "false"; + button.classList.remove("hint"); + } + }, + uuid + ); + } else { + settings.active = false; + setEncodings( + sender, + settings, + function (uid) { + warnlog("muted the output to:" + uid); + var button = document.querySelector('[data-action-type="solo-chat"][data--u-u-i-d="' + uid + '"]'); + if (button) { + button.classList.remove("pressed"); + button.ariaPressed = "false"; + button.classList.add("hint"); + } + }, + uuid + ); + } + }); + } catch (e) { + errorlog(e); + } + } + if (apply == false) { + if (session.soloChatUUID.length) { + session.muted_savedState = session.muted; + session.muted = false; + data = {}; + data.muteState = session.muted; + for (var i = 0; i < session.soloChatUUID.length; i++) { + session.sendMessage(data, session.soloChatUUID[i]); + } + } else { + session.muted = session.muted_savedState; + } + toggleMute(true); + } +}; + +function senderAudioUpdate(callbackUUID = false, videoSource = null) { + try { + let tracks = []; + + if (!videoSource) { + checkBasicStreamsExist(); + videoSource = session.videoElement.srcObject; + } + + tracks = videoSource.getAudioTracks(); + + if (session.audioContentHint && tracks.length) { + tracks.forEach(trk => { + try { + trk.contentHint = session.audioContentHint; + } catch (e) { + errorlog(e); + } + }); + } + + if (session.whipOut && session.whipOut.getSenders && tracks.length) { + // mixMinus won't work with meshcast, so don't bother. + session.whipOut.getSenders().forEach(sender => { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track && sender.track.kind == "audio") { + tracks.forEach(trk => { + sender.replaceTrack(trk); + }); + } + }); + } + + for (UUID in session.pcs) { + if ("realUUID" in session.pcs[UUID]) { + continue; + } // do not process the screen share audio + if (session.chunked && session.pcs[UUID].allowChunked && (session.pcs[UUID].allowChunked !== 2)) { + continue; + } + + if (session.pcs[UUID].allowAudio == true) { + var senders = getSenders2(UUID); + if (session.mixMinus) { + log("mixMinus START .."); + var STRM = mixMinusAudio(UUID); + if (!STRM) { + continue; + } + STRM.getAudioTracks().forEach(trk => { + if (session.audioContentHint) { + trk.contentHint = session.audioContentHint; + } + var added = false; + senders.forEach(sender => { + if (added) { + if (sender.track && sender.track.kind == "audio") { + sender.track.enabled = false; + } + return; + } + if (sender.track && sender.track.kind == "audio") { + sender.replaceTrack(trk); + sender.track.enabled = true; + added = true; + warnlog("ADDED 5"); + } + }); + if (added) { + return; + } + session.pcs[UUID].addTrack(trk, STRM); + }); + continue; + } + senders.forEach(sender => { + var good = false; + if (sender.track && sender.track.id && sender.track.kind == "audio") { + tracks.forEach(function (track) { + // audio also + if (track.id == sender.track.id) { + good = true; + } + }); + } else { + // video or something else; ignore it. + return; + } + if (good) { + return; + } + sender.track.enabled = false; + //session.pcs[UUID].removeTrack(sender); // Apparently removeTrack causes renogiation; also kills send/recv. + }); + + if (tracks.length) { + tracks.forEach(function (track) { + var matched = false; + var senders = getSenders2(UUID); + senders.forEach(sender => { + if (sender.track && sender.track.id && sender.track.kind == "audio") { + warnlog(sender.track.id + " " + track.id); + if (sender.track.id == track.id) { + warnlog("MATCHED 1"); + matched = true; + } + } + }); + if (matched) { + return; + } + var added = false; + var senders = getSenders2(UUID); + senders.forEach(sender => { + if (added) { + return; + } + if (sender.track && sender.track.kind == "audio" && sender.track.enabled == false) { + sender.replaceTrack(track); + sender.track.enabled = true; + added = true; + warnlog("ADDED 2"); + } + }); + if (added) { + return; + } + var sender = session.pcs[UUID].addTrack(track, videoSource); + + }); + } else { + var senders = getSenders2(UUID); + senders.forEach(sender => { + if (sender.track && sender.track.kind == "audio") { + sender.track.enabled = false; // (trying this instead) + //session.pcs[UUID].removeTrack(sender); // Apparently removeTrack causes renogiation; also kills send/recv. + } + }); + } + } + } + if (session.director !== false) { + session.applySoloChat(); // mute streams that should be muted if a director + } + session.applyIsolatedChat(); + + try { + if (toggleSettingsState) { + updateConstraintSliders(); + } + } catch (e) { } + + if (callbackUUID) { + try { + var data = {}; + data.UUID = callbackUUID; + data.audioOptions = listAudioSettingsPrep(); + sendMediaDevices(data.UUID); + session.sendMessage(data, data.UUID); + } catch (e) { } + } + + if (session.twilio) { + session.twilio.updateMixer(); + } + } catch (e) { + errorlog(e); + } + + if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").dataset.audioready = true; + if (document.getElementById("gowebcam").dataset.ready && document.getElementById("gowebcam").dataset.ready == "true") { + document.getElementById("gowebcam").disabled = false; + miniTranslate(document.getElementById("gowebcam"), "start"); + document.getElementById("gowebcam").focus(); + } + } +} + +async function press2talk(clean = false) { + var ele = getById("press2talk"); + ele.style.minWidth = "127px"; + ele.style.padding = "7px"; + getById("settingsbutton").classList.remove("hidden"); + + if (!document.getElementById("controls_director") && session.showDirector) { + createDirectorOnlyBox(); + } + + if (session.taintedSession) { + var msg = {}; + msg.virtualHangup = false; + session.sendMessage(msg); + } + + log("DIRECTOR STREAM SETUP"); + + if (getById("press2talk").dataset.enabled == true) { + log("already enabled"); + return; + } + getById("press2talk").dataset.enabled = true; + + if (session.transcript) { + setTimeout(function () { + setupClosedCaptions(); + }, 1000); + } + getById("press2talk").outerHTML = ""; + getById("mutebutton").classList.remove("hidden"); + getById("hangupbutton2").classList.remove("hidden"); + + if (!session.showDirector && session.recordLocal !== false) { + getById("recordLocalbutton").classList.remove("hidden"); + } + + if (session.screenshareType === 3) { + getById("screenshare3button").className = "float"; + getById("screensharebutton").className = "float hidden"; + getById("screenshare2button").className = "float hidden"; + } else if (session.screenshareType === 1) { + getById("screensharebutton").className = "float"; + getById("screenshare3button").className = "float hidden"; + getById("screenshare2button").className = "float hidden"; + } else if (session.screenshareType === 2) { + getById("screenshare2button").className = "float"; + getById("screensharebutton").className = "float hidden"; + getById("screenshare3button").className = "float hidden"; + } else if (session.broadcast === null) { + // sstype=1, since in self-broadcast mode + getById("screensharebutton").className = "float"; + getById("screenshare2button").className = "float hidden"; + getById("screenshare3button").className = "float hidden"; + } else { + // sstype=3, since not in broadcast mode + getById("screensharebutton").className = "float hidden"; + getById("screenshare2button").className = "float hidden"; + getById("screenshare3button").className = "float"; + } + + checkBasicStreamsExist(); + session.videoElement.id = "videosource"; // could be set to UUID in the future + session.videoElement.dataset.menu = "context-menu-video"; + + if (session.streamID) { + session.videoElement.dataset.sid = session.streamID; + } + + // videosource + session.videoElement.muted = true; + session.videoElement.autoplay = true; + session.videoElement.controls = session.showControls || false; + session.videoElement.setAttribute("playsinline", ""); + + if (document.getElementById("videoContainer_director")) { + getById("videoContainer_director").appendChild(session.videoElement); + } else { + getById("miniPerformer").appendChild(session.videoElement); + } + + if (session.screenShareElement && document.getElementById("videoScreenContainer_director")) { + getById("videoScreenContainer_director").appendChild(session.screenShareElement); + } else if (session.screenShareElement) { + getById("miniPerformer").appendChild(session.screenShareElement); + } + + session.videoElement.title = "This is the preview of the Director's audio and video output."; + + session.videoElement.onpause = event => { + // prevent things from pausing; human or other + + if (!(event.ctrlKey || event.metaKey)) { + log("Video paused; auto playing"); + event.currentTarget + .play() + .then(_ => { + log("playing 9"); + }) + .catch(warnlog); + } + }; + + session.videoElement.addEventListener("click", function (e) { + // show stats of video if double clicked + log("click"); + try { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + + //////////////////////// + + var [menu, innerMenu] = statsMenuCreator(); + + ////////////////////////////////// + + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + printMyStats(innerMenu); + e.stopPropagation(); + + return false; + } + } catch (e) { + errorlog(e); + } + }); + + updatePushId(); + + /* if (session.directorEnabledPPT){ + enumerateDevices().then(gotDevices).then(async function() { + console.log("done"); + toggleSettings(); + }); + + return; + } */ + //await toggleSettings(); + + var constraint = { video: false, audio: true }; + + if (session.videoDevice) { + constraint.video = true; + } + if (session.audioDevice === 0) { + constraint.audio = false; + } + + requestBasicPermissions(constraint, function () { + log("requestBasicPermissions done"); + enumerateDevices() + .then(gotDevices) + .then(async function () { + log("enumerateDevices+gotDevices complete"); + + pokeIframeAPI("director-share", true, false, session.streamID); // director has started publishing; even if no audio/video. + + log("session.directorEnabledPPT: " + session.directorEnabledPPT); + + if (session.directorEnabledPPT) { + return; + } + + if (session.audioDevice !== 0) { + // change from Auto to Selected Audio Device + log("SETTING AUDIO DEVICE!!"); + activatedPreview = false; + await grabAudio("#audioSource3"); + } + + if (session.videoDevice !== 0) { + activatedPreview = false; + if (session.quality !== false) { + await grabVideo(session.quality, "videosource", "#videoSource3"); + } else { + //session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); + await grabVideo(session.roomid ? session.quality_room : session.quality_wb, "videosource", "#videoSource3"); + } + } + + if (session.videoMutedFlag) { + session.videoMuted = true; + toggleVideoMute(true); + } + + session.directorEnabledPPT = true; + toggleMute(true); + + //await toggleSettings(); + + if (session.autorecord || session.autorecordlocal) { + log("AUTO RECORD START"); + setTimeout( + function (v) { + var videoKbps = session.recordDefault; + if (session.recordLocal !== false) { + videoKbps = session.recordLocal; + } + if (document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']")) { + recordLocalVideoToggle(true); + } else if (v.stopWriter || v.recording) { + } else if (v.startWriter) { + v.startWriter(); + } else { + recordLocalVideo(null, videoKbps, v); + } + }, + 2000, + session.videoElement + ); + } + + log("session.seeding: " + session.seeding); + + if (session.seeding) { + setTimeout(function () { if (session.meshcast2) { meshcast2(); } else if (session.meshcast) { @@ -36454,10 +36911,10 @@ async function press2talk(clean = false) { whepOut(); } - }, 1000); - return; - } - + }, 1000); + return; + } + if (session.meshcast2) { meshcast2(); } else if (session.meshcast) { @@ -36468,435 +36925,435 @@ async function press2talk(clean = false) { whepOut(); } - - session.seeding = true; - await session.seedStream(); - - }); - }); -} // publishdirector - -function statsMenuCreator() { - if (getById("menuStatsBox")) { - clearInterval(getById("menuStatsBox").interval); - getById("menuStatsBox").remove(); - } - - var menu = document.createElement("div"); - menu.id = "menuStatsBox"; - menu.className = "debugStats remotestats"; - getById("main").appendChild(menu); - - menu.style.left = parseInt(Math.random() * 10) + 15 + "px"; - menu.style.top = parseInt(Math.random() * 10) + "px"; - - menu.innerHTML = "

    Statistics

    "; - var menuCloseBtn = document.createElement("button"); - menuCloseBtn.className = "close"; - menuCloseBtn.innerHTML = "×"; - menu.appendChild(menuCloseBtn); - - var innerMenu = document.createElement("div"); - menu.appendChild(innerMenu); - - menuCloseBtn.addEventListener("click", function (eve) { - clearInterval(menu.interval); - eve.currentTarget.parentNode.remove(); - eve.preventDefault(); - eve.stopPropagation(); - }); - return [menu, innerMenu]; -} - -// WEBCAM -session.publishStream = function (v) { - // stream is used to generated an SDP - log("STREAM SETUP"); - - if (session.transcript) { - setTimeout(function () { - setupClosedCaptions(); - }, 1000); - } - - if (!session.streamSrc) { - checkBasicStreamsExist(); - } - - session.streamSrc.oninactive = function streamoninactive() { - warnlog("Stream inactive"); - if (session.videoElement.recording) { - session.videoElement.recorder.stop(); - } - }; - - if (session.streamSrc.getVideoTracks().length == 0) { - warnlog("NO VIDEO TRACK INCLUDED"); - } - - if (session.streamSrc.getAudioTracks().length == 0) { - warnlog("NO AUDIO TRACK INCLUDED"); - } - - var container = document.createElement("div"); - v.container = container; - container.id = "container"; - - if (session.cleanOutput) { - container.style.height = "100%"; - v.style.maxWidth = "100%"; - v.style.boxShadow = "none"; - } - - if (session.cover) { - container.style.setProperty("height", "100%", "important"); - } - - //container.className = "vidcon"; - getById("gridlayout").appendChild(container); - - v.className = "tile"; //"tile task"; TODO: get working (will add task later on instead) - - v.muted = true; - v.autoplay = true; - if (session.showControls !== null) { - v.controls = session.showControls; - } else if (session.mobile) { - v.controls = true; - } else { - v.controls = session.showControls || false; - } - v.setAttribute("playsinline", ""); - v.id = "videosource"; // could be set to UUID in the future - v.oncanplay = null; - - session.videoElement = v; - - container.appendChild(v); - - if (session.audioGain !== false) { - changeMainGain(session.audioGain); // just in case we don't mute things in time via the draw / audioMeter interval - } - - toggleMute(true); - - if (session.nopreview) { - v.style.display = "none"; - container.style.display = "none"; - } - - if (((session.roomid === false || session.roomid === "") && session.quality === false) || session.forceMediaSettings) { - try { - if (session.quality_wb !== false && session.quality === false) { - getById("webcamquality3").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : session.quality_wb); - } else if (session.quality !== false) { - getById("webcamquality3").elements.namedItem("resolution").value = session.quality; - } - getById("gear_webcam3").style.display = "inline-block"; - getById("webcamquality3").onchange = function (event) { - if (parseInt(getById("webcamquality3").elements.namedItem("resolution").value) == 2) { - if (session.maxframeRate === false) { - session.maxframeRate = 30; - session.maxframeRate_q2 = true; - } - } else if (session.maxframeRate_q2) { - session.maxframeRate = false; - session.maxframeRate_q2 = false; - } - activatedPreview = false; - session.quality_wb = parseInt(getById("webcamquality3").elements.namedItem("resolution").value); - session.quality_room = session.quality_wb; - grabVideo(session.quality_wb, "videosource", "select#videoSource3"); - }; - } catch (e) { - errorlog(e); - } - } - - var bigPlayButton = document.getElementById("bigPlayButton"); - if (bigPlayButton) { - bigPlayButton.parentNode.removeChild(bigPlayButton); - } - - if (session.streamID) { - session.videoElement.dataset.sid = session.streamID; - } - - if (session.statsMenu) { - var [menu, innerMenu] = statsMenuCreator(); - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - printMyStats(innerMenu); - } - - if (session.director) { - // the director doesn't load a webcam by default anyways. - // audio is not mucked with - } else if (session.scene !== false) { - // it's a scene, and there are no previews in a scene. - //setTimeout(function(){updateMixer();},10); - } else if (session.roomid !== false) { - if (session.roomid === "") { - if (!session.view || session.view === "") { - if (session.fullscreen) { - session.windowed = session.windowed === null ? false : session.windowed; - } else if (session.minipreview) { - session.windowed = session.windowed === null ? false : session.windowed; - } else { - session.windowed = session.windowed === null ? true : session.windowed; - } - - if (session.windowed) { - v.className = "myVideo"; //"myVideo task"; TODO: get working - container.classList.add("vidcon"); - } - - getById("mutespeakerbutton").classList.add("hidden"); - - applyMirror(session.mirrorExclude); - - container.style.width = "100%"; - //container.style.height="100%"; - - container.style.alignItems = "center"; - container.backgroundColor = "#666"; - - setTimeout(function () { - dragElement(v); - }, 1000); - play(); - } else { - session.windowed = session.windowed === null ? false : session.windowed; - applyMirror(session.mirrorExclude); - play(); - //setTimeout(function(){updateMixer();},10); - } - } else { - //session.cbr=0; // we're just going to override it - if (session.stereo == 5) { - // not a scene or director, so we will assume its a guest. changing to stereo=3 - session.stereo = 3; - } - session.windowed = session.windowed === null ? false : session.windowed; - applyMirror(session.mirrorExclude); - - if (session.include.length) { - play(); - } - - //setTimeout(function(){updateMixer();},10); - } - } else { - if (session.fullscreen) { - session.windowed = session.windowed === null ? false : session.windowed; - } else if (session.minipreview) { - session.windowed = session.windowed === null ? false : session.windowed; - } else { - session.windowed = session.windowed === null ? true : session.windowed; - } - if (session.windowed) { - v.className = "myVideo"; //"myVideo task"; TODO: get working - container.classList.add("vidcon"); - } - getById("mutespeakerbutton").classList.add("hidden"); - - applyMirror(session.mirrorExclude); - - container.style.width = "100%"; - //container.style.height="100%"; - //container.style.display = "flex"; - - container.style.alignItems = "center"; - container.backgroundColor = "#666"; - - setTimeout(function () { - dragElement(v); - }, 1000); - } - - v.addEventListener("click", function (e) { - log("click"); - try { - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - - var [menu, innerMenu] = statsMenuCreator(); - - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - - printMyStats(innerMenu); - e.stopPropagation(); - return false; - } - } catch (e) { - errorlog(e); - } - }); - - v.touchTimeOut = null; - v.touchLastTap = 0; - v.touchCount = 0; - - v.addEventListener("touchend", function (event) { - if (session.disableMouseEvents) { - return; - } - - }); - - updateReshareLink(); - pokeIframeAPI("started-camera"); // depreciated - pokeIframeAPI("camera-share", true); - - if (session.videoMutedFlag) { - session.videoMuted = true; - toggleVideoMute(true); - } - - if (!gotDevices2AlreadyRan) { - enumerateDevices().then(gotDevices2); // this is needed for iOS; was previous set to timeout at 100ms, but would be useful everywhere I think - } - - v.dataset.menu = "context-menu-video"; - if (!session.cleanOutput) { - v.classList.add("task"); // this adds the right-click menu - } - - session.postPublish(); - - if (session.autorecord || session.autorecordlocal) { - log("AUTO RECORD START"); - setTimeout( - function (v) { - var videoKbps = session.recordDefault; - if (session.recordLocal !== false) { - videoKbps = session.recordLocal; - } - - if (session.director) { - recordVideo(document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']"), null, videoKbps); - } else if (v.stopWriter || v.recording) { - } else if (v.startWriter) { - v.startWriter(); - } else { - recordLocalVideo(null, videoKbps, v); - } - }, - 2000, - v - ); - } - - setTimeout(function () { - updateMixer(); - }, 10); -}; // publishStream - -function stickyMessage(message) { - var textOverlay = getById("stickyMsgs"); - if (textOverlay) { - var spanOverlay = document.createElement("span"); - spanOverlay.innerHTML = message; - var closeBtn = document.createElement("button"); - closeBtn.className = "overlayCloseBtn red"; - closeBtn.innerHTML = "❌"; - closeBtn.title = "Close this message"; - closeBtn.onclick = function () { - this.parentNode.remove(); - }; - textOverlay.appendChild(spanOverlay); - spanOverlay.appendChild(closeBtn); - textOverlay.classList.remove("hidden"); - setTimeout( - function (spanOverlay) { - if (spanOverlay) { - spanOverlay.style.animation = "fadeout 1s"; - spanOverlay.style.opacity = "0"; - setTimeout( - function (spanOverlay) { - spanOverlay.remove(); - }, - 900, - spanOverlay - ); - } - }, - 30000, - spanOverlay - ); - } -} - -session.postPublish = async function () { - log("Post publish"); - if (session.welcomeMessage) { - stickyMessage(session.welcomeMessage); - // getChatMessage(session.welcomeMessage, false, true, true); - } - if (session.queue && (session.queueType == 3 || session.queueType == 4) && !session.director) { - youreWaitingToBeActivated(); - } - - if (session.welcomeImage) { - let welcomeoverlay = document.createElement("img"); - welcomeoverlay.src = session.welcomeImage; - welcomeoverlay.className = "welcomeOverlay"; - document.body.appendChild(welcomeoverlay); - await sleep(2000); - setTimeout( - function (welcomeoverlay) { - welcomeoverlay.style = "animation: fadeout 1s;"; - setTimeout( - function (welcomeoverlay) { - welcomeoverlay.remove(); - }, - 990, - welcomeoverlay - ); - }, - 1000, - welcomeoverlay - ); - } - - if (session.welcomeHTML) { - let welcomeHTML = document.createElement("div"); - welcomeHTML.innerHTML = session.welcomeHTML; - welcomeHTML.className = "welcomeOverlay"; - document.body.appendChild(welcomeHTML); - setTimeout( - function (welcomeHTML) { - welcomeHTML.style = "animation: fadeout 1s;"; - setTimeout( - function (welcomeHTML) { - welcomeHTML.remove(); - }, - 990, - welcomeHTML - ); - }, - 3000, - welcomeHTML - ); - } - - if (session.waitPage && session.iFramesAllowed) { - let waitPageIFrame = parseURL4Iframe(session.waitPage); - if (waitPageIFrame) { - session.layout = combinedLayout([{ "w": 100, "h": 100, "x": 0, "y": 0, "z": 1, "slot": 1, "cover": false, "borderThickness": 20, "animated": 1000, "borderColor": "#0000", "backgroundMedia": "", "foregroundMedia": "", "iframeSrc": waitPageIFrame, "defaultStreamID": "", "margin": 50, "rounded": 30, "muted": false }]); - updateMixer(); - } - } - - clearInterval(session.updateLocalStatsInterval); - session.updateLocalStatsInterval = setInterval(function () { - updateLocalStats(); - }, session.statsInterval); - - pokeIframeAPI("screen-share-state", false); - - session.seeding = true; - session.seedStream(); - + + session.seeding = true; + await session.seedStream(); + + }); + }); +} // publishdirector + +function statsMenuCreator() { + if (getById("menuStatsBox")) { + clearInterval(getById("menuStatsBox").interval); + getById("menuStatsBox").remove(); + } + + var menu = document.createElement("div"); + menu.id = "menuStatsBox"; + menu.className = "debugStats remotestats"; + getById("main").appendChild(menu); + + menu.style.left = parseInt(Math.random() * 10) + 15 + "px"; + menu.style.top = parseInt(Math.random() * 10) + "px"; + + menu.innerHTML = "

    Statistics

    "; + var menuCloseBtn = document.createElement("button"); + menuCloseBtn.className = "close"; + menuCloseBtn.innerHTML = "×"; + menu.appendChild(menuCloseBtn); + + var innerMenu = document.createElement("div"); + menu.appendChild(innerMenu); + + menuCloseBtn.addEventListener("click", function (eve) { + clearInterval(menu.interval); + eve.currentTarget.parentNode.remove(); + eve.preventDefault(); + eve.stopPropagation(); + }); + return [menu, innerMenu]; +} + +// WEBCAM +session.publishStream = function (v) { + // stream is used to generated an SDP + log("STREAM SETUP"); + + if (session.transcript) { + setTimeout(function () { + setupClosedCaptions(); + }, 1000); + } + + if (!session.streamSrc) { + checkBasicStreamsExist(); + } + + session.streamSrc.oninactive = function streamoninactive() { + warnlog("Stream inactive"); + if (session.videoElement.recording) { + session.videoElement.recorder.stop(); + } + }; + + if (session.streamSrc.getVideoTracks().length == 0) { + warnlog("NO VIDEO TRACK INCLUDED"); + } + + if (session.streamSrc.getAudioTracks().length == 0) { + warnlog("NO AUDIO TRACK INCLUDED"); + } + + var container = document.createElement("div"); + v.container = container; + container.id = "container"; + + if (session.cleanOutput) { + container.style.height = "100%"; + v.style.maxWidth = "100%"; + v.style.boxShadow = "none"; + } + + if (session.cover) { + container.style.setProperty("height", "100%", "important"); + } + + //container.className = "vidcon"; + getById("gridlayout").appendChild(container); + + v.className = "tile"; //"tile task"; TODO: get working (will add task later on instead) + + v.muted = true; + v.autoplay = true; + if (session.showControls !== null) { + v.controls = session.showControls; + } else if (session.mobile) { + v.controls = true; + } else { + v.controls = session.showControls || false; + } + v.setAttribute("playsinline", ""); + v.id = "videosource"; // could be set to UUID in the future + v.oncanplay = null; + + session.videoElement = v; + + container.appendChild(v); + + if (session.audioGain !== false) { + changeMainGain(session.audioGain); // just in case we don't mute things in time via the draw / audioMeter interval + } + + toggleMute(true); + + if (session.nopreview) { + v.style.display = "none"; + container.style.display = "none"; + } + + if (((session.roomid === false || session.roomid === "") && session.quality === false) || session.forceMediaSettings) { + try { + if (session.quality_wb !== false && session.quality === false) { + getById("webcamquality3").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : session.quality_wb); + } else if (session.quality !== false) { + getById("webcamquality3").elements.namedItem("resolution").value = session.quality; + } + getById("gear_webcam3").style.display = "inline-block"; + getById("webcamquality3").onchange = function (event) { + if (parseInt(getById("webcamquality3").elements.namedItem("resolution").value) == 2) { + if (session.maxframeRate === false) { + session.maxframeRate = 30; + session.maxframeRate_q2 = true; + } + } else if (session.maxframeRate_q2) { + session.maxframeRate = false; + session.maxframeRate_q2 = false; + } + activatedPreview = false; + session.quality_wb = parseInt(getById("webcamquality3").elements.namedItem("resolution").value); + session.quality_room = session.quality_wb; + grabVideo(session.quality_wb, "videosource", "select#videoSource3"); + }; + } catch (e) { + errorlog(e); + } + } + + var bigPlayButton = document.getElementById("bigPlayButton"); + if (bigPlayButton) { + bigPlayButton.parentNode.removeChild(bigPlayButton); + } + + if (session.streamID) { + session.videoElement.dataset.sid = session.streamID; + } + + if (session.statsMenu) { + var [menu, innerMenu] = statsMenuCreator(); + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + printMyStats(innerMenu); + } + + if (session.director) { + // the director doesn't load a webcam by default anyways. + // audio is not mucked with + } else if (session.scene !== false) { + // it's a scene, and there are no previews in a scene. + //setTimeout(function(){updateMixer();},10); + } else if (session.roomid !== false) { + if (session.roomid === "") { + if (!session.view || session.view === "") { + if (session.fullscreen) { + session.windowed = session.windowed === null ? false : session.windowed; + } else if (session.minipreview) { + session.windowed = session.windowed === null ? false : session.windowed; + } else { + session.windowed = session.windowed === null ? true : session.windowed; + } + + if (session.windowed) { + v.className = "myVideo"; //"myVideo task"; TODO: get working + container.classList.add("vidcon"); + } + + getById("mutespeakerbutton").classList.add("hidden"); + + applyMirror(session.mirrorExclude); + + container.style.width = "100%"; + //container.style.height="100%"; + + container.style.alignItems = "center"; + container.backgroundColor = "#666"; + + setTimeout(function () { + dragElement(v); + }, 1000); + play(); + } else { + session.windowed = session.windowed === null ? false : session.windowed; + applyMirror(session.mirrorExclude); + play(); + //setTimeout(function(){updateMixer();},10); + } + } else { + //session.cbr=0; // we're just going to override it + if (session.stereo == 5) { + // not a scene or director, so we will assume its a guest. changing to stereo=3 + session.stereo = 3; + } + session.windowed = session.windowed === null ? false : session.windowed; + applyMirror(session.mirrorExclude); + + if (session.include.length) { + play(); + } + + //setTimeout(function(){updateMixer();},10); + } + } else { + if (session.fullscreen) { + session.windowed = session.windowed === null ? false : session.windowed; + } else if (session.minipreview) { + session.windowed = session.windowed === null ? false : session.windowed; + } else { + session.windowed = session.windowed === null ? true : session.windowed; + } + if (session.windowed) { + v.className = "myVideo"; //"myVideo task"; TODO: get working + container.classList.add("vidcon"); + } + getById("mutespeakerbutton").classList.add("hidden"); + + applyMirror(session.mirrorExclude); + + container.style.width = "100%"; + //container.style.height="100%"; + //container.style.display = "flex"; + + container.style.alignItems = "center"; + container.backgroundColor = "#666"; + + setTimeout(function () { + dragElement(v); + }, 1000); + } + + v.addEventListener("click", function (e) { + log("click"); + try { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + + var [menu, innerMenu] = statsMenuCreator(); + + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + + printMyStats(innerMenu); + e.stopPropagation(); + return false; + } + } catch (e) { + errorlog(e); + } + }); + + v.touchTimeOut = null; + v.touchLastTap = 0; + v.touchCount = 0; + + v.addEventListener("touchend", function (event) { + if (session.disableMouseEvents) { + return; + } + + }); + + updateReshareLink(); + pokeIframeAPI("started-camera"); // depreciated + pokeIframeAPI("camera-share", true); + + if (session.videoMutedFlag) { + session.videoMuted = true; + toggleVideoMute(true); + } + + if (!gotDevices2AlreadyRan) { + enumerateDevices().then(gotDevices2); // this is needed for iOS; was previous set to timeout at 100ms, but would be useful everywhere I think + } + + v.dataset.menu = "context-menu-video"; + if (!session.cleanOutput) { + v.classList.add("task"); // this adds the right-click menu + } + + session.postPublish(); + + if (session.autorecord || session.autorecordlocal) { + log("AUTO RECORD START"); + setTimeout( + function (v) { + var videoKbps = session.recordDefault; + if (session.recordLocal !== false) { + videoKbps = session.recordLocal; + } + + if (session.director) { + recordVideo(document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']"), null, videoKbps); + } else if (v.stopWriter || v.recording) { + } else if (v.startWriter) { + v.startWriter(); + } else { + recordLocalVideo(null, videoKbps, v); + } + }, + 2000, + v + ); + } + + setTimeout(function () { + updateMixer(); + }, 10); +}; // publishStream + +function stickyMessage(message) { + var textOverlay = getById("stickyMsgs"); + if (textOverlay) { + var spanOverlay = document.createElement("span"); + spanOverlay.innerHTML = message; + var closeBtn = document.createElement("button"); + closeBtn.className = "overlayCloseBtn red"; + closeBtn.innerHTML = "❌"; + closeBtn.title = "Close this message"; + closeBtn.onclick = function () { + this.parentNode.remove(); + }; + textOverlay.appendChild(spanOverlay); + spanOverlay.appendChild(closeBtn); + textOverlay.classList.remove("hidden"); + setTimeout( + function (spanOverlay) { + if (spanOverlay) { + spanOverlay.style.animation = "fadeout 1s"; + spanOverlay.style.opacity = "0"; + setTimeout( + function (spanOverlay) { + spanOverlay.remove(); + }, + 900, + spanOverlay + ); + } + }, + 30000, + spanOverlay + ); + } +} + +session.postPublish = async function () { + log("Post publish"); + if (session.welcomeMessage) { + stickyMessage(session.welcomeMessage); + // getChatMessage(session.welcomeMessage, false, true, true); + } + if (session.queue && (session.queueType == 3 || session.queueType == 4) && !session.director) { + youreWaitingToBeActivated(); + } + + if (session.welcomeImage) { + let welcomeoverlay = document.createElement("img"); + welcomeoverlay.src = session.welcomeImage; + welcomeoverlay.className = "welcomeOverlay"; + document.body.appendChild(welcomeoverlay); + await sleep(2000); + setTimeout( + function (welcomeoverlay) { + welcomeoverlay.style = "animation: fadeout 1s;"; + setTimeout( + function (welcomeoverlay) { + welcomeoverlay.remove(); + }, + 990, + welcomeoverlay + ); + }, + 1000, + welcomeoverlay + ); + } + + if (session.welcomeHTML) { + let welcomeHTML = document.createElement("div"); + welcomeHTML.innerHTML = session.welcomeHTML; + welcomeHTML.className = "welcomeOverlay"; + document.body.appendChild(welcomeHTML); + setTimeout( + function (welcomeHTML) { + welcomeHTML.style = "animation: fadeout 1s;"; + setTimeout( + function (welcomeHTML) { + welcomeHTML.remove(); + }, + 990, + welcomeHTML + ); + }, + 3000, + welcomeHTML + ); + } + + if (session.waitPage && session.iFramesAllowed) { + let waitPageIFrame = parseURL4Iframe(session.waitPage); + if (waitPageIFrame) { + session.layout = combinedLayout([{ "w": 100, "h": 100, "x": 0, "y": 0, "z": 1, "slot": 1, "cover": false, "borderThickness": 20, "animated": 1000, "borderColor": "#0000", "backgroundMedia": "", "foregroundMedia": "", "iframeSrc": waitPageIFrame, "defaultStreamID": "", "margin": 50, "rounded": 30, "muted": false }]); + updateMixer(); + } + } + + clearInterval(session.updateLocalStatsInterval); + session.updateLocalStatsInterval = setInterval(function () { + updateLocalStats(); + }, session.statsInterval); + + pokeIframeAPI("screen-share-state", false); + + session.seeding = true; + session.seedStream(); + if (session.meshcast2) { await meshcast2(); } else if (session.whipOutput) { // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024 @@ -36907,632 +37364,632 @@ session.postPublish = async function () { whepOut(); } - if (session.chunkcast) { - session.chunkedStream(null); - } - - if (session.motionRecord && session.videoElement && !session.motionDetectionInterval) { - session.motionDetectionInterval = setTimeout(function () { - setInterval(function () { - motionDetection(session.videoElement, session.motionRecord); - }, 400); - }, 2000); - } - - if (session.poke) { - if (session.poke === true) { - let topic = await generateTopic(session.roomid, session.streamID, false, false, session.hash, window.location.hostname); - await triggerNotification(topic) - } else { - await triggerNotification(session.poke); - } - } - - if (session.autoEnd) { - log("Auto-end timer started: " + session.autoEnd + "ms"); - - // Create countdown display - const countdownDiv = document.createElement("div"); - countdownDiv.id = "autoEndCountdown"; - countdownDiv.style.cssText = "position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 10px 15px; border-radius: 5px; font-size: 16px; z-index: 9999; display: flex; align-items: center; gap: 8px;"; - countdownDiv.innerHTML = '⏱️--:--'; - document.body.appendChild(countdownDiv); - - // Update countdown every second - let remainingTime = session.autoEnd; - const updateCountdown = () => { - const minutes = Math.floor(remainingTime / 60000); - const seconds = Math.floor((remainingTime % 60000) / 1000); - document.getElementById("autoEndTime").textContent = - String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0'); - remainingTime -= 1000; - - if (remainingTime < 0) { - clearInterval(session.autoEndInterval); - } - }; - - updateCountdown(); // Initial update - session.autoEndInterval = setInterval(updateCountdown, 1000); - - // Set timer to end stream - session.autoEndTimer = setTimeout(() => { - log("Auto-end timer expired, ending stream"); - clearInterval(session.autoEndInterval); - session.hangup(); - }, session.autoEnd); - } - -}; -function triggerNotification(topic, customMessage = null) { - if (!topic) return false; - - const message = customMessage || ((session.label ? session.label : 'Someone') + - (session.roomid ? ' joined your room' : ' joined your stream')); - - const notifyUrl = `https://notify.vdo.ninja/?notify=${topic}&message=${encodeURIComponent(message)}`; - - console.log('Sending notification to:', notifyUrl); - - return fetch(notifyUrl) - .then(response => { - console.log('Notification response status:', response.status); - if (!response.ok) { - return response.text().then(text => { - try { - const errorData = JSON.parse(text); - console.error('Notification server error:', errorData); - return false; - } catch (e) { - console.error('Notification error response:', text); - return false; - } - }); - } - - return response.json(); - }) - .then(data => { - if (data === false) return false; - - console.log('Notification result:', data); - - // Check push results to diagnose issues - if (data.pushResults && Array.isArray(data.pushResults)) { - data.pushResults.forEach(result => { - if (!result.success) { - console.warn('Push notification failed:', result); - } - }); - } - - return data.success === true; - }) - .catch(error => { - console.error('Error sending notification:', error); - return false; - }); -} -function hashTopic(text) { - const salt1 = "abc12345ASB234ASD1116"; - const salt2 = "xyzJKL789MNO567PQR890"; - const salt3 = "9843kasdjfh234jhk234j"; - let saltedText = salt1 + text + salt2 + text.split('').reverse().join('') + salt3; - let hash = 0; - if (saltedText.length === 0) return "0"; - for (let i = 0; i < saltedText.length; i++) { - const char = saltedText.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - let hash2 = 0; - for (let i = 0; i < saltedText.length; i++) { - hash2 = ((hash2 << 7) + hash2) + saltedText.charCodeAt(i); - hash2 = hash2 & hash2; - } - const combinedHash = Math.abs(hash).toString(36) + Math.abs(hash2).toString(36); - if (combinedHash.length < 10) { - return combinedHash + Math.random().toString(36).substring(2, 12); - } - return combinedHash; -} - -async function generateTopic(roomId, pushId, viewId, password, hash, domain) { - domain = domain || 'vdo.ninja'; - if (!roomId && !viewId && !pushId) { - console.error('At least one of roomId, viewId or pushId is required'); - return null; - } - const components = { - room: roomId || viewId || pushId, - domain: domain.replace(/\./g, '_') - }; - let sensitiveData = Object.values(components).filter(Boolean).join('_'); - if (hash) { - sensitiveData += `_${hash}`; - } else if (password) { - const passwordHash = await generateHash(password); - sensitiveData += `_${passwordHash}`; - } - const secureTopicHash = hashTopic(sensitiveData); - const finalPrefix = components.domain; - const finalTopic = `${finalPrefix}_${secureTopicHash}`; - return finalTopic; -} - -async function publishScreen2(constraints, audioList = [], audio = true, overrideFramerate = false) { - // webcam stream is used to generated an SDP - log("SCREEN SHARE SETUP - publishScreen2"); - - if (!navigator.mediaDevices.getDisplayMedia) { - setTimeout(function () { - if (iOS || iPad) { - warnUser("Sorry, but your iOS browser does not support screen-sharing.\n\nPlease see this guide for an alternative method to do so.", false, false); - } else if (session.mobile) { - warnUser("Sorry, your browser does not support screen-sharing.\n\nThe Android native app should support it though.", false, false); - } else { - warnUser("Sorry, your browser does not support screen-sharing.\n\nPlease use the desktop versions of Firefox or Chrome instead."); - } - }, 1); - return false; - } - if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - if (!ElectronDesktopCapture) { - if (!(session.cleanOutput && session.cleanish == false)) { - warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)"); - } - return false; - } - } - - var streams = []; - log(audioList); - for (var i = 0; i < audioList.length; i++) { - // mic sources; not screen . - let constraintAudio = { video: false, audio: { deviceId: { exact: audioList[i] } } }; - - if (session.echoCancellation === false) { - // default should be ON. we won't even add it since deviceId is specified and Browser defaults to on already - constraintAudio.audio.echoCancellation = false; - } else { - constraintAudio.audio.echoCancellation = true; - } - if (session.autoGainControl === false) { - constraintAudio.audio.autoGainControl = false; - } else { - constraintAudio.audio.autoGainControl = true; - } - if (session.noiseSuppression === false) { - constraintAudio.audio.noiseSuppression = false; - } else { - constraintAudio.audio.noiseSuppression = true; - } - - if (session.voiceIsolation === true) { - constraintAudio.audio.voiceIsolation = true; - } - - if (session.audioInputChannels) { - if (constraintAudio.audio === true) { - constraintAudio.audio = {}; - constraintAudio.audio.channelCount = session.audioInputChannels; - } else if (constraintAudio.audio) { - constraintAudio.audio.channelCount = session.audioInputChannels; - } - } - - if (session.micSampleRate) { - if (constraintAudio.audio === true) { - constraintAudio.audio = {}; - constraintAudio.audio.sampleRate = parseInt(session.micSampleRate); - } else if (constraintAudio.audio) { - constraintAudio.audio.sampleRate = parseInt(session.micSampleRate); - } - } - if (session.micSampleSize) { - if (constraintAudio.audio === true) { - constraintAudio.audio = {}; - constraintAudio.audio.sampleSize = parseInt(session.micSampleSize); - } else if (constraintAudio.audio) { - constraintAudio.audio.sampleSize = parseInt(session.micSampleSize); - } - } - getUserMediaRequestID += 1; - var gumID = getUserMediaRequestID; - - if (Firefox) { - constraintAudio = toFirefoxConstraint(constraintAudio); - } - - log(constraintAudio); - - warnlog("navigator.mediaDevices.getUserMedia starting..."); - await navigator.mediaDevices - .getUserMedia(constraintAudio) - .then(stream => { - if (getUserMediaRequestID !== gumID) { - warnlog("GET USER MEDIA CALL HAS EXPIRED 3"); - stream.getTracks().forEach(function (track) { - stream.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - return; - } - streams.push(stream); - }) - .catch(errorlog); - } - - if (session.audioDevice === 0) { - constraints.audio = false; - } - - if (session.screenshareVideoOnly) { - constraints.audio = false; - } - - if (constraints.video !== false && Object.keys(constraints.video).length == 0) { - constraints.video = true; - } - - log(constraints); - getUserMediaRequestID += 1; - var gumID = getUserMediaRequestID; - return navigator.mediaDevices - .getDisplayMedia(constraints) - .then(async function (stream) { - if (getUserMediaRequestID !== gumID) { - warnlog("GET USER MEDIA CALL HAS EXPIRED 3"); - stream.getTracks().forEach(function (track) { - stream.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - return; - } - - try { - var constraint = {}; - - if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) { - constraint.aspectRatio = parseFloat(session.forceAspectRatio); - } else if (session.forceScreenShareAspectRatio) { - constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio); - } - if (overrideFramerate) { - constraint.frameRate = overrideFramerate; - } - if (Object.keys(constraint).length) { - await stream.getVideoTracks()[0].applyConstraints({ - advanced: [constraint] - }); - log({ - advanced: [constraint] - }); - } - } catch (e) { - errorlog(e); - } - - /// RETURN stream for preview? rather than jumping right in. - session.screenShareState = true; - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - notifyOfScreenShare(); - - try { - stream.getVideoTracks()[0].onended = function () { - toggleScreenShare(); - }; - } catch (e) { - log("No Video selected; screensharing?"); - } - - // OR, jump right in, and let user change from there - if (session.roomid !== false) { - if (session.roomid === "" && (!session.view || session.view === "")) { - if (session.manual === null) { - session.manual = session.manual === null ? true : session.manual; - } - if (!session.cleanOutput) { - var showReshare = getStorage("showReshare"); - if (showReshare) { - generateHash(session.streamID + session.salt + "bca321", 4) - .then(function (hash) { - // million to one error. - if (showReshare === hash) { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - } else if (session.permaid === null) { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - } - }) - .catch(errorlog); - } - } - } else { - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - log("ROOMID EANBLED"); - log("Update Mixer Event on REsize SET"); - window.onresize = updateMixer; - window.onorientationchange = function () { - if (Firefox) { - updateForceRotate(true); - } - setTimeout(async function () { - if (session.forceAspectRatio) { - await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - } - if (session.effect && session.effect === "7") { - digitalZoom(); - } - updateForceRotate(); - updateMixer(); - }, 200); - }; - joinRoom(session.roomid); - } - } else { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - getById("logoname").style.display = "none"; - } - - updatePushId(); - - if (stream.getAudioTracks().length) { - screenShareAudioTrack = stream.getAudioTracks()[0]; - } - - log("adding tracks"); - for (var i = 0; i < streams.length; i++) { - streams[i].getAudioTracks().forEach(track => { - stream.addTrack(track); - }); - } - streams = null; - if (!session.screenshareVideoOnly && session.audioDevice !== 0) { - if (stream.getAudioTracks().length == 0) { - if (!session.cleanOutput) { - if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - // Electron has no audio. - } else if (!getStorage("leaveInPeaceSSWarning")) { - setStorage("leaveInPeaceSSWarning", "true", 720); - setTimeout(function () { - warnUser(getTranslation("no-audio-source-detected"), 10000, false); - }, 300); - } - } - } - } - - try { - session.streamSrc = stream; - } catch (e) { - errorlog(e); - } - toggleMute(true); - - var v = createVideoElement(); - session.videoElement = v; - - if (session.streamID) { - session.videoElement.dataset.sid = session.streamID; - } - - var container = document.createElement("div"); - v.container = container; - container.id = "container_screen"; - container.style.height = "100%"; - - if (session.cleanOutput) { - v.style.maxWidth = "100%"; - v.style.boxShadow = "none"; - } - - //container.className = "vidcon"; - getById("gridlayout").appendChild(container); - - if (session.nopreview) { - v.style.display = "none"; - container.style.display = "none"; - } - - //if (session.cover){ - // container.style.setProperty('height', '100%', 'important'); - //} - - container.appendChild(v); - - v.className = "tile"; - - if (session.director) { - } else if (session.scene !== false) { - setTimeout(function () { - updateMixer(); - }, 1); - } else if (session.roomid !== false) { - if (session.roomid === "") { - if (!session.view || session.view === "") { - getById("mutespeakerbutton").classList.add("hidden"); - if (session.fullscreen) { - session.windowed = session.windowed === null ? false : session.windowed; - } else { - session.windowed = session.windowed === null ? true : session.windowed; - } - if (!session.windowed) { - if (session.mirrored && session.flipped) { - v.style.transform = " scaleX(-1) scaleY(-1)"; - v.classList.add("mirrorControl"); - } else if (session.mirrored) { - v.style.transform = "scaleX(-1)"; - v.classList.add("mirrorControl"); - } else if (session.flipped) { - v.style.transform = "scaleY(-1)"; - v.classList.remove("mirrorControl"); - } else { - v.style.transform = ""; - v.classList.remove("mirrorControl"); - } - } else { - v.className = "myVideo"; - if (session.mirrored && session.flipped) { - v.style.transform = " scaleX(-1) scaleY(-1) translate(0, 50%)"; - v.classList.add("mirrorControl"); - } else if (session.mirrored) { - v.style.transform = "scaleX(-1) translate(0, -50%)"; - v.classList.add("mirrorControl"); - } else if (session.flipped) { - v.style.transform = "scaleY(-1) translate(0, 50%)"; - v.classList.remove("mirrorControl"); - } else { - v.style.transform = " translate(0, -50%)"; - v.classList.remove("mirrorControl"); - } - } - container.style.width = "100%"; - //container.style.height="100%"; - container.style.alignItems = "center"; - container.backgroundColor = "#666"; - setTimeout(function () { - dragElement(v); - }, 1000); - play(); - } else { - play(); - setTimeout(function () { - updateMixer(); - }, 1); - } - } else { - setTimeout(function () { - updateMixer(); - }, 1); - } - } else { - getById("mutespeakerbutton").classList.add("hidden"); - if (session.fullscreen) { - session.windowed = session.windowed === null ? false : session.windowed; - } else { - session.windowed = session.windowed === null ? true : session.windowed; - } - if (!session.windowed) { - if (session.mirrored && session.flipped) { - v.style.transform = " scaleX(-1) scaleY(-1)"; - v.classList.add("mirrorControl"); - } else if (session.mirrored) { - v.style.transform = "scaleX(-1)"; - v.classList.add("mirrorControl"); - } else if (session.flipped) { - v.style.transform = "scaleY(-1)"; - v.classList.remove("mirrorControl"); - } else { - v.style.transform = ""; - v.classList.remove("mirrorControl"); - } - } else { - v.className = "myVideo"; - container.classList.add("vidcon"); - if (session.mirrored && session.flipped) { - v.style.transform = " scaleX(-1) scaleY(-1) translate(0, 50%)"; - v.classList.add("mirrorControl"); - } else if (session.mirrored) { - v.style.transform = "scaleX(-1) translate(0, -50%)"; - v.classList.add("mirrorControl"); - } else if (session.flipped) { - v.style.transform = "scaleY(-1) translate(0, 50%)"; - v.classList.remove("mirrorControl"); - } else { - v.style.transform = " translate(0, -50%)"; - v.classList.remove("mirrorControl"); - } - } - - container.style.width = "100%"; - //container.style.height="100%"; - container.style.alignItems = "center"; - container.backgroundColor = "#666"; - } - - if (!session.windowed) { - window.onresize = updateMixer; - window.onorientationchange = function () { - if (Firefox) { - updateForceRotate(true); - } - setTimeout(async function () { - if (session.forceAspectRatio) { - await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - } - if (session.effect && session.effect === "7") { - digitalZoom(); - } - updateForceRotate(); - updateMixer(); - }, 200); - }; - } - - v.autoplay = true; - v.controls = session.showControls || false; - v.setAttribute("playsinline", ""); - v.muted = true; - v.id = "videosource"; - v.dataset.menu = "context-menu-video"; - - if (!session.cleanOutput) { - v.classList.add("task"); // this adds the right-click menu - } - - //if (!v.srcObject || v.srcObject.id !== stream.id) { - // v.srcObject = stream; - v.srcObject = outboundAudioPipeline(); - //} - - v.onpause = event => { - // prevent things from pausing; human or other - if (!(event.ctrlKey || event.metaKey)) { - log("Video paused; auto playing"); - event.currentTarget - .play() - .then(_ => { - log("playing 11"); - }) - .catch(warnlog); - } - }; - - v.addEventListener("click", function (e) { - // show stats of video if double clicked - log("click"); - try { - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - var [menu, innerMenu] = statsMenuCreator(); - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - printMyStats(innerMenu); - e.stopPropagation(); - return false; - } - } catch (e) { - errorlog(e); - } - }); - - updateReshareLink(); - - if (session.videoMutedFlag) { - session.videoMuted = true; - toggleVideoMute(true); - } - - clearInterval(session.updateLocalStatsInterval); - session.updateLocalStatsInterval = setInterval(function () { - updateLocalStats(); - }, session.statsInterval); - + if (session.chunkcast) { + session.chunkedStream(null); + } + + if (session.motionRecord && session.videoElement && !session.motionDetectionInterval) { + session.motionDetectionInterval = setTimeout(function () { + setInterval(function () { + motionDetection(session.videoElement, session.motionRecord); + }, 400); + }, 2000); + } + + if (session.poke) { + if (session.poke === true) { + let topic = await generateTopic(session.roomid, session.streamID, false, false, session.hash, window.location.hostname); + await triggerNotification(topic) + } else { + await triggerNotification(session.poke); + } + } + + if (session.autoEnd) { + log("Auto-end timer started: " + session.autoEnd + "ms"); + + // Create countdown display + const countdownDiv = document.createElement("div"); + countdownDiv.id = "autoEndCountdown"; + countdownDiv.style.cssText = "position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 10px 15px; border-radius: 5px; font-size: 16px; z-index: 9999; display: flex; align-items: center; gap: 8px;"; + countdownDiv.innerHTML = '⏱️--:--'; + document.body.appendChild(countdownDiv); + + // Update countdown every second + let remainingTime = session.autoEnd; + const updateCountdown = () => { + const minutes = Math.floor(remainingTime / 60000); + const seconds = Math.floor((remainingTime % 60000) / 1000); + document.getElementById("autoEndTime").textContent = + String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0'); + remainingTime -= 1000; + + if (remainingTime < 0) { + clearInterval(session.autoEndInterval); + } + }; + + updateCountdown(); // Initial update + session.autoEndInterval = setInterval(updateCountdown, 1000); + + // Set timer to end stream + session.autoEndTimer = setTimeout(() => { + log("Auto-end timer expired, ending stream"); + clearInterval(session.autoEndInterval); + session.hangup(); + }, session.autoEnd); + } + +}; +function triggerNotification(topic, customMessage = null) { + if (!topic) return false; + + const message = customMessage || ((session.label ? session.label : 'Someone') + + (session.roomid ? ' joined your room' : ' joined your stream')); + + const notifyUrl = `https://notify.vdo.ninja/?notify=${topic}&message=${encodeURIComponent(message)}`; + + console.log('Sending notification to:', notifyUrl); + + return fetch(notifyUrl) + .then(response => { + console.log('Notification response status:', response.status); + if (!response.ok) { + return response.text().then(text => { + try { + const errorData = JSON.parse(text); + console.error('Notification server error:', errorData); + return false; + } catch (e) { + console.error('Notification error response:', text); + return false; + } + }); + } + + return response.json(); + }) + .then(data => { + if (data === false) return false; + + console.log('Notification result:', data); + + // Check push results to diagnose issues + if (data.pushResults && Array.isArray(data.pushResults)) { + data.pushResults.forEach(result => { + if (!result.success) { + console.warn('Push notification failed:', result); + } + }); + } + + return data.success === true; + }) + .catch(error => { + console.error('Error sending notification:', error); + return false; + }); +} +function hashTopic(text) { + const salt1 = "abc12345ASB234ASD1116"; + const salt2 = "xyzJKL789MNO567PQR890"; + const salt3 = "9843kasdjfh234jhk234j"; + let saltedText = salt1 + text + salt2 + text.split('').reverse().join('') + salt3; + let hash = 0; + if (saltedText.length === 0) return "0"; + for (let i = 0; i < saltedText.length; i++) { + const char = saltedText.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + let hash2 = 0; + for (let i = 0; i < saltedText.length; i++) { + hash2 = ((hash2 << 7) + hash2) + saltedText.charCodeAt(i); + hash2 = hash2 & hash2; + } + const combinedHash = Math.abs(hash).toString(36) + Math.abs(hash2).toString(36); + if (combinedHash.length < 10) { + return combinedHash + Math.random().toString(36).substring(2, 12); + } + return combinedHash; +} + +async function generateTopic(roomId, pushId, viewId, password, hash, domain) { + domain = domain || 'vdo.ninja'; + if (!roomId && !viewId && !pushId) { + console.error('At least one of roomId, viewId or pushId is required'); + return null; + } + const components = { + room: roomId || viewId || pushId, + domain: domain.replace(/\./g, '_') + }; + let sensitiveData = Object.values(components).filter(Boolean).join('_'); + if (hash) { + sensitiveData += `_${hash}`; + } else if (password) { + const passwordHash = await generateHash(password); + sensitiveData += `_${passwordHash}`; + } + const secureTopicHash = hashTopic(sensitiveData); + const finalPrefix = components.domain; + const finalTopic = `${finalPrefix}_${secureTopicHash}`; + return finalTopic; +} + +async function publishScreen2(constraints, audioList = [], audio = true, overrideFramerate = false) { + // webcam stream is used to generated an SDP + log("SCREEN SHARE SETUP - publishScreen2"); + + if (!navigator.mediaDevices.getDisplayMedia) { + setTimeout(function () { + if (iOS || iPad) { + warnUser("Sorry, but your iOS browser does not support screen-sharing.\n\nPlease see this guide for an alternative method to do so.", false, false); + } else if (session.mobile) { + warnUser("Sorry, your browser does not support screen-sharing.\n\nThe Android native app should support it though.", false, false); + } else { + warnUser("Sorry, your browser does not support screen-sharing.\n\nPlease use the desktop versions of Firefox or Chrome instead."); + } + }, 1); + return false; + } + if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + if (!ElectronDesktopCapture) { + if (!(session.cleanOutput && session.cleanish == false)) { + warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)"); + } + return false; + } + } + + var streams = []; + log(audioList); + for (var i = 0; i < audioList.length; i++) { + // mic sources; not screen . + let constraintAudio = { video: false, audio: { deviceId: { exact: audioList[i] } } }; + + if (session.echoCancellation === false) { + // default should be ON. we won't even add it since deviceId is specified and Browser defaults to on already + constraintAudio.audio.echoCancellation = false; + } else { + constraintAudio.audio.echoCancellation = true; + } + if (session.autoGainControl === false) { + constraintAudio.audio.autoGainControl = false; + } else { + constraintAudio.audio.autoGainControl = true; + } + if (session.noiseSuppression === false) { + constraintAudio.audio.noiseSuppression = false; + } else { + constraintAudio.audio.noiseSuppression = true; + } + + if (session.voiceIsolation === true) { + constraintAudio.audio.voiceIsolation = true; + } + + if (session.audioInputChannels) { + if (constraintAudio.audio === true) { + constraintAudio.audio = {}; + constraintAudio.audio.channelCount = session.audioInputChannels; + } else if (constraintAudio.audio) { + constraintAudio.audio.channelCount = session.audioInputChannels; + } + } + + if (session.micSampleRate) { + if (constraintAudio.audio === true) { + constraintAudio.audio = {}; + constraintAudio.audio.sampleRate = parseInt(session.micSampleRate); + } else if (constraintAudio.audio) { + constraintAudio.audio.sampleRate = parseInt(session.micSampleRate); + } + } + if (session.micSampleSize) { + if (constraintAudio.audio === true) { + constraintAudio.audio = {}; + constraintAudio.audio.sampleSize = parseInt(session.micSampleSize); + } else if (constraintAudio.audio) { + constraintAudio.audio.sampleSize = parseInt(session.micSampleSize); + } + } + getUserMediaRequestID += 1; + var gumID = getUserMediaRequestID; + + if (Firefox) { + constraintAudio = toFirefoxConstraint(constraintAudio); + } + + log(constraintAudio); + + warnlog("navigator.mediaDevices.getUserMedia starting..."); + await navigator.mediaDevices + .getUserMedia(constraintAudio) + .then(stream => { + if (getUserMediaRequestID !== gumID) { + warnlog("GET USER MEDIA CALL HAS EXPIRED 3"); + stream.getTracks().forEach(function (track) { + stream.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + return; + } + streams.push(stream); + }) + .catch(errorlog); + } + + if (session.audioDevice === 0) { + constraints.audio = false; + } + + if (session.screenshareVideoOnly) { + constraints.audio = false; + } + + if (constraints.video !== false && Object.keys(constraints.video).length == 0) { + constraints.video = true; + } + + log(constraints); + getUserMediaRequestID += 1; + var gumID = getUserMediaRequestID; + return navigator.mediaDevices + .getDisplayMedia(constraints) + .then(async function (stream) { + if (getUserMediaRequestID !== gumID) { + warnlog("GET USER MEDIA CALL HAS EXPIRED 3"); + stream.getTracks().forEach(function (track) { + stream.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + return; + } + + try { + var constraint = {}; + + if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) { + constraint.aspectRatio = parseFloat(session.forceAspectRatio); + } else if (session.forceScreenShareAspectRatio) { + constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio); + } + if (overrideFramerate) { + constraint.frameRate = overrideFramerate; + } + if (Object.keys(constraint).length) { + await stream.getVideoTracks()[0].applyConstraints({ + advanced: [constraint] + }); + log({ + advanced: [constraint] + }); + } + } catch (e) { + errorlog(e); + } + + /// RETURN stream for preview? rather than jumping right in. + session.screenShareState = true; + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + notifyOfScreenShare(); + + try { + stream.getVideoTracks()[0].onended = function () { + toggleScreenShare(); + }; + } catch (e) { + log("No Video selected; screensharing?"); + } + + // OR, jump right in, and let user change from there + if (session.roomid !== false) { + if (session.roomid === "" && (!session.view || session.view === "")) { + if (session.manual === null) { + session.manual = session.manual === null ? true : session.manual; + } + if (!session.cleanOutput) { + var showReshare = getStorage("showReshare"); + if (showReshare) { + generateHash(session.streamID + session.salt + "bca321", 4) + .then(function (hash) { + // million to one error. + if (showReshare === hash) { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + } else if (session.permaid === null) { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + } + }) + .catch(errorlog); + } + } + } else { + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + log("ROOMID EANBLED"); + log("Update Mixer Event on REsize SET"); + window.onresize = updateMixer; + window.onorientationchange = function () { + if (Firefox) { + updateForceRotate(true); + } + setTimeout(async function () { + if (session.forceAspectRatio) { + await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + } + if (session.effect && session.effect === "7") { + digitalZoom(); + } + updateForceRotate(); + updateMixer(); + }, 200); + }; + joinRoom(session.roomid); + } + } else { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + getById("logoname").style.display = "none"; + } + + updatePushId(); + + if (stream.getAudioTracks().length) { + screenShareAudioTrack = stream.getAudioTracks()[0]; + } + + log("adding tracks"); + for (var i = 0; i < streams.length; i++) { + streams[i].getAudioTracks().forEach(track => { + stream.addTrack(track); + }); + } + streams = null; + if (!session.screenshareVideoOnly && session.audioDevice !== 0) { + if (stream.getAudioTracks().length == 0) { + if (!session.cleanOutput) { + if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + // Electron has no audio. + } else if (!getStorage("leaveInPeaceSSWarning")) { + setStorage("leaveInPeaceSSWarning", "true", 720); + setTimeout(function () { + warnUser(getTranslation("no-audio-source-detected"), 10000, false); + }, 300); + } + } + } + } + + try { + session.streamSrc = stream; + } catch (e) { + errorlog(e); + } + toggleMute(true); + + var v = createVideoElement(); + session.videoElement = v; + + if (session.streamID) { + session.videoElement.dataset.sid = session.streamID; + } + + var container = document.createElement("div"); + v.container = container; + container.id = "container_screen"; + container.style.height = "100%"; + + if (session.cleanOutput) { + v.style.maxWidth = "100%"; + v.style.boxShadow = "none"; + } + + //container.className = "vidcon"; + getById("gridlayout").appendChild(container); + + if (session.nopreview) { + v.style.display = "none"; + container.style.display = "none"; + } + + //if (session.cover){ + // container.style.setProperty('height', '100%', 'important'); + //} + + container.appendChild(v); + + v.className = "tile"; + + if (session.director) { + } else if (session.scene !== false) { + setTimeout(function () { + updateMixer(); + }, 1); + } else if (session.roomid !== false) { + if (session.roomid === "") { + if (!session.view || session.view === "") { + getById("mutespeakerbutton").classList.add("hidden"); + if (session.fullscreen) { + session.windowed = session.windowed === null ? false : session.windowed; + } else { + session.windowed = session.windowed === null ? true : session.windowed; + } + if (!session.windowed) { + if (session.mirrored && session.flipped) { + v.style.transform = " scaleX(-1) scaleY(-1)"; + v.classList.add("mirrorControl"); + } else if (session.mirrored) { + v.style.transform = "scaleX(-1)"; + v.classList.add("mirrorControl"); + } else if (session.flipped) { + v.style.transform = "scaleY(-1)"; + v.classList.remove("mirrorControl"); + } else { + v.style.transform = ""; + v.classList.remove("mirrorControl"); + } + } else { + v.className = "myVideo"; + if (session.mirrored && session.flipped) { + v.style.transform = " scaleX(-1) scaleY(-1) translate(0, 50%)"; + v.classList.add("mirrorControl"); + } else if (session.mirrored) { + v.style.transform = "scaleX(-1) translate(0, -50%)"; + v.classList.add("mirrorControl"); + } else if (session.flipped) { + v.style.transform = "scaleY(-1) translate(0, 50%)"; + v.classList.remove("mirrorControl"); + } else { + v.style.transform = " translate(0, -50%)"; + v.classList.remove("mirrorControl"); + } + } + container.style.width = "100%"; + //container.style.height="100%"; + container.style.alignItems = "center"; + container.backgroundColor = "#666"; + setTimeout(function () { + dragElement(v); + }, 1000); + play(); + } else { + play(); + setTimeout(function () { + updateMixer(); + }, 1); + } + } else { + setTimeout(function () { + updateMixer(); + }, 1); + } + } else { + getById("mutespeakerbutton").classList.add("hidden"); + if (session.fullscreen) { + session.windowed = session.windowed === null ? false : session.windowed; + } else { + session.windowed = session.windowed === null ? true : session.windowed; + } + if (!session.windowed) { + if (session.mirrored && session.flipped) { + v.style.transform = " scaleX(-1) scaleY(-1)"; + v.classList.add("mirrorControl"); + } else if (session.mirrored) { + v.style.transform = "scaleX(-1)"; + v.classList.add("mirrorControl"); + } else if (session.flipped) { + v.style.transform = "scaleY(-1)"; + v.classList.remove("mirrorControl"); + } else { + v.style.transform = ""; + v.classList.remove("mirrorControl"); + } + } else { + v.className = "myVideo"; + container.classList.add("vidcon"); + if (session.mirrored && session.flipped) { + v.style.transform = " scaleX(-1) scaleY(-1) translate(0, 50%)"; + v.classList.add("mirrorControl"); + } else if (session.mirrored) { + v.style.transform = "scaleX(-1) translate(0, -50%)"; + v.classList.add("mirrorControl"); + } else if (session.flipped) { + v.style.transform = "scaleY(-1) translate(0, 50%)"; + v.classList.remove("mirrorControl"); + } else { + v.style.transform = " translate(0, -50%)"; + v.classList.remove("mirrorControl"); + } + } + + container.style.width = "100%"; + //container.style.height="100%"; + container.style.alignItems = "center"; + container.backgroundColor = "#666"; + } + + if (!session.windowed) { + window.onresize = updateMixer; + window.onorientationchange = function () { + if (Firefox) { + updateForceRotate(true); + } + setTimeout(async function () { + if (session.forceAspectRatio) { + await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + } + if (session.effect && session.effect === "7") { + digitalZoom(); + } + updateForceRotate(); + updateMixer(); + }, 200); + }; + } + + v.autoplay = true; + v.controls = session.showControls || false; + v.setAttribute("playsinline", ""); + v.muted = true; + v.id = "videosource"; + v.dataset.menu = "context-menu-video"; + + if (!session.cleanOutput) { + v.classList.add("task"); // this adds the right-click menu + } + + //if (!v.srcObject || v.srcObject.id !== stream.id) { + // v.srcObject = stream; + v.srcObject = outboundAudioPipeline(); + //} + + v.onpause = event => { + // prevent things from pausing; human or other + if (!(event.ctrlKey || event.metaKey)) { + log("Video paused; auto playing"); + event.currentTarget + .play() + .then(_ => { + log("playing 11"); + }) + .catch(warnlog); + } + }; + + v.addEventListener("click", function (e) { + // show stats of video if double clicked + log("click"); + try { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + var [menu, innerMenu] = statsMenuCreator(); + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + printMyStats(innerMenu); + e.stopPropagation(); + return false; + } + } catch (e) { + errorlog(e); + } + }); + + updateReshareLink(); + + if (session.videoMutedFlag) { + session.videoMuted = true; + toggleVideoMute(true); + } + + clearInterval(session.updateLocalStatsInterval); + session.updateLocalStatsInterval = setInterval(function () { + updateLocalStats(); + }, session.statsInterval); + if (session.meshcast2) { await meshcast2(); } else if (session.whipOutput) { // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024 @@ -37543,879 +38000,879 @@ async function publishScreen2(constraints, audioList = [], audio = true, overrid whepOut(); } - - session.seeding = true; - session.seedStream(); - - //pokeIframeAPI('started-screenshare'); // depreciated - pokeIframeAPI("screen-share-state", true, null, session.streamID); // (action, value = null, UUID = null, SID=null) - - if (session.autorecord || session.autorecordlocal) { - log("AUTO RECORD START"); - setTimeout( - function (v) { - var videoKbps = session.recordDefault; - if (session.recordLocal !== false) { - videoKbps = session.recordLocal; - } - if (session.director) { - recordVideo(document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']"), null, videoKbps); - } else if (v.stopWriter || v.recording) { - } else if (v.startWriter) { - v.startWriter(); - } else { - recordLocalVideo(null, videoKbps, v); - } - }, - 2000, - v - ); - } - - return true; - }) - .catch(function (err) { - errorlog(err); - errorlog(err.name); - if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") { - // User Stopped it. (is this next part needed??) - session.screenShareState = false; - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - notifyOfScreenShare(); - - if (macOS) { - warnUser(getTranslation("screen-permissions-denied"), false, false); - } - return false; - } else { - if (audio == true) { - if (err.name == "NotReadableError") { - if (!session.cleanOutput) { - warnUser(getTranslation("change-audio-output-device"), false, false); - } - constraints.audio = false; - return publishScreen2(constraints, audioList, false); - } else { - constraints.audio = false; - if (!session.cleanOutput) { - setTimeout(function () { - warnUser(err); - }, 1); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported - } - return publishScreen2(constraints, audioList, false); - } - } else { - if (!session.cleanOutput) { - setTimeout(function () { - warnUser(err); - }, 1); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported - } - return false; - } - } - }); -} // publishStream2 - -var transferList = []; -var msgTransferList = []; - -function cancelFile(ele) { - var idx = ele.dataset.tid; - try { - transferList[idx].dc.close(); - } catch (e) { } - transferList[idx].status = 5; - updateDownloadLink(idx); -} - -function requestFile(ele) { - var idx = ele.dataset.tid; - transferList[idx].status = 1; - - var fid = ele.dataset.fid; - var UUID = ele.dataset.uuid; - var msg = {}; - msg.requestFile = fid; - msg.UUID = UUID; - session.sendRequest(msg, msg.UUID); - - updateDownloadLink(idx); - pokeIframeAPI("request-file", fid, UUID); -} - -function clearDownloadFile(ele) { - var idx = ele.dataset.tid; - transferList[idx].status = 6; - updateDownloadLink(idx); -} - -function addDownloadLink(fileList, UUID, pc) { - if (session.nodownloads) { - return; - } // downloads are blocked - log(fileList); - if (!fileList || !fileList.length) { - return; - } - for (var i = 0; i < fileList.length; i++) { - fileList[i].UUID = UUID; - fileList[i].completed = 0; - fileList[i].status = 0; - fileList[i].time = Date.now(); - fileList[i].pc = pc[UUID]; - transferList.push(fileList[i]); - } - - if (session.chatbutton === false) { - return; - } // messages can still appear as overlays - - updateMessages(); - - if (session.beepToNotify) { - playtone(); - } - - if (session.chat == false) { - getById("chattoggle").className = "las la-comments toggleSize pulsate"; - getById("chatbutton").className = "float"; - - if (getById("chatNotification").value) { - getById("chatNotification").value = getById("chatNotification").value + 1; - } else { - getById("chatNotification").value = 1; - } - getById("chatNotification").classList.add("notification", "red"); - } - - //if (session.broadcastChannel !== false) { - // session.broadcastChannel.postMessage(data); /* send */ - //} -} - -function updateDownloadLink(idx) { - idx = parseInt(idx); - var elements = document.querySelectorAll('[data-tid="' + idx + '"]'); - if (elements[0]) { - if (transferList[idx].status === 0) { - elements[0].innerHTML = "Download it here"; - } else if (transferList[idx].status === 1) { - elements[0].innerHTML = "Requested"; - //elements[0].onclick='cancelFile(this);' - } else if (transferList[idx].status === 2) { - elements[0].innerHTML = "Downloading: " + parseInt(transferList[idx].completed * 100) + "%"; - elements[0].onclick = function () { - cancelFile(this); - }; - } else if (transferList[idx].status === 3) { - elements[0].innerHTML = "Completed"; - elements[0].onclick = null; - elements[0].disabled = true; - } else if (transferList[idx].status === 4) { - elements[0].innerHTML = "No longer available"; - elements[0].onclick = null; - elements[0].disabled = true; - } else if (transferList[idx].status === 5) { - elements[0].innerHTML = "Cancelled"; - elements[0].onclick = null; - elements[0].disabled = true; - } else if (transferList[idx].status === 6) { - getById("transfer_" + idx).style.display = "none"; - //delete(transferList[idx]); - } - } -} - -function showDownloadLinks() { - if (session.nodownloads) { - return; - } // downloads are blocked - msgTransferList = []; - if (!transferList || !transferList.length) { - return; - } - for (var i = 0; i < transferList.length; i++) { - fileShareMessage(transferList[i], i); - } -} - -function fileShareMessage(fileinfo, idx) { - fileinfo.name = sanitizeChat(fileinfo.name); // keep it clean. - - var label = false; - if (fileinfo.pc) { - if (fileinfo.pc.label) { - label = sanitizeLabel(fileinfo.pc.label); - } - } - var data = {}; - data.idx = idx; - if (fileinfo.status === 0) { - data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    Do you trust them? "; - } else if (fileinfo.status === 1) { - data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; - } else if (fileinfo.status === 2) { - data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; - } else if (fileinfo.status === 3) { - data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; - transferList[idx].status = 6; - } else if (fileinfo.status === 4) { - data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; - } else if (fileinfo.status === 5) { - data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; - transferList[idx].status = 6; - } else if (fileinfo.status === 6) { - return; - } - - var director = false; // add back in later. - if (session.directorList.indexOf(fileinfo.UUID) >= 0) { - director = true; - } - if (label) { - data.label = label; - if (director) { - data.label = "" + data.label + ""; - } else { - data.label = "" + data.label + ""; - } - } else if (director) { - data.label = "Director"; - } else { - const identifier = getPeerDisplayName(fileinfo.UUID, false); - if (identifier) { - data.label = "" + identifier + ""; - } else { - data.label = "Someone"; - } - } - data.type = "action"; - - msgTransferList.push(data); -} - -session.shareFile = function (ele, UUID = false, event = false) { - const file = ele.files[0]; - if (!file) return; - - try { - const fileId = session.generateStreamID(7); - - session.hostedFiles.push({ - id: fileId, - name: file.name, - size: file.size, - state: 1, - restricted: UUID || false, - file: file // Store the actual File object - }); - - log(session.hostedFiles); - - enhancedFileTransfers.initTransfer(fileId, file.size); - - // Provide file list to appropriate peers - if (UUID === false) { - for (let peerUUID in session.pcs) { - session.provideFileList(peerUUID); - } - for (let peerUUID in session.rpcs) { - if (!(peerUUID in session.pcs)) { - session.provideFileList(peerUUID); - } - } - } else { - session.provideFileList(UUID); - } - - pokeIframeAPI("file-share", true); - - // Update the file share display - updateFileShare(); - - closeModal(); - - } catch (e) { - errorlog(e); - } finally { - // Clear the file input - ele.value = ''; - } -}; - -function arrayBufferToString(buffer, encoding, callback) { - var blob = new Blob([buffer], { type: "text/plain" }); - var reader = new FileReader(); - reader.onload = function (evt) { - callback(evt.target.result); - }; - reader.readAsText(blob, encoding); -} -session.hostFile = function (ele, event = false) { - // webcam stream is used to generated an SDP - log("FILE TRANSFER SETUP"); - session.hostedFiles = []; - - for (var i = 0; i < ele.files.length; i++) { - session.hostedFiles.push({ - id: session.generateStreamID(7), - name: ele.files[i].name, - size: ele.files[i].size, - state: 1, - restricted: false, - file: ele.files[i] // Store the actual File object - }); - } - - log(session.hostedFiles); - - var container = document.createElement("div"); - container.id = "container_host"; - getById("gridlayout").appendChild(container); - - if (session.cover) { - container.style.setProperty("height", "100%", "important"); - } - - if (session.roomid !== false) { - if (session.roomid === "" && (!session.view || session.view === "")) { - } else { - log("ROOMID EANBLED"); - //log("Update Mixer Event on REsize SET"); - //window.addEventListener("resize", updateMixer);// TODO FIX - //window.addEventListener("orientationchange", updateMixer);// TODO FIX - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - joinRoom(session.roomid); - } - } else { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - getById("logoname").style.display = "none"; - } - getById("head1").className = "hidden"; - - getById("head1").className = "hidden"; - getById("head2").className = "hidden"; - - if (!session.cleanOutput) { - getById("chatbutton").className = "float"; - getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. - // getById("mediafileshare").classList.remove("hidden"); - getById("hangupbutton").className = "float"; - getById("controlButtons").classList.remove("hidden"); - // getById("legal").classList.remove("hidden"); - //getById("helpbutton").style.display = "inherit"; - //getById("reportbutton").style.display = ""; - } else { - getById("controlButtons").classList.add("hidden"); - // getById("legal").classList.add("hidden"); - } - - updatePushId(); - updateReshareLink(); - updateFileShare(); - - pokeIframeAPI("file-share", true); - pokeIframeAPI("started-fileshare"); // deprecated - - clearInterval(session.updateLocalStatsInterval); - session.updateLocalStatsInterval = setInterval(function () { - updateLocalStats(); - }, session.statsInterval); - - session.seeding = true; - session.seedStream(); - -}; - -function updateReshareLink() { - try { - var m = getById("mainmenu"); - m.remove(); - document.querySelectorAll(".hidden2").forEach(ele2 => { - ele2.classList.remove("hidden2"); - }); - } catch (e) { } - - var added = ""; - if (session.defaultPassword === false) { - if (session.password !== false) { - added = "&pw=" + session.password; - } else { - added = "&pw=false"; - } - } - - var wss = ""; - if (session.wssSetViaUrl) { - if (session.customWSS && session.customWSS !== true) { - wss = "&pie=" + session.customWSS; - } else if (session.customWSS == true) { - wss = "&wss=" + session.wss; - } else { - wss = "&wss2=" + session.wss; - } - } - if (session.audience && !session.audienceToken) { - if (document.getElementById("reshare")) { - if (!session.cleanOutput) { - getById("copythisurl").innerHTML = ""; - getById("head3a").classList.remove("hidden"); - } - document.getElementById("reshare").href = null; - document.getElementById("reshare").text = "loading public view link..."; - document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px"; - } - return; - } else if (session.audience) { - var shareLink = "https://" + location.host + location.pathname + "?view=" + session.streamID + added + wss + "&audience=" + session.audienceToken; - if (document.getElementById("reshare")) { - if (!session.cleanOutput) { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - getById("copythisurl").innerHTML = 'This is your public audience link   '; - } - document.getElementById("reshare").href = shareLink; - document.getElementById("reshare").text = shareLink; - document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px"; - } - pokeIframeAPI("share-link", shareLink); - return; - } - var shareLink = "https://" + location.host + location.pathname + "?view=" + session.streamID + added + wss; - if (document.getElementById("reshare")) { - document.getElementById("reshare").href = shareLink; - document.getElementById("reshare").text = shareLink; - document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px"; - } - if (session.whipOutput) { - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - } - pokeIframeAPI("share-link", shareLink); -} - -function cleanupMediaState(vid) { - if (session.canvasSource) { - session.canvasSource.destroy(); - session.canvasSource = null; - } - - if (vid) { - if (vid.srcObject) { - const tracks = vid.srcObject.getTracks(); - tracks.forEach(track => track.stop()); - vid.srcObject = null; - } - if (vid.currentObjectURL) { - URL.revokeObjectURL(vid.currentObjectURL); - vid.currentObjectURL = null; - } - vid.src = ''; - } - - if (session.streamSrc) { - const tracks = session.streamSrc.getTracks(); - tracks.forEach(track => track.stop()); - session.streamSrc = null; - } -} - -session.changePublishFile = function (ele, event) { - log("FILE STREAM CHANGE"); - var files = Array.from(ele.files); - var vid = getById("videosource"); - - // Clean up existing state first - cleanupMediaState(vid); - - if (files[0].type.startsWith('image/')) { - const objectURL = URL.createObjectURL(files[0]); - session.canvasSource = new CanvasStreamSource(); - session.canvasSource.imgSrc = objectURL; - window.postMessage({ type: 'canvas-frame', frame: session.canvasSource.imgSrc }, '*'); - - session.streamSrc = session.canvasSource.getStream(); - // Process the stream like we do for video - session.streamSrc = outboundAudioPipeline(session.streamSrc); - - var tracks = session.streamSrc.getVideoTracks(); - if (tracks.length) { - pushOutVideoTrack(tracks[0]); - } - senderAudioUpdate(); - - vid.play(); // We still need play() but don't set srcObjec - } else { - log("FILE VIDEO STREAM CHANGE"); - vid.playlist = files; - nextFilePlaylist(vid); - } - session.applySoloChat(); - session.applyIsolatedChat(); - toggleMute(true); -}; - -function nextFilePlaylist(vid) { - log("nextFilePlaylistD"); - var filenext = vid.playlist.shift(); - - cleanupMediaState(vid); - - if (!filenext) { - if (session.blankStream) { - session.streamSrc = session.blankStream; - pushOutVideoTrack(session.blankStream.getVideoTracks()[0]); - } - return; - } - - vid.pause(); - vid.currentObjectURL = URL.createObjectURL(filenext); - vid.src = vid.currentObjectURL; - - // Wrap the onloadeddata in error handling - vid.onloadeddata = function () { - try { - if (Firefox) { - session.streamSrc = vid.mozCaptureStream(); - } else { - session.streamSrc = vid.captureStream(); - } - updateMixer(); - var tracks = session.streamSrc.getVideoTracks(); - if (tracks.length) { - pushOutVideoTrack(tracks[0]); // video only - } - senderAudioUpdate(false, session.streamSrc); - - } catch (e) { - errorlog(e); - if (session.blankStream) { - session.streamSrc = session.blankStream; - pushOutVideoTrack(session.blankStream.getVideoTracks()[0]); - } - } - }; - - vid.onerror = (e) => { - errorlog(e); - cleanupMediaState(vid); - if (session.blankStream) { - session.streamSrc = session.blankStream; - pushOutVideoTrack(session.blankStream.getVideoTracks()[0]); - } - // Try next file if available - if (vid.playlist.length) { - setTimeout(() => nextFilePlaylist(vid), 1000); - } - }; - - vid.load(); - - vid.play() - .then(_ => { - log("playing 2"); - }) - .catch(e => { - warnlog(e); - if (session.blankStream) { - session.streamSrc = session.blankStream; - pushOutVideoTrack(session.blankStream.getVideoTracks()[0]); - } - // Try next file if available - if (vid.playlist.length) { - setTimeout(() => nextFilePlaylist(vid), 1000); - } - }); -} - -session.publishFile = function (ele, event) { - log("FILE STREAM SETUP"); - - if (!session.blankStream) { - const canvas = document.createElement('canvas'); - canvas.width = 1280; - canvas.height = 720; - const ctx = canvas.getContext('2d'); - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - session.blankStream = canvas.captureStream(30); - session.streamSrc = session.blankStream; - } - - if (session.transcript) { - setTimeout(function () { - setupClosedCaptions(); - }, 1000); - } - - const files = Array.from(ele.files); - log(files); - const file = files[0]; - const isImage = file.type.startsWith('image/'); - - var fileURL = URL.createObjectURL(file); - var container = document.createElement("div"); - container.id = "container"; - - if (session.cover) { - container.style.setProperty("height", "100%", "important"); - } - - var v = createVideoElement(); - v.container = container; - - if (session.cleanOutput) { - container.style.height = "100%"; - v.style.maxWidth = "100%"; - v.style.boxShadow = "none"; - } - - container.appendChild(v); - - if (session.streamID) { - v.dataset.sid = session.streamID; - } - - v.autoplay = false; - if (session.showControls !== null) { - v.controls = session.showControls; - } else { - v.controls = true; - } - v.muted = false; - v.loop = files.length == 1; - - v.id = "videosource"; - v.dataset.menu = "context-menu-video"; - v.setAttribute("playsinline", ""); - - if (isImage) { - - session.canvasSource = new CanvasStreamSource(); - session.canvasSource.imgSrc = fileURL; - - window.postMessage({ type: 'canvas-frame', frame: session.canvasSource.imgSrc }, '*'); - - session.streamSrc = session.canvasSource.getStream(); - v.srcObject = session.streamSrc; - v.play(); - handleUIAndStream(); - } else { - v.src = fileURL; - } - - v.playlist = files; - v.addEventListener("ended", function (e) { - log("MY HANDLER TRIGGERED"); - var vid = getById("videosource"); - nextFilePlaylist(vid); - }, false); - - v.className = "tile clean fileshare"; - - session.videoElement = v; - session.mirrorExclude = true; - - v.addEventListener("click", (e) => { - log("click"); - try { - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - - var [menu, innerMenu] = statsMenuCreator(); - - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - - printMyStats(innerMenu); - e.stopPropagation(); - return false; - } - } catch (e) { - errorlog(e); - } - }); - - v.touchTimeOut = null; - v.touchLastTap = 0; - v.touchCount = 0; - - v.addEventListener("touchend", (event) => { - if (session.disableMouseEvents) { - return; - } - log("touched"); - - document.onmousemove = null; - document.ontouchmove = null; - - var currentTime = new Date().getTime(); - var tapLength = currentTime - v.touchLastTap; - clearTimeout(v.touchTimeOut); - if (tapLength < 500 && tapLength > 0) { - log("double touched"); - v.touchCount += 1; - event.preventDefault(); - if (v.touchCount < 5) { - v.touchLastTap = currentTime; - return false; - } - v.touchLastTap = 0; - v.touchCount = 0; - - var [menu, innerMenu] = statsMenuCreator(); - - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - - printMyStats(innerMenu); - event.stopPropagation(); - return false; - } else { - v.touchCount = 1; - v.touchTimeOut = setTimeout( - function (vv) { - clearTimeout(vv.touchTimeOut); - vv.touchLastTap = 0; - vv.touchCount = 0; - }, - 5000, - v - ); - v.touchLastTap = currentTime; - } - }); - - v.onerror = () => { - if (session.cleanOutput) { - errorlog("File failed to load.\n\nSelect a compatible media file."); - } else { - warnUser("File failed to load.\n\nSelect a compatible media file."); - } - }; - - v.onloadeddata = async () => { - session.mediafileShare = true; - getById("mainmenu").remove(); - - if (Firefox) { - session.streamSrc = v.mozCaptureStream(); - } else { - session.streamSrc = v.captureStream(); - } - - if (session.framegrab && session.framegrabAudioRequested && session.pendingFramegrabAudioSettings) { - try { - const maybePromise = session.startFramegrabAudio(session.pendingFramegrabAudioSettings); - if (maybePromise && typeof maybePromise.then === "function") { - maybePromise.catch(errorlog); - } - } catch (err) { - errorlog(err); - } - } - - handleUIAndStream(); - }; - - getById("gridlayout").appendChild(container); - - function handleUIAndStream() { - if (session.roomid !== false) { - if (session.roomid === "" && (!session.view || session.view === "")) { - } else { - log("ROOMID ENABLED"); - log("Update Mixer Event on REsize SET"); - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - joinRoom(session.roomid); - } - } else { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - getById("logoname").style.display = "none"; - } - - updatePushId(); - - getById("head1").className = "hidden"; - getById("head2").className = "hidden"; - - if (!session.cleanOutput) { - getById("chatbutton").className = "float"; - getById("mediafileshare").classList.remove("hidden"); - getById("hangupbutton").className = "float"; - getById("controlButtons").classList.remove("hidden"); - // getById("legal").classList.remove("hidden"); - } else { - getById("controlButtons").classList.add("hidden"); - // getById("legal").classList.add("hidden"); - } - - toggleMute(true); - - if (session.director) { - } else if (session.scene !== false) { - } else if (session.roomid !== false) { - if (session.roomid === "") { - if (!session.view || session.view === "") { - if (session.fullscreen) { - session.windowed = session.windowed === null ? false : session.windowed; - } else if (session.minipreview) { - session.windowed = session.windowed === null ? false : session.windowed; - } else { - session.windowed = session.windowed === null ? true : session.windowed; - } - - if (session.windowed) { - session.videoElement.className = "myVideo clean fileshare"; - container.classList.add("vidcon"); - } - getById("mutespeakerbutton").classList.add("hidden"); - container.style.width = "100%"; - container.style.alignItems = "center"; - container.backgroundColor = "#666"; - play(); - } else { - session.windowed = session.windowed === null ? false : session.windowed; - play(); - } - } else { - if (session.stereo == 5) { - session.stereo = 3; - } - session.windowed = session.windowed === null ? false : session.windowed; - } - } else { - if (session.fullscreen) { - session.windowed = session.windowed === null ? false : session.windowed; - } else if (session.minipreview) { - session.windowed = session.windowed === null ? false : session.windowed; - } else { - session.windowed = session.windowed === null ? true : session.windowed; - } - if (session.windowed) { - session.videoElement.className = "myVideo clean fileshare"; - container.classList.add("vidcon"); - } - getById("mutespeakerbutton").classList.add("hidden"); - container.style.width = "100%"; - container.style.alignItems = "center"; - container.backgroundColor = "#666"; - } - applyMirror(session.mirrorExclude); - - updateReshareLink(); - pokeIframeAPI("started-fileshare"); - pokeIframeAPI("file-share", true); - - clearInterval(session.updateLocalStatsInterval); - session.updateLocalStatsInterval = setInterval(function () { - updateLocalStats(); - }, session.statsInterval); - + + session.seeding = true; + session.seedStream(); + + //pokeIframeAPI('started-screenshare'); // depreciated + pokeIframeAPI("screen-share-state", true, null, session.streamID); // (action, value = null, UUID = null, SID=null) + + if (session.autorecord || session.autorecordlocal) { + log("AUTO RECORD START"); + setTimeout( + function (v) { + var videoKbps = session.recordDefault; + if (session.recordLocal !== false) { + videoKbps = session.recordLocal; + } + if (session.director) { + recordVideo(document.querySelector("[data-action-type='recorder-local'][data-sid='" + session.streamID + "']"), null, videoKbps); + } else if (v.stopWriter || v.recording) { + } else if (v.startWriter) { + v.startWriter(); + } else { + recordLocalVideo(null, videoKbps, v); + } + }, + 2000, + v + ); + } + + return true; + }) + .catch(function (err) { + errorlog(err); + errorlog(err.name); + if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") { + // User Stopped it. (is this next part needed??) + session.screenShareState = false; + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + notifyOfScreenShare(); + + if (macOS) { + warnUser(getTranslation("screen-permissions-denied"), false, false); + } + return false; + } else { + if (audio == true) { + if (err.name == "NotReadableError") { + if (!session.cleanOutput) { + warnUser(getTranslation("change-audio-output-device"), false, false); + } + constraints.audio = false; + return publishScreen2(constraints, audioList, false); + } else { + constraints.audio = false; + if (!session.cleanOutput) { + setTimeout(function () { + warnUser(err); + }, 1); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported + } + return publishScreen2(constraints, audioList, false); + } + } else { + if (!session.cleanOutput) { + setTimeout(function () { + warnUser(err); + }, 1); // TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio capture is not supported + } + return false; + } + } + }); +} // publishStream2 + +var transferList = []; +var msgTransferList = []; + +function cancelFile(ele) { + var idx = ele.dataset.tid; + try { + transferList[idx].dc.close(); + } catch (e) { } + transferList[idx].status = 5; + updateDownloadLink(idx); +} + +function requestFile(ele) { + var idx = ele.dataset.tid; + transferList[idx].status = 1; + + var fid = ele.dataset.fid; + var UUID = ele.dataset.uuid; + var msg = {}; + msg.requestFile = fid; + msg.UUID = UUID; + session.sendRequest(msg, msg.UUID); + + updateDownloadLink(idx); + pokeIframeAPI("request-file", fid, UUID); +} + +function clearDownloadFile(ele) { + var idx = ele.dataset.tid; + transferList[idx].status = 6; + updateDownloadLink(idx); +} + +function addDownloadLink(fileList, UUID, pc) { + if (session.nodownloads) { + return; + } // downloads are blocked + log(fileList); + if (!fileList || !fileList.length) { + return; + } + for (var i = 0; i < fileList.length; i++) { + fileList[i].UUID = UUID; + fileList[i].completed = 0; + fileList[i].status = 0; + fileList[i].time = Date.now(); + fileList[i].pc = pc[UUID]; + transferList.push(fileList[i]); + } + + if (session.chatbutton === false) { + return; + } // messages can still appear as overlays + + updateMessages(); + + if (session.beepToNotify) { + playtone(); + } + + if (session.chat == false) { + getById("chattoggle").className = "las la-comments toggleSize pulsate"; + getById("chatbutton").className = "float"; + + if (getById("chatNotification").value) { + getById("chatNotification").value = getById("chatNotification").value + 1; + } else { + getById("chatNotification").value = 1; + } + getById("chatNotification").classList.add("notification", "red"); + } + + //if (session.broadcastChannel !== false) { + // session.broadcastChannel.postMessage(data); /* send */ + //} +} + +function updateDownloadLink(idx) { + idx = parseInt(idx); + var elements = document.querySelectorAll('[data-tid="' + idx + '"]'); + if (elements[0]) { + if (transferList[idx].status === 0) { + elements[0].innerHTML = "Download it here"; + } else if (transferList[idx].status === 1) { + elements[0].innerHTML = "Requested"; + //elements[0].onclick='cancelFile(this);' + } else if (transferList[idx].status === 2) { + elements[0].innerHTML = "Downloading: " + parseInt(transferList[idx].completed * 100) + "%"; + elements[0].onclick = function () { + cancelFile(this); + }; + } else if (transferList[idx].status === 3) { + elements[0].innerHTML = "Completed"; + elements[0].onclick = null; + elements[0].disabled = true; + } else if (transferList[idx].status === 4) { + elements[0].innerHTML = "No longer available"; + elements[0].onclick = null; + elements[0].disabled = true; + } else if (transferList[idx].status === 5) { + elements[0].innerHTML = "Cancelled"; + elements[0].onclick = null; + elements[0].disabled = true; + } else if (transferList[idx].status === 6) { + getById("transfer_" + idx).style.display = "none"; + //delete(transferList[idx]); + } + } +} + +function showDownloadLinks() { + if (session.nodownloads) { + return; + } // downloads are blocked + msgTransferList = []; + if (!transferList || !transferList.length) { + return; + } + for (var i = 0; i < transferList.length; i++) { + fileShareMessage(transferList[i], i); + } +} + +function fileShareMessage(fileinfo, idx) { + fileinfo.name = sanitizeChat(fileinfo.name); // keep it clean. + + var label = false; + if (fileinfo.pc) { + if (fileinfo.pc.label) { + label = sanitizeLabel(fileinfo.pc.label); + } + } + var data = {}; + data.idx = idx; + if (fileinfo.status === 0) { + data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    Do you trust them? "; + } else if (fileinfo.status === 1) { + data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; + } else if (fileinfo.status === 2) { + data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; + } else if (fileinfo.status === 3) { + data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; + transferList[idx].status = 6; + } else if (fileinfo.status === 4) { + data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; + } else if (fileinfo.status === 5) { + data.msg = " has a shared a file with you:
    " + fileinfo.name + "
    "; + transferList[idx].status = 6; + } else if (fileinfo.status === 6) { + return; + } + + var director = false; // add back in later. + if (session.directorList.indexOf(fileinfo.UUID) >= 0) { + director = true; + } + if (label) { + data.label = label; + if (director) { + data.label = "" + data.label + ""; + } else { + data.label = "" + data.label + ""; + } + } else if (director) { + data.label = "Director"; + } else { + const identifier = getPeerDisplayName(fileinfo.UUID, false); + if (identifier) { + data.label = "" + identifier + ""; + } else { + data.label = "Someone"; + } + } + data.type = "action"; + + msgTransferList.push(data); +} + +session.shareFile = function (ele, UUID = false, event = false) { + const file = ele.files[0]; + if (!file) return; + + try { + const fileId = session.generateStreamID(7); + + session.hostedFiles.push({ + id: fileId, + name: file.name, + size: file.size, + state: 1, + restricted: UUID || false, + file: file // Store the actual File object + }); + + log(session.hostedFiles); + + enhancedFileTransfers.initTransfer(fileId, file.size); + + // Provide file list to appropriate peers + if (UUID === false) { + for (let peerUUID in session.pcs) { + session.provideFileList(peerUUID); + } + for (let peerUUID in session.rpcs) { + if (!(peerUUID in session.pcs)) { + session.provideFileList(peerUUID); + } + } + } else { + session.provideFileList(UUID); + } + + pokeIframeAPI("file-share", true); + + // Update the file share display + updateFileShare(); + + closeModal(); + + } catch (e) { + errorlog(e); + } finally { + // Clear the file input + ele.value = ''; + } +}; + +function arrayBufferToString(buffer, encoding, callback) { + var blob = new Blob([buffer], { type: "text/plain" }); + var reader = new FileReader(); + reader.onload = function (evt) { + callback(evt.target.result); + }; + reader.readAsText(blob, encoding); +} +session.hostFile = function (ele, event = false) { + // webcam stream is used to generated an SDP + log("FILE TRANSFER SETUP"); + session.hostedFiles = []; + + for (var i = 0; i < ele.files.length; i++) { + session.hostedFiles.push({ + id: session.generateStreamID(7), + name: ele.files[i].name, + size: ele.files[i].size, + state: 1, + restricted: false, + file: ele.files[i] // Store the actual File object + }); + } + + log(session.hostedFiles); + + var container = document.createElement("div"); + container.id = "container_host"; + getById("gridlayout").appendChild(container); + + if (session.cover) { + container.style.setProperty("height", "100%", "important"); + } + + if (session.roomid !== false) { + if (session.roomid === "" && (!session.view || session.view === "")) { + } else { + log("ROOMID EANBLED"); + //log("Update Mixer Event on REsize SET"); + //window.addEventListener("resize", updateMixer);// TODO FIX + //window.addEventListener("orientationchange", updateMixer);// TODO FIX + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + joinRoom(session.roomid); + } + } else { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + getById("logoname").style.display = "none"; + } + getById("head1").className = "hidden"; + + getById("head1").className = "hidden"; + getById("head2").className = "hidden"; + + if (!session.cleanOutput) { + getById("chatbutton").className = "float"; + getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. + // getById("mediafileshare").classList.remove("hidden"); + getById("hangupbutton").className = "float"; + getById("controlButtons").classList.remove("hidden"); + // getById("legal").classList.remove("hidden"); + //getById("helpbutton").style.display = "inherit"; + //getById("reportbutton").style.display = ""; + } else { + getById("controlButtons").classList.add("hidden"); + // getById("legal").classList.add("hidden"); + } + + updatePushId(); + updateReshareLink(); + updateFileShare(); + + pokeIframeAPI("file-share", true); + pokeIframeAPI("started-fileshare"); // deprecated + + clearInterval(session.updateLocalStatsInterval); + session.updateLocalStatsInterval = setInterval(function () { + updateLocalStats(); + }, session.statsInterval); + + session.seeding = true; + session.seedStream(); + +}; + +function updateReshareLink() { + try { + var m = getById("mainmenu"); + m.remove(); + document.querySelectorAll(".hidden2").forEach(ele2 => { + ele2.classList.remove("hidden2"); + }); + } catch (e) { } + + var added = ""; + if (session.defaultPassword === false) { + if (session.password !== false) { + added = "&pw=" + session.password; + } else { + added = "&pw=false"; + } + } + + var wss = ""; + if (session.wssSetViaUrl) { + if (session.customWSS && session.customWSS !== true) { + wss = "&pie=" + session.customWSS; + } else if (session.customWSS == true) { + wss = "&wss=" + session.wss; + } else { + wss = "&wss2=" + session.wss; + } + } + if (session.audience && !session.audienceToken) { + if (document.getElementById("reshare")) { + if (!session.cleanOutput) { + getById("copythisurl").innerHTML = ""; + getById("head3a").classList.remove("hidden"); + } + document.getElementById("reshare").href = null; + document.getElementById("reshare").text = "loading public view link..."; + document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px"; + } + return; + } else if (session.audience) { + var shareLink = "https://" + location.host + location.pathname + "?view=" + session.streamID + added + wss + "&audience=" + session.audienceToken; + if (document.getElementById("reshare")) { + if (!session.cleanOutput) { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + getById("copythisurl").innerHTML = 'This is your public audience link   '; + } + document.getElementById("reshare").href = shareLink; + document.getElementById("reshare").text = shareLink; + document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px"; + } + pokeIframeAPI("share-link", shareLink); + return; + } + var shareLink = "https://" + location.host + location.pathname + "?view=" + session.streamID + added + wss; + if (document.getElementById("reshare")) { + document.getElementById("reshare").href = shareLink; + document.getElementById("reshare").text = shareLink; + document.getElementById("reshare").style.width = (document.getElementById("reshare").text.length + 1) * 1.15 * 8 + "px"; + } + if (session.whipOutput) { + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + } + pokeIframeAPI("share-link", shareLink); +} + +function cleanupMediaState(vid) { + if (session.canvasSource) { + session.canvasSource.destroy(); + session.canvasSource = null; + } + + if (vid) { + if (vid.srcObject) { + const tracks = vid.srcObject.getTracks(); + tracks.forEach(track => track.stop()); + vid.srcObject = null; + } + if (vid.currentObjectURL) { + URL.revokeObjectURL(vid.currentObjectURL); + vid.currentObjectURL = null; + } + vid.src = ''; + } + + if (session.streamSrc) { + const tracks = session.streamSrc.getTracks(); + tracks.forEach(track => track.stop()); + session.streamSrc = null; + } +} + +session.changePublishFile = function (ele, event) { + log("FILE STREAM CHANGE"); + var files = Array.from(ele.files); + var vid = getById("videosource"); + + // Clean up existing state first + cleanupMediaState(vid); + + if (files[0].type.startsWith('image/')) { + const objectURL = URL.createObjectURL(files[0]); + session.canvasSource = new CanvasStreamSource(); + session.canvasSource.imgSrc = objectURL; + window.postMessage({ type: 'canvas-frame', frame: session.canvasSource.imgSrc }, '*'); + + session.streamSrc = session.canvasSource.getStream(); + // Process the stream like we do for video + session.streamSrc = outboundAudioPipeline(session.streamSrc); + + var tracks = session.streamSrc.getVideoTracks(); + if (tracks.length) { + pushOutVideoTrack(tracks[0]); + } + senderAudioUpdate(); + + vid.play(); // We still need play() but don't set srcObjec + } else { + log("FILE VIDEO STREAM CHANGE"); + vid.playlist = files; + nextFilePlaylist(vid); + } + session.applySoloChat(); + session.applyIsolatedChat(); + toggleMute(true); +}; + +function nextFilePlaylist(vid) { + log("nextFilePlaylistD"); + var filenext = vid.playlist.shift(); + + cleanupMediaState(vid); + + if (!filenext) { + if (session.blankStream) { + session.streamSrc = session.blankStream; + pushOutVideoTrack(session.blankStream.getVideoTracks()[0]); + } + return; + } + + vid.pause(); + vid.currentObjectURL = URL.createObjectURL(filenext); + vid.src = vid.currentObjectURL; + + // Wrap the onloadeddata in error handling + vid.onloadeddata = function () { + try { + if (Firefox) { + session.streamSrc = vid.mozCaptureStream(); + } else { + session.streamSrc = vid.captureStream(); + } + updateMixer(); + var tracks = session.streamSrc.getVideoTracks(); + if (tracks.length) { + pushOutVideoTrack(tracks[0]); // video only + } + senderAudioUpdate(false, session.streamSrc); + + } catch (e) { + errorlog(e); + if (session.blankStream) { + session.streamSrc = session.blankStream; + pushOutVideoTrack(session.blankStream.getVideoTracks()[0]); + } + } + }; + + vid.onerror = (e) => { + errorlog(e); + cleanupMediaState(vid); + if (session.blankStream) { + session.streamSrc = session.blankStream; + pushOutVideoTrack(session.blankStream.getVideoTracks()[0]); + } + // Try next file if available + if (vid.playlist.length) { + setTimeout(() => nextFilePlaylist(vid), 1000); + } + }; + + vid.load(); + + vid.play() + .then(_ => { + log("playing 2"); + }) + .catch(e => { + warnlog(e); + if (session.blankStream) { + session.streamSrc = session.blankStream; + pushOutVideoTrack(session.blankStream.getVideoTracks()[0]); + } + // Try next file if available + if (vid.playlist.length) { + setTimeout(() => nextFilePlaylist(vid), 1000); + } + }); +} + +session.publishFile = function (ele, event) { + log("FILE STREAM SETUP"); + + if (!session.blankStream) { + const canvas = document.createElement('canvas'); + canvas.width = 1280; + canvas.height = 720; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + session.blankStream = canvas.captureStream(30); + session.streamSrc = session.blankStream; + } + + if (session.transcript) { + setTimeout(function () { + setupClosedCaptions(); + }, 1000); + } + + const files = Array.from(ele.files); + log(files); + const file = files[0]; + const isImage = file.type.startsWith('image/'); + + var fileURL = URL.createObjectURL(file); + var container = document.createElement("div"); + container.id = "container"; + + if (session.cover) { + container.style.setProperty("height", "100%", "important"); + } + + var v = createVideoElement(); + v.container = container; + + if (session.cleanOutput) { + container.style.height = "100%"; + v.style.maxWidth = "100%"; + v.style.boxShadow = "none"; + } + + container.appendChild(v); + + if (session.streamID) { + v.dataset.sid = session.streamID; + } + + v.autoplay = false; + if (session.showControls !== null) { + v.controls = session.showControls; + } else { + v.controls = true; + } + v.muted = false; + v.loop = files.length == 1; + + v.id = "videosource"; + v.dataset.menu = "context-menu-video"; + v.setAttribute("playsinline", ""); + + if (isImage) { + + session.canvasSource = new CanvasStreamSource(); + session.canvasSource.imgSrc = fileURL; + + window.postMessage({ type: 'canvas-frame', frame: session.canvasSource.imgSrc }, '*'); + + session.streamSrc = session.canvasSource.getStream(); + v.srcObject = session.streamSrc; + v.play(); + handleUIAndStream(); + } else { + v.src = fileURL; + } + + v.playlist = files; + v.addEventListener("ended", function (e) { + log("MY HANDLER TRIGGERED"); + var vid = getById("videosource"); + nextFilePlaylist(vid); + }, false); + + v.className = "tile clean fileshare"; + + session.videoElement = v; + session.mirrorExclude = true; + + v.addEventListener("click", (e) => { + log("click"); + try { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + + var [menu, innerMenu] = statsMenuCreator(); + + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + + printMyStats(innerMenu); + e.stopPropagation(); + return false; + } + } catch (e) { + errorlog(e); + } + }); + + v.touchTimeOut = null; + v.touchLastTap = 0; + v.touchCount = 0; + + v.addEventListener("touchend", (event) => { + if (session.disableMouseEvents) { + return; + } + log("touched"); + + document.onmousemove = null; + document.ontouchmove = null; + + var currentTime = new Date().getTime(); + var tapLength = currentTime - v.touchLastTap; + clearTimeout(v.touchTimeOut); + if (tapLength < 500 && tapLength > 0) { + log("double touched"); + v.touchCount += 1; + event.preventDefault(); + if (v.touchCount < 5) { + v.touchLastTap = currentTime; + return false; + } + v.touchLastTap = 0; + v.touchCount = 0; + + var [menu, innerMenu] = statsMenuCreator(); + + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + + printMyStats(innerMenu); + event.stopPropagation(); + return false; + } else { + v.touchCount = 1; + v.touchTimeOut = setTimeout( + function (vv) { + clearTimeout(vv.touchTimeOut); + vv.touchLastTap = 0; + vv.touchCount = 0; + }, + 5000, + v + ); + v.touchLastTap = currentTime; + } + }); + + v.onerror = () => { + if (session.cleanOutput) { + errorlog("File failed to load.\n\nSelect a compatible media file."); + } else { + warnUser("File failed to load.\n\nSelect a compatible media file."); + } + }; + + v.onloadeddata = async () => { + session.mediafileShare = true; + getById("mainmenu").remove(); + + if (Firefox) { + session.streamSrc = v.mozCaptureStream(); + } else { + session.streamSrc = v.captureStream(); + } + + if (session.framegrab && session.framegrabAudioRequested && session.pendingFramegrabAudioSettings) { + try { + const maybePromise = session.startFramegrabAudio(session.pendingFramegrabAudioSettings); + if (maybePromise && typeof maybePromise.then === "function") { + maybePromise.catch(errorlog); + } + } catch (err) { + errorlog(err); + } + } + + handleUIAndStream(); + }; + + getById("gridlayout").appendChild(container); + + function handleUIAndStream() { + if (session.roomid !== false) { + if (session.roomid === "" && (!session.view || session.view === "")) { + } else { + log("ROOMID ENABLED"); + log("Update Mixer Event on REsize SET"); + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + joinRoom(session.roomid); + } + } else { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + getById("logoname").style.display = "none"; + } + + updatePushId(); + + getById("head1").className = "hidden"; + getById("head2").className = "hidden"; + + if (!session.cleanOutput) { + getById("chatbutton").className = "float"; + getById("mediafileshare").classList.remove("hidden"); + getById("hangupbutton").className = "float"; + getById("controlButtons").classList.remove("hidden"); + // getById("legal").classList.remove("hidden"); + } else { + getById("controlButtons").classList.add("hidden"); + // getById("legal").classList.add("hidden"); + } + + toggleMute(true); + + if (session.director) { + } else if (session.scene !== false) { + } else if (session.roomid !== false) { + if (session.roomid === "") { + if (!session.view || session.view === "") { + if (session.fullscreen) { + session.windowed = session.windowed === null ? false : session.windowed; + } else if (session.minipreview) { + session.windowed = session.windowed === null ? false : session.windowed; + } else { + session.windowed = session.windowed === null ? true : session.windowed; + } + + if (session.windowed) { + session.videoElement.className = "myVideo clean fileshare"; + container.classList.add("vidcon"); + } + getById("mutespeakerbutton").classList.add("hidden"); + container.style.width = "100%"; + container.style.alignItems = "center"; + container.backgroundColor = "#666"; + play(); + } else { + session.windowed = session.windowed === null ? false : session.windowed; + play(); + } + } else { + if (session.stereo == 5) { + session.stereo = 3; + } + session.windowed = session.windowed === null ? false : session.windowed; + } + } else { + if (session.fullscreen) { + session.windowed = session.windowed === null ? false : session.windowed; + } else if (session.minipreview) { + session.windowed = session.windowed === null ? false : session.windowed; + } else { + session.windowed = session.windowed === null ? true : session.windowed; + } + if (session.windowed) { + session.videoElement.className = "myVideo clean fileshare"; + container.classList.add("vidcon"); + } + getById("mutespeakerbutton").classList.add("hidden"); + container.style.width = "100%"; + container.style.alignItems = "center"; + container.backgroundColor = "#666"; + } + applyMirror(session.mirrorExclude); + + updateReshareLink(); + pokeIframeAPI("started-fileshare"); + pokeIframeAPI("file-share", true); + + clearInterval(session.updateLocalStatsInterval); + session.updateLocalStatsInterval = setInterval(function () { + updateLocalStats(); + }, session.statsInterval); + if (session.meshcast2) { meshcast2(); } else if (session.whipOutput) { @@ -38426,385 +38883,385 @@ session.publishFile = function (ele, event) { whepOut(); } - - session.seeding = true; - - if (session.videoMutedFlag) { - session.videoMuted = true; - toggleVideoMute(true); - } - - session.seedStream(); - } -}; // publishFile - -class CanvasStreamSource { - constructor() { - this.canvas = document.createElement('canvas'); - this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); - this.stream = this.canvas.captureStream(30); - - this.initialized = false; - this.boundHandleFrame = this.handleFrame.bind(this); - this.lastFrameTime = Date.now(); - this.indicatorRotation = 0; - - this.frameCheckInterval = setInterval(() => { - this.drawKeyframeTrigger(); - }, 100); - window.addEventListener('message', this.boundHandleFrame); - } - - drawKeyframeTrigger() { - if (!this.canvas || !this.ctx || Date.now() - this.lastFrameTime < 500) return; - - const state = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); - - this.ctx.save(); - this.ctx.translate(this.canvas.width - 15, this.canvas.height - 15); - this.ctx.rotate(this.indicatorRotation); - - const opacity = 0.15 + Math.sin(this.indicatorRotation) * 0.05; - this.ctx.strokeStyle = `rgba(200, 200, 200, ${opacity})`; - this.ctx.lineWidth = 1; - - const indicator = new Path2D(); - indicator.arc(0, 0, 3, 0, Math.PI * 2); - indicator.moveTo(5, 0); - indicator.arc(0, 0, 5, 0, Math.PI * 2); - this.ctx.stroke(indicator); - - this.ctx.restore(); - this.ctx.putImageData(state, 0, 0); - - this.indicatorRotation += 0.01; - } - - initializeForFrame(frame) { - if (this.initialized) return; - - try { - if (typeof frame !== "string") { - const videoTrack = new MediaStreamTrackGenerator({ kind: 'video' }); - this.writer = videoTrack.writable.getWriter(); - const newStream = new MediaStream([videoTrack]); - - this.stream.getVideoTracks().forEach(track => { - this.stream.removeTrack(track); - track.stop(); - }); - newStream.getVideoTracks().forEach(track => { - this.stream.addTrack(track); - }); - } - this.initialized = true; - } catch (e) { - console.error("Failed to initialize MediaStreamTrackGenerator, keeping canvas:", e); - this.initialized = true; - } - } - - handleFrame(event) { - if (!event.data || !event.data.frame || (event.data.type != "canvas-frame")) return; - - const frame = event.data.frame; - this.initializeForFrame(frame); - if (this.writer && typeof frame !== "string") { - this.writer.write(frame).catch(e => { - console.error("Error writing frame:", e); - if (frame.close) frame.close(); - }); - } else if (this.canvas) { - const img = new Image(); - img.onload = () => { - this.canvas.width = img.width; - this.canvas.height = img.height; - this.ctx.drawImage(img, 0, 0); - this.lastFrameTime = Date.now(); - img.remove(); - }; - img.src = frame; - } - } - - getStream() { - return this.stream; - } - - destroy() { - if (this.frameCheckInterval) { - clearInterval(this.frameCheckInterval); - } - if (this.writer) { - this.writer.close(); - } - if (this.stream) { - this.stream.getTracks().forEach(track => { - track.stop(); - this.stream.removeTrack(track); - }); - } - if (this.imgSrc) { - URL.revokeObjectURL(this.imgSrc); - } - window.removeEventListener('message', this.boundHandleFrame); - this.canvas = null; - this.ctx = null; - this.stream = null; - this.writer = null; - this.initialized = false; - } -} - -session.publishFrameSource = function (ele, event) { - - var container = document.createElement("div"); - container.id = "container"; - - if (session.cover) { - container.style.setProperty("height", "100%", "important"); - } - - var v = createVideoElement(); - v.container = container; - - if (session.cleanOutput) { - container.style.height = "100%"; - v.style.maxWidth = "100%"; - v.style.boxShadow = "none"; - } - - container.appendChild(v); - - if (session.streamID) { - v.dataset.sid = session.streamID; - } - - getById("gridlayout").appendChild(container); - - if (session.roomid !== false) { - if (session.roomid === "" && (!session.view || session.view === "")) { - } else { - log("ROOMID EANBLED"); - log("Update Mixer Event on REsize SET"); - //window.addEventListener("resize", updateMixer);// TODO FIX - //window.addEventListener("orientationchange", updateMixer);// TODO FIX - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - joinRoom(session.roomid); - } - } else { - getById("head3").classList.remove("hidden"); - getById("head3a").classList.remove("hidden"); - getById("logoname").style.display = "none"; - } - - updatePushId(); - - getById("head1").className = "hidden"; - getById("head2").className = "hidden"; - - if (!session.cleanOutput) { - getById("chatbutton").className = "float"; - getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. - getById("hangupbutton").className = "float"; - getById("controlButtons").classList.remove("hidden"); - // getById("legal").classList.remove("hidden"); - //getById("helpbutton").style.display = "inherit"; - //getById("reportbutton").style.display = ""; - } else { - getById("controlButtons").classList.add("hidden"); - // getById("legal").classList.add("hidden"); - } - - var bigPlayButton = document.getElementById("bigPlayButton"); - if (bigPlayButton) { - bigPlayButton.parentNode.removeChild(bigPlayButton); - } - - v.autoplay = false; - if (session.showControls !== null) { - v.controls = session.showControls; - } else { - v.controls = true; - } - v.muted = false; - - v.id = "videosource"; // could be set to UUID in the future - v.dataset.menu = "context-menu-video"; - v.setAttribute("playsinline", ""); - - session.canvasSource = new CanvasStreamSource(); - session.streamSrc = session.canvasSource.getStream(); - v.srcObject = session.streamSrc; - - v.className = "tile clean"; - - session.videoElement = v; - session.mirrorExclude = true; - - v.addEventListener("click", (e) => { - log("click"); - try { - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - - var [menu, innerMenu] = statsMenuCreator(); - - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - - printMyStats(innerMenu); - e.stopPropagation(); - return false; - } - } catch (e) { - errorlog(e); - } - }); - - v.touchTimeOut = null; - v.touchLastTap = 0; - v.touchCount = 0; - - v.addEventListener("touchend", (event) => { - if (session.disableMouseEvents) { - return; - } - log("touched"); - - //document.ontouchup = null; - //document.onmouseup = null; - document.onmousemove = null; - document.ontouchmove = null; - - var currentTime = new Date().getTime(); - var tapLength = currentTime - v.touchLastTap; - clearTimeout(v.touchTimeOut); - if (tapLength < 500 && tapLength > 0) { - /// - log("double touched"); - v.touchCount += 1; - event.preventDefault(); - if (v.touchCount < 5) { - v.touchLastTap = currentTime; - return false; - } - v.touchLastTap = 0; - v.touchCount = 0; - - var [menu, innerMenu] = statsMenuCreator(); - - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - - printMyStats(innerMenu); - event.stopPropagation(); - return false; - ////// - } else { - v.touchCount = 1; - v.touchTimeOut = setTimeout( - function (vv) { - clearTimeout(vv.touchTimeOut); - vv.touchLastTap = 0; - vv.touchCount = 0; - }, - 5000, - v - ); - v.touchLastTap = currentTime; - } - }); - - v.onerror = (e) => { - errorlog(e); - }; - - v.onloadeddata = async () => { - - session.mediafileShare = true; - getById("mainmenu").remove(); - - if (Firefox) { - session.streamSrc = v.mozCaptureStream(); - } else { - session.streamSrc = v.captureStream(); // gaaaaaaaaaaaahhhhhhhh! - } - - if (session.framegrab && session.framegrabAudioRequested && session.pendingFramegrabAudioSettings) { - try { - const maybePromise = session.startFramegrabAudio(session.pendingFramegrabAudioSettings); - if (maybePromise && typeof maybePromise.then === "function") { - maybePromise.catch(errorlog); - } - } catch (err) { - errorlog(err); - } - } - - toggleMute(true); - - if (session.director) { - } else if (session.scene !== false) { - } else if (session.roomid !== false) { - if (session.roomid === "") { - if (!session.view || session.view === "") { - if (session.fullscreen) { - session.windowed = session.windowed === null ? false : session.windowed; - } else if (session.minipreview) { - session.windowed = session.windowed === null ? false : session.windowed; - } else { - session.windowed = session.windowed === null ? true : session.windowed; - } - - if (session.windowed) { - v.className = "myVideo clean"; - container.classList.add("vidcon"); - } - getById("mutespeakerbutton").classList.add("hidden"); - container.style.width = "100%"; - container.style.alignItems = "center"; - container.backgroundColor = "#666"; - play(); - } else { - session.windowed = session.windowed === null ? false : session.windowed; - play(); - } - } else { - //session.cbr=0; // we're just going to override it - if (session.stereo == 5) { - session.stereo = 3; - } - session.windowed = session.windowed === null ? false : session.windowed; - } - applyMirror(session.mirrorExclude); - } else { - if (session.fullscreen) { - session.windowed = session.windowed === null ? false : session.windowed; - } else if (session.minipreview) { - session.windowed = session.windowed === null ? false : session.windowed; - } else { - session.windowed = session.windowed === null ? true : session.windowed; - } - if (session.windowed) { - v.className = "myVideo clean "; - container.classList.add("vidcon"); - } - getById("mutespeakerbutton").classList.add("hidden"); - container.style.width = "100%"; - container.style.alignItems = "center"; - container.backgroundColor = "#666"; - applyMirror(session.mirrorExclude); - } - - updateReshareLink(); - pokeIframeAPI("started-frameshare"); // depreciated - pokeIframeAPI("frame-share", true); - - clearInterval(session.updateLocalStatsInterval); - session.updateLocalStatsInterval = setInterval(function () { - updateLocalStats(); - }, session.statsInterval); - + + session.seeding = true; + + if (session.videoMutedFlag) { + session.videoMuted = true; + toggleVideoMute(true); + } + + session.seedStream(); + } +}; // publishFile + +class CanvasStreamSource { + constructor() { + this.canvas = document.createElement('canvas'); + this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + this.stream = this.canvas.captureStream(30); + + this.initialized = false; + this.boundHandleFrame = this.handleFrame.bind(this); + this.lastFrameTime = Date.now(); + this.indicatorRotation = 0; + + this.frameCheckInterval = setInterval(() => { + this.drawKeyframeTrigger(); + }, 100); + window.addEventListener('message', this.boundHandleFrame); + } + + drawKeyframeTrigger() { + if (!this.canvas || !this.ctx || Date.now() - this.lastFrameTime < 500) return; + + const state = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.save(); + this.ctx.translate(this.canvas.width - 15, this.canvas.height - 15); + this.ctx.rotate(this.indicatorRotation); + + const opacity = 0.15 + Math.sin(this.indicatorRotation) * 0.05; + this.ctx.strokeStyle = `rgba(200, 200, 200, ${opacity})`; + this.ctx.lineWidth = 1; + + const indicator = new Path2D(); + indicator.arc(0, 0, 3, 0, Math.PI * 2); + indicator.moveTo(5, 0); + indicator.arc(0, 0, 5, 0, Math.PI * 2); + this.ctx.stroke(indicator); + + this.ctx.restore(); + this.ctx.putImageData(state, 0, 0); + + this.indicatorRotation += 0.01; + } + + initializeForFrame(frame) { + if (this.initialized) return; + + try { + if (typeof frame !== "string") { + const videoTrack = new MediaStreamTrackGenerator({ kind: 'video' }); + this.writer = videoTrack.writable.getWriter(); + const newStream = new MediaStream([videoTrack]); + + this.stream.getVideoTracks().forEach(track => { + this.stream.removeTrack(track); + track.stop(); + }); + newStream.getVideoTracks().forEach(track => { + this.stream.addTrack(track); + }); + } + this.initialized = true; + } catch (e) { + console.error("Failed to initialize MediaStreamTrackGenerator, keeping canvas:", e); + this.initialized = true; + } + } + + handleFrame(event) { + if (!event.data || !event.data.frame || (event.data.type != "canvas-frame")) return; + + const frame = event.data.frame; + this.initializeForFrame(frame); + if (this.writer && typeof frame !== "string") { + this.writer.write(frame).catch(e => { + console.error("Error writing frame:", e); + if (frame.close) frame.close(); + }); + } else if (this.canvas) { + const img = new Image(); + img.onload = () => { + this.canvas.width = img.width; + this.canvas.height = img.height; + this.ctx.drawImage(img, 0, 0); + this.lastFrameTime = Date.now(); + img.remove(); + }; + img.src = frame; + } + } + + getStream() { + return this.stream; + } + + destroy() { + if (this.frameCheckInterval) { + clearInterval(this.frameCheckInterval); + } + if (this.writer) { + this.writer.close(); + } + if (this.stream) { + this.stream.getTracks().forEach(track => { + track.stop(); + this.stream.removeTrack(track); + }); + } + if (this.imgSrc) { + URL.revokeObjectURL(this.imgSrc); + } + window.removeEventListener('message', this.boundHandleFrame); + this.canvas = null; + this.ctx = null; + this.stream = null; + this.writer = null; + this.initialized = false; + } +} + +session.publishFrameSource = function (ele, event) { + + var container = document.createElement("div"); + container.id = "container"; + + if (session.cover) { + container.style.setProperty("height", "100%", "important"); + } + + var v = createVideoElement(); + v.container = container; + + if (session.cleanOutput) { + container.style.height = "100%"; + v.style.maxWidth = "100%"; + v.style.boxShadow = "none"; + } + + container.appendChild(v); + + if (session.streamID) { + v.dataset.sid = session.streamID; + } + + getById("gridlayout").appendChild(container); + + if (session.roomid !== false) { + if (session.roomid === "" && (!session.view || session.view === "")) { + } else { + log("ROOMID EANBLED"); + log("Update Mixer Event on REsize SET"); + //window.addEventListener("resize", updateMixer);// TODO FIX + //window.addEventListener("orientationchange", updateMixer);// TODO FIX + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + joinRoom(session.roomid); + } + } else { + getById("head3").classList.remove("hidden"); + getById("head3a").classList.remove("hidden"); + getById("logoname").style.display = "none"; + } + + updatePushId(); + + getById("head1").className = "hidden"; + getById("head2").className = "hidden"; + + if (!session.cleanOutput) { + getById("chatbutton").className = "float"; + getById("sharefilebutton").classList.remove("hidden"); // we won't override "display:none", if set, though. + getById("hangupbutton").className = "float"; + getById("controlButtons").classList.remove("hidden"); + // getById("legal").classList.remove("hidden"); + //getById("helpbutton").style.display = "inherit"; + //getById("reportbutton").style.display = ""; + } else { + getById("controlButtons").classList.add("hidden"); + // getById("legal").classList.add("hidden"); + } + + var bigPlayButton = document.getElementById("bigPlayButton"); + if (bigPlayButton) { + bigPlayButton.parentNode.removeChild(bigPlayButton); + } + + v.autoplay = false; + if (session.showControls !== null) { + v.controls = session.showControls; + } else { + v.controls = true; + } + v.muted = false; + + v.id = "videosource"; // could be set to UUID in the future + v.dataset.menu = "context-menu-video"; + v.setAttribute("playsinline", ""); + + session.canvasSource = new CanvasStreamSource(); + session.streamSrc = session.canvasSource.getStream(); + v.srcObject = session.streamSrc; + + v.className = "tile clean"; + + session.videoElement = v; + session.mirrorExclude = true; + + v.addEventListener("click", (e) => { + log("click"); + try { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + + var [menu, innerMenu] = statsMenuCreator(); + + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + + printMyStats(innerMenu); + e.stopPropagation(); + return false; + } + } catch (e) { + errorlog(e); + } + }); + + v.touchTimeOut = null; + v.touchLastTap = 0; + v.touchCount = 0; + + v.addEventListener("touchend", (event) => { + if (session.disableMouseEvents) { + return; + } + log("touched"); + + //document.ontouchup = null; + //document.onmouseup = null; + document.onmousemove = null; + document.ontouchmove = null; + + var currentTime = new Date().getTime(); + var tapLength = currentTime - v.touchLastTap; + clearTimeout(v.touchTimeOut); + if (tapLength < 500 && tapLength > 0) { + /// + log("double touched"); + v.touchCount += 1; + event.preventDefault(); + if (v.touchCount < 5) { + v.touchLastTap = currentTime; + return false; + } + v.touchLastTap = 0; + v.touchCount = 0; + + var [menu, innerMenu] = statsMenuCreator(); + + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + + printMyStats(innerMenu); + event.stopPropagation(); + return false; + ////// + } else { + v.touchCount = 1; + v.touchTimeOut = setTimeout( + function (vv) { + clearTimeout(vv.touchTimeOut); + vv.touchLastTap = 0; + vv.touchCount = 0; + }, + 5000, + v + ); + v.touchLastTap = currentTime; + } + }); + + v.onerror = (e) => { + errorlog(e); + }; + + v.onloadeddata = async () => { + + session.mediafileShare = true; + getById("mainmenu").remove(); + + if (Firefox) { + session.streamSrc = v.mozCaptureStream(); + } else { + session.streamSrc = v.captureStream(); // gaaaaaaaaaaaahhhhhhhh! + } + + if (session.framegrab && session.framegrabAudioRequested && session.pendingFramegrabAudioSettings) { + try { + const maybePromise = session.startFramegrabAudio(session.pendingFramegrabAudioSettings); + if (maybePromise && typeof maybePromise.then === "function") { + maybePromise.catch(errorlog); + } + } catch (err) { + errorlog(err); + } + } + + toggleMute(true); + + if (session.director) { + } else if (session.scene !== false) { + } else if (session.roomid !== false) { + if (session.roomid === "") { + if (!session.view || session.view === "") { + if (session.fullscreen) { + session.windowed = session.windowed === null ? false : session.windowed; + } else if (session.minipreview) { + session.windowed = session.windowed === null ? false : session.windowed; + } else { + session.windowed = session.windowed === null ? true : session.windowed; + } + + if (session.windowed) { + v.className = "myVideo clean"; + container.classList.add("vidcon"); + } + getById("mutespeakerbutton").classList.add("hidden"); + container.style.width = "100%"; + container.style.alignItems = "center"; + container.backgroundColor = "#666"; + play(); + } else { + session.windowed = session.windowed === null ? false : session.windowed; + play(); + } + } else { + //session.cbr=0; // we're just going to override it + if (session.stereo == 5) { + session.stereo = 3; + } + session.windowed = session.windowed === null ? false : session.windowed; + } + applyMirror(session.mirrorExclude); + } else { + if (session.fullscreen) { + session.windowed = session.windowed === null ? false : session.windowed; + } else if (session.minipreview) { + session.windowed = session.windowed === null ? false : session.windowed; + } else { + session.windowed = session.windowed === null ? true : session.windowed; + } + if (session.windowed) { + v.className = "myVideo clean "; + container.classList.add("vidcon"); + } + getById("mutespeakerbutton").classList.add("hidden"); + container.style.width = "100%"; + container.style.alignItems = "center"; + container.backgroundColor = "#666"; + applyMirror(session.mirrorExclude); + } + + updateReshareLink(); + pokeIframeAPI("started-frameshare"); // depreciated + pokeIframeAPI("frame-share", true); + + clearInterval(session.updateLocalStatsInterval); + session.updateLocalStatsInterval = setInterval(function () { + updateLocalStats(); + }, session.statsInterval); + if (session.meshcast2) { await meshcast2(); } else if (session.whipOutput) { // was handling these functions within session.seedStream(); doing it here now instead. 8-08-2024 @@ -38815,10259 +39272,10267 @@ session.publishFrameSource = function (ele, event) { whepOut(); } - - session.seeding = true; - - if (session.videoMutedFlag) { - session.videoMuted = true; - toggleVideoMute(true); - } - - session.seedStream(); - }; -}; // publishFrameSource - -function tryAgain(event) { - // audio or video agnostic track reconnect ------------not actually in use,. maybe out of date - log("TRY AGAIN TRIGGERED"); - warnlog(event); -} - -function enterPressedClick(event, ele) { - if (event.keyCode === 13) { - event.preventDefault(); - ele.click(); - } -} - -function enterPressed(event, callback) { - // Number 13 is the "Enter" key on the keyboard - if (event.keyCode === 13) { - event.preventDefault(); - callback(); - } -} - -function dragElement(elmnt) { - if (session.disableMouseEvents) { - return; - } - log("dragElement started"); - - function onvideoclick() { - log("onvideoclick"); - log(pos3 + " " + pos4); - //log(pos3o + " " + pos4o); - tapToFocus(parseInt((pos3 * 100) / elmnt.clientWidth), parseInt((pos4 / elmnt.clientHeight) * 100)); - return false; - } - - function elementDrag(e) { - e = e || window.event; - e.preventDefault(); - // calculate the new cursor position: - log("dragging"); - log(e); - if (Date.now() - millis < 100) { - return; - } - - dragged = true; - millis = Date.now(); - - if (e.type == "touchstart" || e.type == "touchmove" || e.type == "touchend" || e.type == "touchcancel") { - var touch = e.touches[0] || e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; - pos1 = touch.clientX; - pos2 = touch.clientY; - } else if (e.type == "mousedown" || e.type == "mouseup" || e.type == "mousemove" || e.type == "mouseover" || e.type == "mouseout" || e.type == "mouseenter" || e.type == "mouseleave") { - pos1 = e.clientX; - pos2 = e.clientY; - } - - if (!zoomable) { - return; - } - - var zoom = parseFloat(((pos4 - pos2) * 2) / elmnt.offsetHeight); - - if (zoom > 1) { - zoom = 1.0; - } else if (zoom < -1) { - zoom = -1.0; - } - input.value = zoom * (input.max - input.min) + input.min; - updateCameraConstraints("zoom", input.value, false, false); - } - function closeDragElement(e) { - log("closeDragElement"); - log(e); - - // focusable - if (!dragged) { - log("dragged: " + dragged); - onvideoclick(); - } - dragged = false; - - elmnt.removeEventListener("touchend", closeDragElement); - elmnt.removeEventListener("mouseup", closeDragElement); - - /* stop moving when mouse button is released:*/ - //document.ontouchend = null; - //document.onmouseup = null; - document.onmousemove = null; - document.ontouchmove = null; - } - function dragMouseDown(e) { - log("dragMouseDown"); - log(e); - - dragged = false; - millis = Date.now(); - - e = e || window.event; - e.preventDefault(); - - pos0 = input.value; - if (e.type == "touchstart" || e.type == "touchmove" || e.type == "touchend" || e.type == "touchcancel") { - var touch = e.touches[0] || e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; - pos3 = touch.clientX; - pos4 = touch.clientY; - //pos3o = touch.offsetX; - //pos4o = touch.offsetX; - } else if (e.type == "mousedown" || e.type == "mouseup" || e.type == "mousemove" || e.type == "mouseover" || e.type == "mouseout" || e.type == "mouseenter" || e.type == "mouseleave") { - pos3 = e.clientX; - pos4 = e.clientY; - //pos3o = e.offsetX; - //pos4o = e.offsetX; - } - elmnt.addEventListener("touchend", closeDragElement); - elmnt.addEventListener("mouseup", closeDragElement); - document.ontouchmove = elementDrag; - document.onmousemove = elementDrag; - } - - try { - var stream = elmnt.srcObject; - try { - var track0 = stream.getVideoTracks(); - } catch (e) { - return; - } - - if (!track0.length) { - return; - } - var focusable = false; - var zoomable = false; - var dragged = false; - var input = getById("zoomSlider"); - track0 = track0[0]; - if (track0.getCapabilities) { - var capabilities = track0.getCapabilities(); - var settings = track0.getSettings(); - - if ("focusDistance" in capabilities) { - log("focusable"); - focusable = true; - } - - if ("zoom" in capabilities) { - if (capabilities.zoom.min !== capabilities.zoom.max) { - log("zoomable;"); - zoomable = true; - input.min = capabilities.zoom.min; - input.max = capabilities.zoom.max; - input.step = capabilities.zoom.step; - input.value = settings.zoom; - } - } - } - - var millis = Date.now(); - var pos0 = 1; - var pos3 = 0; - var pos4 = 0; - var pos1 = 0; - var pos2 = 0; - //var pos3o = 0; - //var pos4o = 0; - } catch (e) { - errorlog(e); - return; - } - - if (!focusable && !zoomable) { - return; - } // can't be zoomed or focused. - - log("drag on"); - elmnt.onmousedown = dragMouseDown; - elmnt.ontouchstart = dragMouseDown; -} - -function previewIframe(iframeSrc) { - // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: loadIframe();"> , but don't call it before the page loads. - if (!session.iFramesAllowed) { - warnUser("Can't create iFRAME - security is tainted due to possible CSS injection"); - errorlog("Can't create iFRAME - security is tainted due to possible CSS injection"); - return; - } - var iframe = document.createElement("iframe"); - iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location - iframe.style.width = "100%"; - iframe.style.height = "100%"; - iframe.style.border = "10px dashed rgb(64 65 62)"; - iframe.classList.add("insecure"); - iframe.setAttribute("allowtransparency", "true"); - iframe.setAttribute("crossorigin", "anonymous"); - iframe.setAttribute("credentialless", "true"); - - iframeSrc = parseURL4Iframe(iframeSrc); - - /* if (typeof iframeSrc == "object"){ // special handler. - iframeSrc = iframeSrc.parsedSrc; - } */ - - iframe.src = iframeSrc; - getById("previewIframe").innerHTML = ""; - getById("previewIframe").style.width = "640px"; - getById("previewIframe").style.height = "360px"; - getById("previewIframe").style.margin = "auto"; - getById("previewIframe").appendChild(iframe); -} - -function loadIframe(iframesrc, target) { - // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: loadIframe();"> , but don't call it before the page loads. - /* if (document.getElementById("mainmenu")) { - var m = getById("mainmenu"); - m.remove(); - } */ - - if (!session.iFramesAllowed) { - return false; - } - - if (!target) { - return false; - } - - if (typeof target == "string") { - let UUID = target; - var iframe = document.createElement("iframe"); - iframe.style.width = "100%"; - iframe.style.height = "100%"; - iframe.id = "iframe_" + UUID; - iframe.dataset.UUID = UUID; - iframe.loadedYoutubeListen = false; - - if (session.director) { - // - } else if (session.scene !== false) { - if (session.view) { - // specific video to be played - iframe.style.display = "block"; - } else if (session.scene === "0") { - iframe.style.display = "block"; - } else { - // group scene I guess; needs to be added manually - iframe.style.display = "none"; - } - } else if (session.roomid !== false) { - // - } else { - iframe.style.display = "block"; - } - } else { - var iframe = target; - } - - iframe.classList.add("insecure"); - iframe.setAttribute("allowtransparency", "true"); - iframe.setAttribute("crossorigin", "anonymous"); - iframe.setAttribute("credentialless", "true"); - iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location - - if (iframesrc == "") { - iframesrc = "./"; - iframe.classList.remove("insecure"); - } - - // trusted domains - var ipsafe = false; - - if (iframesrc.startsWith("https://www.youtube.com/") || iframesrc.startsWith("https://youtube.com/")) { - iframe.classList.remove("insecure"); - setTimeout( - function (iframe_id) { - YoutubeListen(iframe_id); - }, - 1000, - iframe.id - ); // create stats feedback for the director; syncing. - if (session.noaudio) { - if (iframesrc.includes("?")) { - iframesrc += "&mute=1"; - } else { - iframesrc += "?mute=1"; - } - } - ipsafe = true; - } else if (iframesrc.includes("vdo.ninja/")) { - iframe.classList.remove("insecure"); - ipsafe = false; - if (isIFrame) { - console.warn("You're not allow to put this domain inside an iframe of an iframe."); - return false; - } - } else if (iframesrc.includes("obs.ninja/")) { - iframe.classList.remove("insecure"); - ipsafe = false; - if (isIFrame) { - console.warn("You're not allow to put this domain inside an iframe of an iframe."); - return false; - } - } else if (iframesrc.includes("versus.cam/")) { - iframe.classList.remove("insecure"); - ipsafe = false; - if (isIFrame) { - console.warn("You're not allow to put this domain inside an iframe of an iframe."); - return false; - } - } else if (iframesrc.includes("invite.cam/")) { - ipsafe = false; - if (isIFrame) { - console.warn("You're not allow to put this domain inside an iframe of an iframe."); - return false; - } - } else if (iframesrc.startsWith("https://player.twitch.tv/")) { - iframe.classList.remove("insecure"); - ipsafe = true; - } else if (iframesrc.startsWith("https://x.com/")) { - iframe.classList.remove("insecure"); - ipsafe = false; - } else if (iframesrc.startsWith("https://twitch.tv/")) { - iframe.classList.remove("insecure"); - ipsafe = true; - } else if (iframesrc.startsWith("https://caption.ninja/")) { - iframe.classList.remove("insecure"); - ipsafe = true; - } else if (iframesrc.startsWith("https://www.twitch.tv/")) { - iframe.classList.remove("insecure"); - ipsafe = true; - } else if (iframesrc.startsWith("https://vimeo.com/")) { - iframe.classList.remove("insecure"); - ipsafe = true; - } else if (iframesrc.startsWith("https://player.vimeo.com/")) { - iframe.classList.remove("insecure"); - ipsafe = true; - } else if (iframesrc.startsWith("https://meshcast.io/")) { - iframe.classList.remove("insecure"); - try { - if (document.domain.endsWith(".vdo.ninja")) { - document.domain = "vdo.ninja"; - } - } catch (e) { - errorlog(e); - } - ipsafe = true; - } else if (iframesrc.startsWith("https://app.stageten.tv/")) { - iframe.classList.remove("insecure"); - ipsafe = true; - } else if (iframesrc.startsWith("https://socialstream.ninja/")) { - iframe.classList.remove("insecure"); - ipsafe = false; - } else if (session.cleanOutput && window.obsstudio) { - iframe.classList.remove("insecure"); - } - - if (!ipsafe) { - iframe.title = "⚠️ This section is an iframe that may be of untrusted origin. Use caution."; - } - - if (!ipsafe && (urlParams.has("privacy") || urlParams.has("private"))) { - if (session.cleanOutput || window.obsstudio) { - iframesrc = "./confirm.html?clean&url=" + encodeURI(iframesrc); - } else { - iframesrc = "./confirm.html?url=" + encodeURI(iframesrc); - } - } else if (!ipsafe && (typeof target == "object")) { // likely widget - if (session.widgetwidth <= 28) { // 28 for marcus - iframe.classList.remove("insecure"); - } else if (isIFrame && (session.widgetwidth <= 50)) { - iframe.classList.remove("insecure"); - } else { - if (session.cleanOutput || window.obsstudio) { - iframesrc = "./confirm.html?clean&url=" + encodeURI(iframesrc); - } else { - iframesrc = "./confirm.html?url=" + encodeURI(iframesrc); - } - } - } - - if (isIFrame && ["invite.cam", "invitecamera.com", "vdo.ninja", "versus.cam", "dev.versus.cam", "backup.vdo.ninja", "proxy.vdo.ninia", "proxy.obs.ninja", "insecure.vdo.ninja", "insecure.obs.ninja", "rtc.ninja"].includes(getParentHostname())) { - iframe.classList.add("insecure"); - } - - iframe.src = iframesrc; - - pokeIframeAPI("iframe-loaded", iframesrc); - return iframe; -} - -function dropDownButtonAction(ele) { - var ele = getById("dropButton"); - if (ele) { - ele.parentNode.removeChild(ele); - //getById('container-5').classList.remove('hidden'); - //getById('container-8').classList.remove('hidden'); - //getById('container-6').classList.remove('hidden'); - document.querySelectorAll("div.column.card").forEach(child => { - child.classList.remove("hidden"); - }); - } -} - -function updateConstraintSliders() { - log("updateConstraintSliders"); - if (session.roomid !== false && session.roomid !== "" && session.director !== true && session.forceMediaSettings == false) { - if (session.controlRoomBitrate !== false) { - listCameraSettings(); - } - if (session.effect !== false) { - //if ((iOS) || (iPad)){ - //} else { - getById("effectsDiv3").style.display = "block"; - getById("effectSelector3").value = session.effect || "0"; - //} - } - } else { - listAudioSettings(); - listCameraSettings(); - - //if ((iOS) || (iPad)){ - // } else { - if (session.effect !== false) { - getById("effectsDiv3").style.display = "block"; - try { - getById("effectSelector3").value = session.effect || "0"; - } catch (E) { } - } - //} - } - //checkIfPIP(); // this doesn't actually work on iOS still, so whatever. -} - -function checkIfPIP() { - try { - if (session.videoElement && ((session.videoElement.webkitSupportsPresentationMode && typeof session.videoElement.webkitSetPresentationMode === "function") || document.pictureInPictureEnabled || !videoElement.disablePictureInPicture)) { - // Toggle PiP when the user clicks the button. - - getById("pIpStartButton").addEventListener("click", function (event) { - // if ( (document.pictureInPictureEnabled || !videoElement.disablePictureInPicture)){ - //session.videoElement.requestPictureInPicture(); - // } else { - session.videoElement.webkitSetPresentationMode(session.videoElement.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture"); - // } - }); - getById("pIpStartButton").style.display = "inline-block"; - } - } catch (e) { - errorlog(e); - } -} - -function togglePictureInPicture(videoElement) { - if (document.pictureInPictureElement) { - if (document.pictureInPictureElement.id == videoElement.id) { - document.exitPictureInPicture(); - pokeIframeAPI("picture-in-picture", false); - return false; - } else { - document.exitPictureInPicture(); - pokeIframeAPI("picture-in-picture", false); - videoElement.requestPictureInPicture(); - pokeIframeAPI("picture-in-picture", true); - } - } else if (document.pictureInPictureEnabled) { - videoElement.requestPictureInPicture(); - pokeIframeAPI("picture-in-picture", true); - } - return true; -} - -function mixMinusAudio(uid = false) { - if (session.stereo === false) { - var merger = session.audioCtx.createChannelMerger(1); - } else { - var merger = session.audioCtx.createChannelMerger(2); - } - - if (session.videoElement && session.videoElement.srcObject) { - var tracks = session.videoElement.srcObject.getAudioTracks(); - for (var i = 0; i < tracks.length; i++) { - try { - var tempStream = createMediaStream(); - tempStream.addTrack(tracks[i]); - trackStream = session.audioCtx.createMediaStreamSource(tempStream); - - if (session.stereo !== false) { - var splitter = session.audioCtx.createChannelSplitter(2); - trackStream.connect(splitter); - splitter.connect(merger, 0, 0); - try { - splitter.connect(merger, 1, 1); - } catch (e) { - errorlog(e); - try { - splitter.connect(merger, 0, 1); // hack. - } catch (e) { - errorlog(e); - } - } - } else { - trackStream.connect(merger, 0, 0); - } - } catch (e) { - errorlog(e); - } - } - } - - for (var UUID in session.rpcs) { - if (uid && UUID === uid) { - continue; - } - if (!session.rpcs[UUID].videoElement) { - continue; - } else if (!session.rpcs[UUID].videoElement.srcObject) { - continue; - } - - var tracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); - for (var i = 0; i < tracks.length; i++) { - try { - var tempStream = createMediaStream(); - tempStream.addTrack(tracks[i]); - trackStream = session.audioCtx.createMediaStreamSource(tempStream); - - if (session.stereo !== false) { - var splitter = session.audioCtx.createChannelSplitter(2); - trackStream.connect(splitter); - splitter.connect(merger, 0, 0); - try { - splitter.connect(merger, 1, 1); - } catch (e) { - errorlog(e); - try { - splitter.connect(merger, 0, 1); // hack. - } catch (e) { - errorlog(e); - } - } - } else { - trackStream.connect(merger, 0, 0); - } - } catch (e) { - errorlog(e); - } - } - } - - var destination = session.audioCtx.createMediaStreamDestination(); - merger.connect(destination); - return destination.stream; -} - -// Cleanup audio nodes for a guest's mix-minus to prevent memory leaks -function cleanupMixMinusAudioNodes(uuid) { - if (!session.mixMinusState || !session.mixMinusState[uuid]) { - return; - } - - var nodes = session.mixMinusState[uuid].audioNodes; - if (!nodes) { - return; - } - - try { - // Disconnect all source nodes - if (nodes.sources) { - for (var i = 0; i < nodes.sources.length; i++) { - try { - nodes.sources[i].disconnect(); - } catch (e) { } - } - nodes.sources = []; - } - - // Disconnect all splitter nodes - if (nodes.splitters) { - for (var i = 0; i < nodes.splitters.length; i++) { - try { - nodes.splitters[i].disconnect(); - } catch (e) { } - } - nodes.splitters = []; - } - - // Disconnect merger - if (nodes.merger) { - try { - nodes.merger.disconnect(); - } catch (e) { } - nodes.merger = null; - } - - // Destination doesn't need explicit disconnect - nodes.destination = null; - - } catch (e) { - warnlog("Error cleaning up mix-minus audio nodes: " + e); - } -} - -// Director mix-minus: Creates a custom audio mix for a specific guest -// Includes all other guests' audio + director's audio, excluding the target guest's own audio -function createDirectorMixMinusForGuest(targetUUID) { - // Check if this guest has mix enabled (either via &mixminus or via UI) - // Allow if directorMixMinus is set OR if guest-specific state is enabled - if (!session.directorMixMinus && (!session.mixMinusState || !session.mixMinusState[targetUUID] || !session.mixMinusState[targetUUID].enabled)) { - return null; - } - - if (!session.mixMinusState) { - session.mixMinusState = {}; - } - - if (!session.audioCtx) { - warnlog("Audio context not initialized for mix-minus"); - return null; - } - - // Initialize state for this guest if not exists - if (!session.mixMinusState[targetUUID]) { - initMixMinusStateForGuest(targetUUID); - } - - var guestState = session.mixMinusState[targetUUID]; - - // Cleanup previous audio nodes before creating new ones (or if disabled) - cleanupMixMinusAudioNodes(targetUUID); - - if (!guestState || !guestState.enabled) { - return null; - } - - // Ensure audioNodes object exists - if (!guestState.audioNodes) { - guestState.audioNodes = { - merger: null, - destination: null, - sources: [], - splitters: [] - }; - } - - // Create channel merger based on stereo setting - var merger; - try { - if (session.stereo === false) { - merger = session.audioCtx.createChannelMerger(1); - } else { - merger = session.audioCtx.createChannelMerger(2); - } - guestState.audioNodes.merger = merger; - } catch (e) { - errorlog("Failed to create audio merger for mix-minus: " + e); - return null; - } - - // Helper function to connect audio track to merger - function connectTrackToMerger(track) { - try { - var tempStream = createMediaStream(); - tempStream.addTrack(track); - var trackStream = session.audioCtx.createMediaStreamSource(tempStream); - guestState.audioNodes.sources.push(trackStream); - - if (session.stereo !== false) { - var splitter = session.audioCtx.createChannelSplitter(2); - guestState.audioNodes.splitters.push(splitter); - trackStream.connect(splitter); - splitter.connect(merger, 0, 0); - try { - splitter.connect(merger, 1, 1); - } catch (e) { - errorlog(e); - try { - splitter.connect(merger, 0, 1); - } catch (e) { - errorlog(e); - } - } - } else { - trackStream.connect(merger, 0, 0); - } - } catch (e) { - errorlog(e); - } - } - - // Add director's processed audio mix (if enabled) - if (guestState.useDirectorMix !== false && session.videoElement && session.videoElement.srcObject) { - var mixTracks = session.videoElement.srcObject.getAudioTracks(); - for (var i = 0; i < mixTracks.length; i++) { - connectTrackToMerger(mixTracks[i]); - } - } - - // Add raw input devices (if any are enabled) - if (guestState.rawDevices && session.streamSrc) { - var inputTracks = session.streamSrc.getAudioTracks(); - for (var i = 0; i < inputTracks.length; i++) { - var track = inputTracks[i]; - var deviceId = track.getSettings().deviceId || track.id; - if (guestState.rawDevices[deviceId] === true) { - connectTrackToMerger(track); - } - } - } - - // Add all other guests' audio (from session.rpcs) - for (var UUID in session.rpcs) { - // Skip the target guest (they don't need to hear themselves) - if (UUID === targetUUID) { - continue; - } - - // Check if this source is excluded for this guest - if (guestState.excludeSources && guestState.excludeSources.includes(UUID)) { - continue; - } - - // If using include mode, check if source is explicitly included - if (guestState.includeSources && guestState.includeSources.length > 0) { - if (!guestState.includeSources.includes(UUID)) { - continue; - } - } - - // Skip if rpcs entry doesn't exist or has no audio - if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].videoElement.srcObject) { - continue; - } - - var guestTracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); - for (var i = 0; i < guestTracks.length; i++) { - connectTrackToMerger(guestTracks[i]); - } - } - - // Create destination stream - var destination = session.audioCtx.createMediaStreamDestination(); - merger.connect(destination); - guestState.audioNodes.destination = destination; - - return destination.stream; -} - -// Initialize mix-minus state for a guest -function initMixMinusStateForGuest(uuid) { - if (!session.mixMinusState) { - session.mixMinusState = {}; - } - - if (!session.mixMinusDefaults) { - session.mixMinusDefaults = { - allGuestsEnabled: true, - includeDirectorAudio: true, - includeAllGuests: session.directorMixMinus ? true : false // Only include guests by default if &mixminus is set - }; - } - - // Don't overwrite existing state (audio nodes may already be stored) - if (session.mixMinusState[uuid]) { - return; - } - - session.mixMinusState[uuid] = { - enabled: session.mixMinusDefaults.allGuestsEnabled, - excludeSources: [], - includeSources: [], - // Director audio options - useDirectorMix: true, // Use processed WebAudio output (with effects) - rawDevices: {}, // { deviceId: true/false } for raw input devices - directorAudioDevices: {}, // Legacy - kept for backwards compatibility - // Audio node references for cleanup - audioNodes: { - merger: null, - destination: null, - sources: [], // MediaStreamSource nodes - splitters: [] // ChannelSplitter nodes - } - }; - - // Initialize raw input devices (from session.streamSrc) - disabled by default - if (session.streamSrc) { - var inputTracks = session.streamSrc.getAudioTracks(); - for (var i = 0; i < inputTracks.length; i++) { - var deviceId = inputTracks[i].getSettings().deviceId || inputTracks[i].id; - session.mixMinusState[uuid].rawDevices[deviceId] = false; // Disabled by default - } - } - - // Legacy: also track processed WebAudio output devices - if (session.videoElement && session.videoElement.srcObject) { - var directorTracks = session.videoElement.srcObject.getAudioTracks(); - for (var i = 0; i < directorTracks.length; i++) { - var deviceId = directorTracks[i].getSettings().deviceId || directorTracks[i].id; - session.mixMinusState[uuid].directorAudioDevices[deviceId] = true; - } - } - - // Without &mixminus, exclude other guests by default (director audio only) - // With &mixminus, include all guests by default (mix-minus behavior) - if (!session.mixMinusDefaults.includeAllGuests) { - // Add all current guests (except target) to excludeSources - for (var guestUUID in session.rpcs) { - if (guestUUID !== uuid) { - session.mixMinusState[uuid].excludeSources.push(guestUUID); - } - } - } -} - -// Toggle mix-minus enabled/disabled for a specific guest -function toggleMixMinusForGuest(uuid) { - if (!session.mixMinusState) { - session.mixMinusState = {}; - } - if (!session.mixMinusState[uuid]) { - initMixMinusStateForGuest(uuid); - } - session.mixMinusState[uuid].enabled = !session.mixMinusState[uuid].enabled; - updateMixMinusForGuest(uuid); - return session.mixMinusState[uuid].enabled; -} - -// Toggle a specific audio source in the mix for a guest -function toggleSourceInMixForGuest(sourceUUID, targetUUID) { - if (!session.mixMinusState) { - session.mixMinusState = {}; - } - if (!session.mixMinusState[targetUUID]) { - initMixMinusStateForGuest(targetUUID); - } - - var state = session.mixMinusState[targetUUID]; - var idx = state.excludeSources.indexOf(sourceUUID); - if (idx > -1) { - state.excludeSources.splice(idx, 1); // Remove from exclude list (enable) - } else { - state.excludeSources.push(sourceUUID); // Add to exclude list (disable) - } - - updateMixMinusForGuest(targetUUID); - return idx > -1; // Returns true if source is now enabled -} - -// Toggle a director audio device in the mix for a guest -function toggleDirectorDeviceInMix(deviceId, targetUUID) { - if (!session.mixMinusState) { - session.mixMinusState = {}; - } - if (!session.mixMinusState[targetUUID]) { - initMixMinusStateForGuest(targetUUID); - } - - var state = session.mixMinusState[targetUUID]; - state.directorAudioDevices[deviceId] = !state.directorAudioDevices[deviceId]; - - updateMixMinusForGuest(targetUUID); - return state.directorAudioDevices[deviceId]; -} - -// Set mix-minus state for all guests -function setMixMinusForAll(enabled) { - if (!session.mixMinusDefaults) { - session.mixMinusDefaults = { - allGuestsEnabled: enabled, - includeDirectorAudio: true, - includeAllGuests: true - }; - } else { - session.mixMinusDefaults.allGuestsEnabled = enabled; - } - - if (!session.mixMinusState) { - session.mixMinusState = {}; - return; - } - - for (var uuid in session.mixMinusState) { - session.mixMinusState[uuid].enabled = enabled; - updateMixMinusForGuest(uuid); - } -} - -// Update/rebuild the mix-minus stream for a guest -// This should replace the audio track being sent to the guest -function updateMixMinusForGuest(uuid) { - if (!session.pcs[uuid]) { - return; - } - - var mixStream = createDirectorMixMinusForGuest(uuid); - if (!mixStream) { - return; - } - - var mixTracks = mixStream.getAudioTracks(); - if (!mixTracks.length) { - return; - } - - // Replace ALL audio tracks being sent to this guest - try { - var senders = session.pcs[uuid].getSenders(); - var replacedCount = 0; - for (var i = 0; i < senders.length; i++) { - if (senders[i].track && senders[i].track.kind === "audio") { - senders[i].replaceTrack(mixTracks[0]); - replacedCount++; - } - } - if (replacedCount > 0) { - log("Updated mix-minus audio for guest: " + uuid + " (replaced " + replacedCount + " audio track(s))"); - } - } catch (e) { - errorlog("Error updating mix-minus for guest " + uuid + ": " + e); - } -} - -// Called when a new guest joins - initialize their mix-minus state and send mix -function onGuestJoinedMixMinus(uuid) { - if (!session.directorMixMinus) { - return; - } - - initMixMinusStateForGuest(uuid); - - // Also update existing guests' mixes to include the new guest - for (var existingUUID in session.mixMinusState) { - if (existingUUID !== uuid && session.mixMinusState[existingUUID].enabled) { - updateMixMinusForGuest(existingUUID); - } - } -} - -// Called when a guest leaves - cleanup and update other guests' mixes -function onGuestLeftMixMinus(uuid) { - // Allow cleanup if directorMixMinus is set OR if there's any mix state - if (!session.directorMixMinus && !session.mixMinusState) { - return; - } - - // Cleanup audio nodes before removing state - cleanupMixMinusAudioNodes(uuid); - - // Remove from state - delete session.mixMinusState[uuid]; - - // Remove from exclude/include lists of other guests - for (var otherUUID in session.mixMinusState) { - var state = session.mixMinusState[otherUUID]; - var idx = state.excludeSources.indexOf(uuid); - if (idx > -1) { - state.excludeSources.splice(idx, 1); - } - idx = state.includeSources.indexOf(uuid); - if (idx > -1) { - state.includeSources.splice(idx, 1); - } - // Update their mix since a source left - updateMixMinusForGuest(otherUUID); - } -} - -// UI handler for mix-minus toggle button in director panel -function directToggleMixMinus(ele, event) { - if (!session.directorMixMinus) { - warnUser("Mix-minus not enabled. Add &mixminus to your director URL."); - return; - } - - var UUID = ele.dataset.UUID; - if (!UUID) { - // Try to find UUID from parent element - try { - UUID = ele.closest("[data-UUID]").dataset.UUID; - } catch (e) { - errorlog("Could not find guest UUID for mix-minus toggle"); - return; - } - } - - var enabled = toggleMixMinusForGuest(UUID); - - // Update button appearance - if (enabled) { - ele.classList.add("pressed"); - ele.title = "Mix-minus enabled - this guest hears all other audio"; - } else { - ele.classList.remove("pressed"); - ele.title = "Mix-minus disabled for this guest"; - } - - log("Mix-minus for " + UUID + " is now: " + (enabled ? "enabled" : "disabled")); -} - -// Global variable to track open dropdown -var activeMixDropdown = null; - -// Toggle mix dropdown visibility and populate sources -function toggleMixDropdown(UUID, buttonEle, event) { - if (event) { - event.stopPropagation(); - } - - // Find or get UUID from button - if (!UUID && buttonEle) { - UUID = buttonEle.dataset.UUID; - if (!UUID) { - try { - UUID = buttonEle.closest("[data-UUID]").dataset.UUID; - } catch (e) { - errorlog("Could not find guest UUID for mix dropdown"); - return; - } - } - } - - // Find the dropdown container (sibling to PGM/Mic row) - var container = buttonEle.closest(".row").nextElementSibling; - if (!container || !container.classList.contains("mix-dropdown-container")) { - errorlog("Could not find mix dropdown container"); - return; - } - - var dropdown = container.querySelector(".mix-dropdown"); - if (!dropdown) { - errorlog("Could not find mix dropdown element"); - return; - } - - // Close any other open dropdown - if (activeMixDropdown && activeMixDropdown !== dropdown) { - activeMixDropdown.style.display = "none"; - activeMixDropdown.closest(".mix-dropdown-container").style.display = "none"; - } - - // Toggle visibility - if (dropdown.style.display === "none" || dropdown.style.display === "") { - // Initialize state if needed - if (!session.mixMinusState) { - session.mixMinusState = {}; - } - if (!session.mixMinusState[UUID]) { - initMixMinusStateForGuest(UUID); - } - - // Enable mix-minus for this guest if not already - if (!session.mixMinusState[UUID].enabled) { - session.mixMinusState[UUID].enabled = true; - updateMixMinusForGuest(UUID); - } - - // Populate and show dropdown - populateMixDropdown(UUID, dropdown); - container.style.display = "block"; - dropdown.style.display = "block"; - activeMixDropdown = dropdown; - buttonEle.classList.add("pressed"); - - // Add click outside listener to close dropdown - setTimeout(function() { - document.addEventListener("click", closeMixDropdownOnClickOutside); - }, 10); - } else { - // Hide dropdown - dropdown.style.display = "none"; - container.style.display = "none"; - activeMixDropdown = null; - buttonEle.classList.remove("pressed"); - document.removeEventListener("click", closeMixDropdownOnClickOutside); - } -} - -// Close dropdown when clicking outside -function closeMixDropdownOnClickOutside(event) { - if (activeMixDropdown && !activeMixDropdown.contains(event.target)) { - var container = activeMixDropdown.closest(".mix-dropdown-container"); - activeMixDropdown.style.display = "none"; - if (container) { - container.style.display = "none"; - } - // Find and un-press the Mix button - var row = container ? container.previousElementSibling : null; - if (row) { - var mixBtn = row.querySelector('[data-action-type="custom-mix"]'); - if (mixBtn) { - mixBtn.classList.remove("pressed"); - } - } - activeMixDropdown = null; - document.removeEventListener("click", closeMixDropdownOnClickOutside); - } -} - -// Populate dropdown with available audio sources -function populateMixDropdown(targetUUID, dropdown) { - if (!dropdown) { - return; - } - - var html = '
    Audio Sources
    '; - var state = session.mixMinusState[targetUUID]; - - // Section 1: Director Mix (processed WebAudio output with effects) - html += '
    '; - html += '
    Director Mix
    '; - - if (session.videoElement && session.videoElement.srcObject) { - var mixTracks = session.videoElement.srcObject.getAudioTracks(); - if (mixTracks.length > 0) { - var checked = state.useDirectorMix !== false; - html += '
    '; - html += ''; - html += ''; - html += '
    '; - } else { - html += '
    No director mix available
    '; - } - } else { - html += '
    No director mix available
    '; - } - html += '
    '; - - // Section 2: Director Input Devices (raw, unprocessed) - html += '
    '; - html += '
    Director Input Devices
    '; - - if (session.streamSrc) { - var inputTracks = session.streamSrc.getAudioTracks(); - if (inputTracks.length === 0) { - html += '
    No input devices
    '; - } else { - for (var i = 0; i < inputTracks.length; i++) { - var track = inputTracks[i]; - var deviceId = track.getSettings().deviceId || track.id; - var label = track.label || ("Mic " + (i + 1)); - var checked = state.rawDevices && state.rawDevices[deviceId] === true; - - html += '
    '; - html += ''; - html += ''; - html += '
    '; - } - } - } else { - html += '
    No input devices
    '; - } - html += '
    '; - - // Other guests section - html += '
    '; - html += '
    Guests
    '; - - var guestCount = 0; - for (var UUID in session.rpcs) { - // Skip the target guest (they don't hear themselves) - if (UUID === targetUUID) { - continue; - } - - if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].videoElement.srcObject) { - continue; - } - - var guestTracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); - if (guestTracks.length === 0) { - continue; - } - - guestCount++; - var guestLabel = session.rpcs[UUID].label || "Guest"; - var streamID = session.rpcs[UUID].streamID || UUID.substring(0, 6); - var displayLabel = guestLabel + " - " + streamID; - - // Check if source is excluded - var checked = state.excludeSources.indexOf(UUID) === -1; - - html += '
    '; - html += ''; - html += ''; - html += '
    '; - } - - if (guestCount === 0) { - html += '
    No other guests
    '; - } - - html += '
    '; - - dropdown.innerHTML = html; -} - -// Helper to sanitize labels for HTML display -function sanitizeLabel(str) { - if (!str) return ""; - return str.replace(//g, ">").replace(/"/g, """); -} - -// Toggle a source in the mix and update the mix -function toggleMixSource(targetUUID, sourceId, sourceType, checkbox) { - if (!session.mixMinusState || !session.mixMinusState[targetUUID]) { - return; - } - - var state = session.mixMinusState[targetUUID]; - - if (sourceType === 'mix') { - // Toggle Director Mix (processed WebAudio output) - state.useDirectorMix = !state.useDirectorMix; - updateMixMinusForGuest(targetUUID); - } else if (sourceType === 'raw') { - // Toggle raw input device - if (!state.rawDevices) { - state.rawDevices = {}; - } - state.rawDevices[sourceId] = !state.rawDevices[sourceId]; - updateMixMinusForGuest(targetUUID); - } else if (sourceType === true) { - // Legacy: Toggle director audio device (backwards compatibility) - toggleDirectorDeviceInMix(sourceId, targetUUID); - } else { - // Toggle guest audio source (sourceType === false or undefined) - toggleSourceInMixForGuest(sourceId, targetUUID); - } -} - -function listAudioSettingsPrep() { - try { - var tracks = session.streamSrc.getAudioTracks(); - if (!tracks.length) { - warnlog("session.streamSrc contains no audio tracks"); - //return; - } - } catch (e) { - warnlog(e); - return; - } - - var data = []; - - for (var i = 0; i < tracks.length; i += 1) { - track0 = tracks[i]; - var trackSet = {}; - - if (track0.getCapabilities) { - trackSet.audioConstraints = track0.getCapabilities(); - } else if (Firefox) { - // let's pretend like Firefox doesn't actually suck - trackSet.audioConstraints = { - autoGainControl: [true, false], - // "channelCount": { - // "max": 2, - // "min": 1 - // }, - // "deviceId": "default", - echoCancellation: [true, false], - // "groupId": "a3cbdec54a9b6ed473fd950415626f7e76f9d1b90f8c768faab572175a355a17", - // "latency": { - // "max": 0.01, - // "min": 0.01 - // }, - noiseSuppression: [true, false] - // "sampleRate": { - // "max": 48000, - // "min": 48000 - // }, - // "sampleSize": { - // "max": 16, - // "min": 16 - /// } - }; - } - - if (track0.getSettings) { - trackSet.currentAudioConstraints = track0.getSettings(); - if (!session.stereo) { - try { - delete trackSet.currentAudioConstraints.channelCount; - delete trackSet.audioConstraints.channelCount; - } catch (e) { } - } else if (session.audioInputChannels && session.audioInputChannels == 1) { - // this is pretty hacky, but it gets around not being able to actually set 1-channel. Not sure why. - trackSet.currentAudioConstraints.channelCount = 1; - } - } - - trackSet.trackLabel = "unknown or none"; - if (track0.label) { - trackSet.trackLabel = track0.label; - } - if (track0.id) { - trackSet.deviceId = track0.id; - } - if (i == 0) { - trackSet.equalizer = session.equalizer; // only supporting the first track at the moment. - - for (var waid in session.webAudios) { - // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){.. - try { - trackSet.lowEQ = session.webAudios[waid].lowEQ.gain.value; - trackSet.midEQ = session.webAudios[waid].midEQ.gain.value; - trackSet.highEQ = session.webAudios[waid].highEQ.gain.value; - } catch (e) { } - break; - } - } else { - trackSet.equalizer = false; - } - - if (i == 0) { - trackSet.lowcut = session.lowcut; // only supporting the first track at the moment. - if (session.lowcut) { - for (var waid in session.webAudios) { - // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){.. - try { - trackSet.lowcut = session.webAudios[waid].lowcut1.frequency.value; - } catch (e) { } - break; - } - } - } else { - trackSet.lowcut = false; - } - - trackSet.subGain = false; - for (var waid in session.webAudios) { - // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){.. - try { - if (session.webAudios[waid].subGainNodes && track0.id in session.webAudios[waid].subGainNodes) { - trackSet.subGain = session.webAudios[waid].subGainNodes[track0.id].gain.value; - } - break; - } catch (e) { } - } - - if (i == 0 && !session.disableWebAudio) { - // if web audio is disabled, don't show them - trackSet.gating = session.noisegate; - trackSet.compressor = session.compressor; - trackSet.micDelay = session.micDelay; - trackSet.micPanning = session.micPanning !== false ? session.micPanning : false; - } - - data.push(trackSet); - } - - pokeIframeAPI("listing-audio-settings", data); - return data; -} - -function listVideoSettingsPrep() { - try { - var track0 = session.streamSrc.getVideoTracks(); - if (track0.length) { - track0 = track0[0]; - if (track0.getCapabilities) { - session.cameraConstraints = track0.getCapabilities(); - } - log(session.cameraConstraints); - } - } catch (e) { - warnlog(e); - return; - } - - try { - if (track0.getSettings) { - session.currentCameraConstraints = track0.getSettings(); - - if (screen && screen.orientation && screen.orientation.type) { - if (screen.orientation.type.includes("portrait")) { - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - } else if (window.matchMedia("(orientation: portrait)").matches) { - // legacy - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - } - } catch (e) { - warnlog(e); - return; - } - var msg = {}; - msg.trackLabel = "unknown or none"; - if (track0.label) { - msg.trackLabel = track0.label; - } - msg.currentCameraConstraints = session.currentCameraConstraints; - msg.cameraConstraints = session.cameraConstraints; - - pokeIframeAPI("listing-video-settings", msg); - return msg; -} - -var Final_transcript = ""; -var Interim_transcript = ""; -var Recognition = null; - -if ("webkitSpeechRecognition" in window) { - var SpeechRecognition = webkitSpeechRecognition; -} else if ("SpeechRecognition" in window) { - var SpeechRecognition = window.SpeechRecognition; -} else { - var SpeechRecognition = false; -} - -var TranscriptionCounter = 0; -var retriesRecognition = 0; -var activeRecognition = false; -var timeoutRecognition = null; -function setupClosedCaptions() { - if (activeRecognition) { - return; - } - activeRecognition = true; - - log("CLOSED CAPTIONING SETUP"); - - if (SpeechRecognition) { - Recognition = new SpeechRecognition(); - - Recognition.lang = session.transcript; - - Recognition.continuous = true; - Recognition.interimResults = true; - Recognition.maxAlternatives = 0; - - Recognition.onstart = function () { - log("started transcription: " + Date.now()); - clearTimeout(timeoutRecognition); - timeoutRecognition = setTimeout(function () { - retriesRecognition = 0; - }, 10000); - }; - Recognition.onerror = function (event) { - if (retriesRecognition <= 3) { - console.error(event); - } - errorlog(event); - }; - Recognition.onend = function (e) { - warnlog(e); - log("Stopped transcription " + Date.now()); - clearTimeout(timeoutRecognition); - timeoutRecognition = setTimeout(function () { - Recognition.start(); - }, parseInt(500 * retriesRecognition * retriesRecognition)); // restart it if it fails. - retriesRecognition += 1; - if (retriesRecognition == 3) { - console.error("Captioning service is having a problem connecting"); - } - }; - - Recognition.onresult = function (event) { - Interim_transcript = ""; - if (typeof event.results == "undefined") { - log(event); - return; - } - for (var i = event.resultIndex; i < event.results.length; ++i) { - if (event.results[i].isFinal) { - Final_transcript += event.results[i][0].transcript; - } else { - Interim_transcript += event.results[i][0].transcript; - } - } - - if (Final_transcript.length > 0) { - log("FINAL:" + Final_transcript); - try { - var data = {}; - data.isFinal = true; - data.transcript = Final_transcript; - data.counter = TranscriptionCounter; - session.sendMessage(data); - TranscriptionCounter += 1; - Final_transcript = ""; - Interim_transcript = ""; - pokeIframeAPI("transcription-text", Final_transcript); - } catch (e) { - errorlog(e); - } - } else { - try { - var data = {}; - data.isFinal = false; - data.transcript = Interim_transcript; - data.counter = TranscriptionCounter; - session.sendMessage(data); - } catch (e) { - errorlog(e); - Interim_transcript = ""; - } - } - }; - - Recognition.start(); - } else if (!session.cleanOutput) { - warnUser(getTranslation("speech-not-suppoted"), false, false); - } -} -async function requestGoogleDriveRecord(ele, state = null, bitrate = null, event = null) { - var UUID = ele.dataset.UUID || null; - // Handle CTRL+click for selection - if (event && (event.ctrlKey || event.metaKey)) { - ele.classList.toggle("armed"); - ele.ariaPressed = ele.classList.contains("armed") ? "true" : "false"; - - // Add callback only once for all armed buttons - if (document.querySelectorAll('[data-action-type="recorder-google-drive-remote"].armed').length === 1 && - ele.classList.contains("armed")) { - Callbacks.push([multiGdriveRecord]); - } - return; - } - // Single button normal operation - if (!state && ele.classList.contains("pressed")) { - var msg = {}; - msg.requestVideoRecord = false; - msg.googleDriveRecord = false; - msg.UUID = UUID; - session.sendRequest(msg, msg.UUID); - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } else if (state == null || state) { - if (!(session.gdrive && session.gdrive.accessToken)) { - session.gdrive = setupGoogleDriveUploader(); - if (session.gdrive.promise) { - log("AWAITING PROMISE"); - try { - // Make sure we're initialized before requesting a token - await session.gdrive.ensureInitialized(); - session.gdrive.requestAccessToken(); - await session.gdrive.promise; - console.log("Promise resolved with token"); - } catch (e) { - console.error("Error getting token:", e); - ele.classList.remove("armed"); - return; - } - } - } - - var filename = UUID; - if (session.rpcs[UUID]) { - filename = session.rpcs[UUID].label || session.rpcs[UUID].streamID || UUID; - } - filename = filename.replace(/[\W]+/g, "_"); - filename = filename.substring(0, 55); - filename += "_" + Date.now().toString(); - if (SafariVersion) { - filename += ".mp4"; - } else { - filename += ".webm"; - } - - log("PROMISE DONE"); - var uploadLink = await session.gdrive.startResumableUpload(filename); - - var msg = {}; - msg.requestVideoRecord = true; - msg.googleDriveRecord = uploadLink; - msg.UUID = UUID; - - if (bitrate === null) { - window.focus(); - let response = await promptRecordingOptions(getTranslation("what-bitrate-gdrive")); - if (response) { - msg.value = response.bitrate; - msg.recordConfig = response; - session.sendRequest(msg, msg.UUID); - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.classList.remove("armed"); - } else { - ele.classList.remove("armed"); - return; - } - } else { - msg.value = bitrate; - session.sendRequest(msg, msg.UUID); - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - ele.classList.remove("armed"); - } - - pokeIframeAPI("request-video-record", msg.requestVideoRecord, UUID); - } -} -async function multiGdriveRecord() { - const armedButtons = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"].armed'); - if (!armedButtons.length) return; - - armedButtons.forEach(button => { - button.classList.remove("armed"); - button.ariaPressed = "false"; - }); - - // Get recording settings once for all buttons - window.focus(); - let response = await promptRecordingOptions(getTranslation("what-bitrate-gdrive")); - if (!response) { - return; - } - - // Set up Google Drive authentication once - if (!(session.gdrive && session.gdrive.accessToken)) { - session.gdrive = setupGoogleDriveUploader(); - if (session.gdrive.promise) { - try { - if (typeof session.gdrive.ensureInitialized === "function") { - await session.gdrive.ensureInitialized(); - } - if (typeof session.gdrive.requestAccessToken === "function") { - session.gdrive.requestAccessToken(); - } - await session.gdrive.promise; - } catch (e) { - // Auth failed, clean up armed buttons - armedButtons.forEach(button => { - button.classList.remove("armed"); - button.ariaPressed = "false"; - }); - return; - } - } - } - - // Process each armed button with the same settings - for (const button of armedButtons) { - const UUID = button.dataset.UUID || null; - - // Generate unique filename for each recording - const filename = ((session.rpcs[UUID] && (session.rpcs[UUID].label || session.rpcs[UUID].streamID)) || UUID) - .replace(/[\W]+/g, "_") - .substring(0, 55) + - "_" + Date.now().toString() + - (SafariVersion ? ".mp4" : ".webm"); - - // Get upload link for each recording - const uploadLink = await session.gdrive.startResumableUpload(filename); - - // Create message with shared settings - const msg = { - requestVideoRecord: true, - googleDriveRecord: uploadLink, - UUID: UUID, - value: response.bitrate, - recordConfig: response - }; - - // Send request and update button state - session.sendRequest(msg, msg.UUID); - button.classList.add("pressed"); - button.classList.remove("armed"); - button.ariaPressed = "true"; - - pokeIframeAPI("request-video-record", true, UUID); - } -} - -async function requestVideoRecord(ele, state = null, bitrate = null) { - var UUID = ele.dataset.UUID || null; - - if (!state && ele.classList.contains("pressed")) { - var msg = {}; - msg.requestVideoRecord = false; - msg.UUID = UUID; - session.sendRequest(msg, msg.UUID); - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } else if (state == null || state) { - var msg = {}; - msg.requestVideoRecord = true; - msg.UUID = UUID; - if (bitrate === null) { - window.focus(); - let response = await promptRecordingOptions(getTranslation("what-bitrate")); - if (response) { - msg.value = response.bitrate; - msg.recordConfig = response; - session.sendRequest(msg, msg.UUID); - ele.classList.add("pressed"); - ele.ariaPressed = "true"; // "btn-HL-green" - } else { return; } - } else { - msg.value = bitrate; - session.sendRequest(msg, msg.UUID); - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } - } - pokeIframeAPI("request-video-record", msg.requestVideoRecord, UUID); -} - -function changeOrderDirector(value) { - if (session.order == false) { - session.order = 0; - } - session.order += parseInt(value) || 0; - - var elements = document.querySelectorAll('[data-action-type="order-value-director"]'); - //log(elements); - if (elements[0]) { - elements[0].innerText = parseInt(session.order) || 0; - } - - var data = {}; - data = {}; - data.order = session.order; - session.sendPeers(data); - pokeIframeAPI("director-order", data.order); -} - -function changeOrder(value, UUID) { - var msg = {}; - msg.changeOrder = value; - msg.UUID = UUID; - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("change-order", value, UUID); -} - -function requestVideoHack(keyname, value, UUID, ctrl = false) { - var msg = {}; - msg.requestVideoHack = true; - msg.keyname = keyname; - msg.value = value; - msg.UUID = UUID; - msg.ctrl = ctrl; - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-video-setting", { value: value, keyname: keyname, ctrl: ctrl }, UUID); -} - -function requestAudioHack(keyname, value, UUID, deviceId = "default") { - var msg = {}; - msg.requestAudioHack = true; - msg.keyname = keyname; - msg.value = value; - msg.UUID = UUID; - msg.deviceId = deviceId; - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-audio-setting", { value: value, keyname: keyname, deviceId: deviceId }, UUID); -} - -function requestChangeEQ(keyname, value, UUID, track = 0) { - var msg = {}; - msg.requestChangeEQ = true; - msg.keyname = keyname; - msg.value = value; - msg.UUID = UUID; - msg.track = track; // pointless atm - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-change-eq", { value: value, keyname: keyname, track: track }, UUID); -} - -function requestChangeGating(keyname, value, UUID, track = 0) { - var msg = {}; - msg.requestChangeGating = true; - msg.keyname = keyname; - msg.value = value; - msg.UUID = UUID; - msg.track = track; // pointless atm - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-change-gating", { value: value, keyname: keyname, track: track }, UUID); -} -function requestChangeCompressor(keyname, value, UUID, track = 0) { - var msg = {}; - msg.requestChangeCompressor = true; - msg.keyname = keyname; - msg.value = value; - msg.UUID = UUID; - msg.track = track; // pointless atm - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-change-compressor", { value: value, keyname: keyname, track: track }, UUID); -} -function requestChangeMicDelay(value, UUID, track = 0) { - var msg = {}; - msg.requestChangeMicDelay = true; - msg.value = value; - msg.UUID = UUID; - msg.track = track; // pointless atm - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-change-mic-delay", { value: value, track: track }, UUID); -} - -function requestChangeSubGain(value, UUID, deviceId) { - var msg = {}; - msg.requestChangeSubGain = true; - msg.value = value; - msg.UUID = UUID; - msg.deviceId = deviceId; // pointless atm - log(msg); - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-sub-gain", { value: value, deviceId: deviceId }, UUID); -} - -function requestChangeLowcut(value, UUID, track = 0) { - var msg = {}; - msg.requestChangeLowcut = true; - msg.value = value; - msg.UUID = UUID; - msg.track = track; // pointless atm - session.sendRequest(msg, msg.UUID); - pokeIframeAPI("request-low-cut", value, UUID); -} - -function toggleSystemPip(vid, autoRetry = false) { - if (!vid) { - return Promise.resolve(false); - } - try { - if (vid.webkitSupportsPresentationMode && typeof vid.webkitSetPresentationMode === "function") { - vid.webkitSetPresentationMode(vid.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture"); - clearAutoPiPPrompt(); - return Promise.resolve(true); - } else if (!document.pictureInPictureEnabled) { - return Promise.resolve(false); - } - - var pipPromise = null; - if (document.pictureInPictureElement) { - if (document.pictureInPictureElement === vid) { - pipPromise = document.exitPictureInPicture(); - } else { - pipPromise = document.exitPictureInPicture().catch(errorlog).then(() => vid.requestPictureInPicture()); - } - } else { - pipPromise = vid.requestPictureInPicture(); - } - - if (!pipPromise || typeof pipPromise.then !== "function") { - clearAutoPiPPrompt(); - return Promise.resolve(true); - } - - return pipPromise - .then(() => { - clearAutoPiPPrompt(); - return true; - }) - .catch(err => { - if (autoRetry && err && (err.name === "NotAllowedError" || err.name === "InvalidStateError")) { - showAutoPiPPrompt(vid); - } - errorlog(err); - return false; - }); - } catch (e) { - if (autoRetry && e && (e.name === "NotAllowedError" || e.name === "InvalidStateError")) { - showAutoPiPPrompt(vid); - } - errorlog(e); - return Promise.resolve(false); - } -} - -function showAutoPiPPrompt(vid) { - if (!vid) { - return; - } - session.autoPiPPromptVideo = vid; - if (session.autoPiPPrompt) { - return; - } - var prompt = document.createElement("div"); - prompt.id = "pipPrompt"; - prompt.style.position = "fixed"; - prompt.style.bottom = "16px"; - prompt.style.right = "16px"; - prompt.style.zIndex = "12000"; - prompt.style.background = "rgba(20,20,24,0.95)"; - prompt.style.border = "1px solid rgba(255,255,255,0.2)"; - prompt.style.borderRadius = "8px"; - prompt.style.padding = "12px"; - prompt.style.maxWidth = "280px"; - prompt.style.display = "flex"; - prompt.style.flexDirection = "column"; - prompt.style.gap = "8px"; - prompt.style.boxShadow = "0 4px 12px rgba(0,0,0,0.35)"; - - var message = document.createElement("div"); - message.innerText = "Click to enable picture-in-picture"; - message.style.fontSize = "14px"; - message.style.lineHeight = "18px"; - - var controls = document.createElement("div"); - controls.style.display = "flex"; - controls.style.justifyContent = "space-between"; - controls.style.gap = "8px"; - - var confirmBtn = document.createElement("button"); - confirmBtn.type = "button"; - confirmBtn.innerText = "Open PiP"; - confirmBtn.style.flex = "1"; - confirmBtn.style.padding = "6px 10px"; - confirmBtn.style.borderRadius = "6px"; - confirmBtn.style.border = "1px solid rgba(255,255,255,0.2)"; - confirmBtn.style.background = "var(--accent-color, #3a7afe)"; - confirmBtn.style.color = "#fff"; - confirmBtn.style.cursor = "pointer"; - - var dismissBtn = document.createElement("button"); - dismissBtn.type = "button"; - dismissBtn.innerText = "Not now"; - dismissBtn.style.flex = "1"; - dismissBtn.style.padding = "6px 10px"; - dismissBtn.style.borderRadius = "6px"; - dismissBtn.style.border = "1px solid rgba(255,255,255,0.2)"; - dismissBtn.style.background = "rgba(255,255,255,0.08)"; - dismissBtn.style.color = "#fff"; - dismissBtn.style.cursor = "pointer"; - - confirmBtn.addEventListener("click", function (event) { - event.preventDefault(); - event.stopPropagation(); - var target = session.autoPiPPromptVideo; - clearAutoPiPPrompt(); - if (target) { - toggleSystemPip(target); - } - }); - - dismissBtn.addEventListener("click", function (event) { - event.preventDefault(); - event.stopPropagation(); - clearAutoPiPPrompt(); - }); - - controls.appendChild(confirmBtn); - controls.appendChild(dismissBtn); - prompt.appendChild(message); - prompt.appendChild(controls); - document.body.appendChild(prompt); - session.autoPiPPrompt = prompt; -} - -function clearAutoPiPPrompt() { - if (session.autoPiPPrompt) { - try { - session.autoPiPPrompt.remove(); - } catch (e) { } - session.autoPiPPrompt = false; - } - session.autoPiPPromptVideo = false; -} - -function updateDirectorsAudio(dataN, UUID) { - var audioEle = document.createElement("div"); - query("#container_" + UUID + " .advancedAudioSettings").innerHTML = ""; - - if (query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').classList.contains("pressed")) { - query("#container_" + UUID + " .advancedAudioSettings").classList.remove("hidden"); - } - //query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').classList.add("pressed"); - //query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').ariaPressed = "true"; - - //log(dataN); - if (!dataN.length) { - - var label = document.createElement("label"); - label.innerText = "No microphone selected"; - label.style.display = "block"; - label.id = "remoteAudioLabel_" + UUID; - label.dataset.nomic = true; - label.classList.add("settingsLabel"); - label.dataset.UUID = UUID; - audioEle.appendChild(label); - - query('[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]').disabled = true; - - query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioEle); - return; - } - - query('[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]').disabled = false; - - for (var n = 0; n < dataN.length; n += 1) { - var data = dataN[n]; - - if (dataN.length == 1) { - if (data.trackLabel) { - var label = document.createElement("label"); - label.innerText = data.trackLabel; - label.style.display = "block"; - label.id = "remoteAudioLabel_" + UUID; - label.classList.add("settingsLabel"); - label.dataset.UUID = UUID; - audioEle.appendChild(label); - } - } - //if (n !== 0) { - //var label = document.createElement("span"); - //label.innerText = "Coming Soon"; - //audioEle.appendChild(label); - // continue; // remove to more than one audio device (assuming other fixes are applied) - //} - - if ("micDelay" in data && n == 0) { - var label = document.createElement("label"); - var i = "micDelay"; - var div = document.createElement("div"); - label.id = "label_" + i + "_" + UUID; - label.htmlFor = "constraints_" + i + "_" + UUID; - - var input = document.createElement("input"); - input.min = 0; - input.max = 500; - input.value = data.micDelay || 0; - - input.title = "Previously was: " + input.value; - - input.type = "range"; - input.dataset.keyname = i; - //input.dataset.labelname = "mic delay (ms):"; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + " (ms):"; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.value = data.micDelay || 0; - manualInput.className = "manualInput"; - manualInput.id = "constraints_manual_" + i + "_" + UUID; - manualInput.dataset.UUID = UUID; - manualInput.dataset.track = n; - - input.dataset.track = n; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + UUID; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - input.style.margin = "2px 0px 5px"; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.onchange = function (e) { - //e.target.title = e.target.value; - getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.oninput = function (e) { - getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - } - }; - - audioEle.appendChild(div); - div.appendChild(label); - div.appendChild(manualInput); - audioEle.appendChild(input); - } - - if (data.micPanning !== false && n == 0) { - // Director-side control: Mic Panning (0..180, 90=center) - var label = document.createElement("label"); - var i = "micPanning"; - var div = document.createElement("div"); - label.id = "label_" + i + "_" + UUID; - label.htmlFor = "constraints_" + i + "_" + UUID; - label.innerText = "Mic Pan:"; - - var input = document.createElement("input"); - input.min = 0; - input.max = 180; - input.value = data.micPanning || 90; - input.title = "0=L, 90=C, 180=R"; - input.type = "range"; - input.dataset.keyname = i; - input.dataset.track = n; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + UUID; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - input.style.margin = "2px 0px 5px"; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.value = data.micPanning || 90; - manualInput.className = "manualInput"; - manualInput.id = "constraints_manual_" + i + "_" + UUID; - manualInput.dataset.UUID = UUID; - manualInput.dataset.track = n; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeMicPanning(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.onchange = function (e) { - getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeMicPanning(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.oninput = function (e) { - getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - requestChangeMicPanning(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - } - }; - - audioEle.appendChild(div); - div.appendChild(label); - div.appendChild(manualInput); - audioEle.appendChild(input); - } - - if (data.lowcut !== false && n == 0) { - var label = document.createElement("label"); - var i = "lowCut"; - label.id = "label_" + i + "_" + UUID; - label.htmlFor = "constraints_" + i + "_" + UUID; - - var input = document.createElement("input"); - input.min = 50; - input.max = 150; - input.value = data.lowcut; - - input.title = "Previously was: " + input.value; - - input.type = "range"; - input.dataset.keyname = i; - //input.dataset.labelname = "low cut:"; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.value = data.lowcut; - manualInput.className = "manualInput"; - manualInput.id = "constraints_manual_" + i + "_" + UUID; - manualInput.dataset.UUID = UUID; - manualInput.dataset.track = n; - - input.dataset.track = n; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + UUID; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - input.style.margin = "2px 0px 5px"; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.onchange = function (e) { - //e.target.title = e.target.value; - getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.oninput = function (e) { - getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - } - }; - - audioEle.appendChild(label); - audioEle.appendChild(manualInput); - audioEle.appendChild(input); - } - - if (data.equalizer && n == 0) { - var label = document.createElement("label"); - var i = "Low_EQ"; - //label.id = "label_" + i + "_"+UUID; - label.htmlFor = "constraints_" + i + "_" + UUID; - - var input = document.createElement("input"); - input.min = -50; - input.max = 50; - input.value = data.lowEQ; - input.title = "Previously was: " + input.value; - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = "low EQ:"; - - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.value = data.lowEQ; - manualInput.className = "manualInput"; - manualInput.id = "label_" + i + "_" + UUID; - manualInput.dataset.UUID = UUID; - manualInput.dataset.track = n; - - input.dataset.track = n; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + UUID; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - input.style.margin = "2px 0px 5px"; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - } - }; - - audioEle.appendChild(label); - audioEle.appendChild(manualInput); - audioEle.appendChild(input); - - var label = document.createElement("label"); - var i = "midEQ"; - //label.id = "label_" + i + "_"+UUID; - label.htmlFor = "constraints_" + i + "_" + UUID; - - var input = document.createElement("input"); - input.min = -50; - input.max = 50; - input.value = data.midEQ; - input.title = "Previously was: " + input.value; - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = "mid EQ:"; - - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.value = data.midEQ; - manualInput.className = "manualInput"; - manualInput.id = "label_" + i + "_" + UUID; - manualInput.dataset.UUID = UUID; - manualInput.dataset.track = n; - - input.dataset.track = n; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + UUID; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - input.style.margin = "2px 0px 5px"; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - } - }; - - audioEle.appendChild(label); - audioEle.appendChild(manualInput); - audioEle.appendChild(input); - - var label = document.createElement("label"); - var i = "highEQ"; - //label.id = "label_" + i + "_"+UUID; - label.htmlFor = "constraints_" + i + "_" + UUID; - - var input = document.createElement("input"); - input.min = -50; - input.max = 50; - input.value = data.highEQ; - input.title = "Previously was: " + input.value; - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = "high EQ:"; - - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.value = data.highEQ; - manualInput.className = "manualInput"; - manualInput.id = "label_" + i + "_" + UUID; - manualInput.dataset.UUID = UUID; - manualInput.dataset.track = n; - - input.dataset.track = n; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + UUID; - input.classList.add("inputConstraint"); - input.name = "constraints_" + i; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); - } - }; - - audioEle.appendChild(label); - audioEle.appendChild(manualInput); - audioEle.appendChild(input); - } - - if ("gating" in data && n == 0) { - // only show once. - var label = document.createElement("label"); - var i = "noiseGate"; - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i + "_" + n + "_" + UUID; - label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block; padding:0;"; - label.dataset.keyname = i; - label.dataset.track = n; - var input = document.createElement("select"); - var c = document.createElement("option"); - - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - input.options.add(opt); - - if (data.gating) { - opt.selected = "true"; - } - - input.dataset.deviceId = data.deviceId; - input.id = "constraints_" + i + "_" + n + "_" + UUID; - input.className = "constraintCameraInput"; - input.name = "constraints_" + i + "_" + n; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.track = n; - input.dataset.UUID = UUID; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; - requestChangeGating("gating", e.target.value, e.target.dataset.UUID, parseInt(e.target.dataset.track)); - log(e.target.dataset.keyname, e.target.value); - }; - audioEle.appendChild(div); - div.appendChild(label); - div.appendChild(input); - } - - if ("compressor" in data && n == 0) { - var label = document.createElement("label"); - var i = "compressor"; - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i + "_" + n + "_" + UUID; - label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block; padding:0;"; - label.dataset.keyname = i; - label.dataset.track = n; - - var input = document.createElement("select"); - var c = document.createElement("option"); - - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", 1); - input.options.add(opt); - - if (data.compressor == 1) { - opt.selected = "true"; - } - opt = new Option("Limiter", 2); - input.options.add(opt); - - if (data.compressor == 2) { - opt.selected = "true"; - } - - input.dataset.deviceId = data.deviceId; - input.id = "constraints_" + i + "_" + n + "_" + UUID; - input.className = "constraintCameraInput"; - input.name = "constraints_" + i + "_" + n; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.track = n; - input.dataset.UUID = UUID; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; - requestChangeCompressor("compressor", e.target.value, e.target.dataset.UUID, parseInt(e.target.dataset.track)); - log(e.target.dataset.keyname, e.target.value); - }; - audioEle.appendChild(div); - div.appendChild(label); - div.appendChild(input); - } - - if (dataN.length > 1) { - if (data.trackLabel) { - var label = document.createElement("label"); - label.innerText = data.trackLabel; - label.style.display = "block"; - label.id = "remoteAudioLabel_" + UUID + "_" + n + "_" + UUID; - label.classList.add("settingsLabel"); - audioEle.appendChild(label); - } - } - - for (var i in data.audioConstraints) { - try { - log(i); - log(data.audioConstraints[i]); - if (typeof data.audioConstraints[i] === "object" && data.audioConstraints[i] !== null && "max" in data.audioConstraints[i] && "min" in data.audioConstraints[i]) { - if (i === "aspectRatio") { - continue; - } else if (i === "width") { - continue; - } else if (i === "height") { - continue; - } else if (i === "frameRate") { - continue; - } else if (i === "latency") { - // continue; - } else if (i === "sampleRate") { - continue; - } else if (i === "channelCount") { - // continue; - } else if (i === "volume") { - continue; - } - - if (!("deviceId" in data.audioConstraints)) { - continue; - } // not going to support older versions. - - var label = document.createElement("label"); - //label.id = "label_" + i + "_"+n+ "_"+UUID; - label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - - var input = document.createElement("input"); - input.min = data.audioConstraints[i].min; - input.max = data.audioConstraints[i].max; - - if (parseFloat(input.min) == parseFloat(input.max)) { - continue; - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - - if ("step" in data.audioConstraints[i]) { - input.step = data.audioConstraints[i].step; - manualInput.step = data.audioConstraints[i].step; - } else if ("volume" == i) { - input.step = 0.01; - manualInput.step = 0.01; - } - - manualInput.dataset.keyname = i; - manualInput.className = "manualInput"; - manualInput.id = "label_" + i + "_" + n + "_" + UUID; - manualInput.max = data.audioConstraints[i].max; - manualInput.min = data.audioConstraints[i].min; - manualInput.dataset.UUID = UUID; - manualInput.dataset.track = n; - manualInput.dataset.keyname = i; - - if (i in data.currentAudioConstraints) { - input.value = data.currentAudioConstraints[i]; - manualInput.value = parseFloat(input.value); - //label.innerText = i + ": " + data.currentAudioConstraints[i]; - label.title = "Previously was: " + data.currentAudioConstraints[i]; - input.title = "Previously was: " + data.currentAudioConstraints[i]; - } else { - label.innerText = i; - } - - if (i === "height" || i === "width") { - input.title = "Hold CTRL (or cmd) to lock width and height together when changing them"; - input.min = 16; - } - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.track = n; - input.dataset.deviceId = data.deviceId; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + n + "_" + UUID; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i + "_" + n + "_" + UUID; - - if (i == "channelCount") { - input.style.display = "none"; - manualInput.style.margin = "5px 0px 9px 10px"; - } - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId); - }; - - input.onchange = function (e) { - //e.target.title = e.target.value; - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId); - }; - - audioEle.appendChild(label); - audioEle.appendChild(manualInput); - audioEle.appendChild(input); - } else if (typeof data.audioConstraints[i] === "object" && data.audioConstraints[i] !== null) { - if (i == "resizeMode") { - continue; - } - - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i + "_" + n + "_" + UUID; - label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block; padding:0;"; - var input = document.createElement("select"); - var c = document.createElement("option"); - - if (data.audioConstraints[i].length > 1) { - for (var opts in data.audioConstraints[i]) { - log(opts); - if (data.audioConstraints[i][opts] === false) { - var opt = new Option("Off", data.audioConstraints[i][opts]); - } else if (data.audioConstraints[i][opts] === true) { - var opt = new Option("On", data.audioConstraints[i][opts]); - } else { - var opt = new Option(data.audioConstraints[i][opts], data.audioConstraints[i][opts]); - } - input.options.add(opt); - if (i in data.currentAudioConstraints) { - if (data.audioConstraints[i][opts] == data.currentAudioConstraints[i]) { - opt.selected = "true"; - } - } - } - } else if (i.toLowerCase() == "torch") { - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - input.options.add(opt); - try { - if (i in data.currentAudioConstraints) { - if (data.audioConstraints[i]["torch"] == true) { - opt.selected = "true"; - } - } - } catch (e) { } - } else { - continue; - } - - input.id = "constraints_" + i + "_" + n + "_" + UUID; - input.className = "constraintCameraInput"; - input.name = input.id; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.track = n; - input.dataset.deviceId = data.deviceId; - input.dataset.UUID = UUID; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; - requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId); - log(e.target.dataset.keyname, e.target.value); - }; - audioEle.appendChild(div); - div.appendChild(label); - div.appendChild(input); - } else if (typeof data.audioConstraints[i] === "boolean") { - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i + "_" + n + "_" + UUID; - label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block; padding:0;"; - label.dataset.keyname = i; - label.dataset.track = n; - var input = document.createElement("select"); - var c = document.createElement("option"); - - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - input.options.add(opt); - - try { - if (data.audioConstraints[i] === true) { - opt.selected = "true"; - } - } catch (e) { } - - input.dataset.deviceId = data.deviceId; - input.id = "constraints_" + i + "_" + n + "_" + UUID; - input.className = "constraintCameraInput"; - input.name = input.id; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.track = n; - input.dataset.UUID = UUID; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; - requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId); - log(e.target.dataset.keyname, e.target.value); - }; - audioEle.appendChild(div); - div.appendChild(label); - div.appendChild(input); - } - } catch (e) { - errorlog(e); - } - } - - if (data.subGain !== false) { - var label = document.createElement("label"); - var i = "Gain"; - var div = document.createElement("div"); - label.id = "label_" + i + "_" + n + "_" + UUID; - label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; - - var input = document.createElement("input"); - input.min = 0; - input.max = 200; - input.value = data.subGain * 100; - input.title = "Previously was: " + parseInt(input.value); - input.type = "range"; - input.dataset.keyname = i; - input.dataset.track = n; - input.dataset.labelname = "Gain:"; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.value = data.subGain * 100; - manualInput.className = "manualInput"; - manualInput.id = "label_" + i + "_" + n + "_" + UUID; - manualInput.dataset.UUID = UUID; - manualInput.dataset.track = n; - - input.dataset.track = data.deviceId; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + n + "_" + UUID; - input.style = "display:block; width:100%;"; - input.name = input.id; - input.style.margin = "2px 0px 5px"; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track); - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track); - } - }; - - audioEle.appendChild(div); - div.appendChild(label); - div.appendChild(manualInput); - audioEle.appendChild(input); - } - - query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioEle); - } - - if (fixScrollReset) { - clearTimeout(fixScrollReset); - fixScrollReset = null; - getById("directorlayout").scrollTop = fixScrollResetValue; - } -} - -var remoteSliderTimeout = 0; - -function updateDirectorsVideo(data, UUID) { - var videoEle = document.createElement("div"); - if (data.trackLabel) { - var label = document.createElement("label"); - label.innerText = data.trackLabel; - label.style.display = "block"; - label.id = "remoteVideoLabel_" + UUID; - label.dataset.UUID = UUID; - label.classList.add("settingsLabel"); - videoEle.appendChild(label); - } - - for (var i in data.cameraConstraints) { - try { - log(i); - log(data.cameraConstraints[i]); - - if (i === "focusMode") { - continue; // I'll handle this with FocusDistance instead - } else if (i === "whiteBalanceMode") { - continue; // I'll handle this elsewhere - } else if (i === "exposureMode") { - continue; // I'll handle this elsewhere - } - - if (typeof data.cameraConstraints[i] === "object" && data.cameraConstraints[i] !== null && "max" in data.cameraConstraints[i] && "min" in data.cameraConstraints[i]) { - if (i === "aspectRatio") { - // continue; - } else if (i === "width") { - // continue; - } else if (i === "height") { - // continue; - } else if (i === "frameRate") { - // continue; - } else if (i === "latency") { - // continue; - } else if (i === "sampleRate") { - continue; - } else if (i === "channelCount") { - // continue; - } - - var manualMode = false; - var manualLabel = false; - if (i === "exposureTime") { - if (data.currentCameraConstraints["exposureMode"]) { - manualMode = document.createElement("input"); - manualMode.type = "checkbox"; - manualMode.id = "manual_" + i + "_" + UUID; - manualMode.dataset.UUID = UUID; - manualMode.dataset.keyname = "exposureMode"; - manualMode.onchange = function (e) { - var value = "manual"; - if (e.target.checked) { - value = "continuous"; - } - requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true); - //getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - //getById("label_" + e.target.dataset.keyname+ "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - }; - manualLabel = document.createElement("label"); - manualLabel.htmlFor = manualMode.id; - manualLabel.innerHTML = "Auto: "; - manualLabel.style.marginLeft = "20px"; - if (data.currentCameraConstraints["exposureMode"] == "continuous") { - manualMode.checked = true; - } - } - } else if (i === "focusDistance") { - if (data.currentCameraConstraints["focusMode"]) { - manualMode = document.createElement("input"); - manualMode.type = "checkbox"; - manualMode.id = "manual_" + i + "_" + UUID; - manualMode.dataset.UUID = UUID; - manualMode.dataset.keyname = "focusMode"; - manualMode.onchange = function (e) { - var value = "manual"; - if (e.target.checked) { - value = "continuous"; - } - requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true); - }; - manualLabel = document.createElement("label"); - manualLabel.htmlFor = manualMode.id; - manualLabel.innerHTML = "Auto: "; - manualLabel.style.marginLeft = "20px"; - if (data.currentCameraConstraints["focusMode"] == "continuous") { - manualMode.checked = true; - } - } - } else if (i === "colorTemperature") { - if (data.currentCameraConstraints["whiteBalanceMode"]) { - manualMode = document.createElement("input"); - manualMode.type = "checkbox"; - manualMode.id = "manual_" + i + "_" + UUID; - manualMode.dataset.UUID = UUID; - manualMode.dataset.keyname = "whiteBalanceMode"; - manualMode.onchange = function (e) { - var value = "manual"; - if (e.target.checked) { - value = "continuous"; - } - requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true); - }; - manualLabel = document.createElement("label"); - manualLabel.htmlFor = manualMode.id; - manualLabel.innerHTML = "Auto: "; - manualLabel.style.marginLeft = "20px"; - if (data.currentCameraConstraints["whiteBalanceMode"] == "continuous") { - manualMode.checked = true; - } - } - } - - var label = document.createElement("label"); - //label.id = "label_" + i; - label.htmlFor = "constraints_" + i + " _" + UUID; - if (i === "colorTemperature") { - label.innerText = "Color Temp:"; - } else if (i === "exposureCompensation") { - label.innerText = "Exposure Comp:"; - } else if (i === "exposureTime") { - label.innerText = "Exposure:"; - } else if (i === "focusDistance") { - label.innerText = "Focus:"; - } else { - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - } - - if (i === "zoom" || i === "pan" || i === "til") { - label.innerHTML = " " + label.innerText; - } - - var input = document.createElement("input"); - - if (i === "aspectRatio") { - input.max = 5; - input.min = 0.2; - input.step = 0.00001; - } else if (i === "exposureTime") { - input.min = data.cameraConstraints[i].min; - input.max = Math.min(data.cameraConstraints[i].max, 2000); - } else { - input.min = data.cameraConstraints[i].min; - input.max = data.cameraConstraints[i].max; - } - - if (parseFloat(input.min) == parseFloat(input.max)) { - continue; - } - - if (i in data.currentCameraConstraints) { - input.value = data.currentCameraConstraints[i]; - label.title = "Previously was: " + data.currentCameraConstraints[i]; - input.title = "Previously was: " + data.currentCameraConstraints[i]; - } - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.UUID = UUID; - input.id = "constraints_" + i + "_" + UUID; - input.name = input.id; - input.classList.add("inputConstraint"); - input.manualMode = manualMode; - - if (i === "height" || i === "width") { - input.title = "Hold CTRL (or cmd) to lock width and height together when changing them"; - input.min = 16; - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.value = parseFloat(input.value); - manualInput.className = "manualInput"; - manualInput.id = "label_" + i + "_" + UUID; - manualInput.name = manualInput.id; - manualInput.dataset.keyname = i; - manualInput.dataset.UUID = UUID; - manualInput.manualMode = manualMode; - - if ("step" in data.cameraConstraints[i]) { - manualInput.step = data.cameraConstraints[i].step; - input.step = data.cameraConstraints[i].step; - } else if (i === "aspectRatio") { - input.step = 0.000001; - manualInput.step = 0.005; - } - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (e.target.manualMode) { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); - } else { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); - } - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - //updateVideoConstraints(e.target.dataset.keyname, e.target.value); - if (CtrlPressed || e.target.manualMode) { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); - } else { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); - } - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - if (Date.now() - remoteSliderTimeout > 100) { - remoteSliderTimeout = Date.now(); - if (CtrlPressed) { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); - } else { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); - } - } - }; - - videoEle.appendChild(label); - videoEle.appendChild(manualInput); - - if (manualMode && manualLabel) { - videoEle.appendChild(manualLabel); - videoEle.appendChild(manualMode); - } - - if (i === "aspectRatio") { - var preSelectButton = document.createElement("button"); - preSelectButton.value = 16 / 9.0; - preSelectButton.innerText = "16:9"; - preSelectButton.dataset.keyname = i; - preSelectButton.dataset.UUID = UUID; - preSelectButton.className = "preSelectButton"; - preSelectButton.onclick = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); - }; - videoEle.appendChild(preSelectButton); - var preSelectButton = document.createElement("button"); - preSelectButton.value = 9 / 16.0; - preSelectButton.innerText = "9:16"; - preSelectButton.dataset.UUID = UUID; - preSelectButton.className = "preSelectButton"; - preSelectButton.dataset.keyname = i; - preSelectButton.onclick = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); - }; - videoEle.appendChild(preSelectButton); - } - - videoEle.appendChild(input); - } else if (typeof data.cameraConstraints[i] === "object" && data.cameraConstraints[i] !== null) { - if (i == "resizeMode") { - continue; - } - - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i + "_" + UUID; - label.name = label.id; - label.htmlFor = "constraints_" + i + "_" + UUID; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block; padding:0;"; - label.dataset.keyname = i; - label.dataset.UUID = UUID; - var input = document.createElement("select"); - var c = document.createElement("option"); - - if (data.cameraConstraints[i].length > 1) { - for (var opts in data.cameraConstraints[i]) { - log(opts); - if (data.cameraConstraints[i][opts] === false) { - var opt = new Option("Off", data.cameraConstraints[i][opts]); - } else if (data.cameraConstraints[i][opts] === true) { - var opt = new Option("On", data.cameraConstraints[i][opts]); - } else { - var opt = new Option(data.cameraConstraints[i][opts], data.cameraConstraints[i][opts]); - } - input.options.add(opt); - if (i in data.currentCameraConstraints) { - if (data.cameraConstraints[i][opts] == data.currentCameraConstraints[i]) { - opt.selected = "true"; - } - } - } - } else if (i.toLowerCase() == "torch") { - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - input.options.add(opt); - try { - if (i in data.currentCameraConstraints) { - if (data.cameraConstraints[i]["torch"] == true) { - opt.selected = "true"; - } - } - } catch (e) { } - } else { - continue; - } - - input.id = "constraints_" + i + "_" + UUID; - input.className = "constraintCameraInput"; - input.name = input.id; - input.dataset.UUID = UUID; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname+ "_" + e.target.dataset.UUID).innerText =e.target.dataset.keyname+": "+e.target.value; - //updateVideoConstraints(e.target.dataset.keyname, e.target.value); - if (CtrlPressed) { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); - } else { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); - } - log(e.target.dataset.keyname, e.target.value); - }; - videoEle.appendChild(div); - div.appendChild(label); - div.appendChild(input); - } else if (typeof data.cameraConstraints[i] === "boolean") { - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i + "_" + UUID; - label.name = label.id; - label.htmlFor = "constraints_" + i + "_" + UUID; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block; padding:0;"; - label.dataset.keyname = i; - label.dataset.UUID = UUID; - var input = document.createElement("select"); - var c = document.createElement("option"); - - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - input.options.add(opt); - - try { - if (data.audioConstraints[i] === true) { - opt.selected = "true"; - } - } catch (e) { } - - input.id = "constraints_" + i + "_" + UUID; - input.className = "constraintCameraInput"; - input.name = input.id; - input.style = "display:inline; padding:2px;"; - input.dataset.UUID = UUID; - input.dataset.keyname = i; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname+ "_" + e.target.dataset.UUID).innerText =e.target.dataset.keyname+": "+e.target.value; - //updateVideoConstraints(e.target.dataset.keyname, e.target.value); - if (CtrlPressed) { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); - } else { - requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); - } - log(e.target.dataset.keyname, e.target.value); - }; - videoEle.appendChild(div); - div.appendChild(label); - div.appendChild(input); - } - } catch (e) { - errorlog(e); - } - } - - query("#container_" + UUID + " .advancedVideoSettings").innerHTML = ""; - query("#container_" + UUID + " .advancedVideoSettings").appendChild(videoEle); - query("#container_" + UUID + " .advancedVideoSettings").classList.remove("hidden"); - - if (fixScrollReset) { - clearTimeout(fixScrollReset); - fixScrollReset = null; - getById("directorlayout").scrollTop = fixScrollResetValue; - } -} - -/////// -function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); -} - -function listAudioSettings() { - getById("popupSelector_constraints_audio").innerHTML = ""; - - var tracks = session.streamSrc.getAudioTracks(); - if (!tracks.length) { - warnlog("session.streamSrc contains no audio tracks"); - return; - } - - for (var ii = 0; ii < tracks.length; ii++) { - track0 = tracks[ii]; - if (track0.getCapabilities) { - session.audioConstraints = track0.getCapabilities(); - } else if (Firefox) { - // let's pretend like Firefox doesn't actually suck - session.audioConstraints = { - autoGainControl: [true, false], - // "channelCount": { - // "max": 2, - // "min": 1 - // }, - // "deviceId": "default", - echoCancellation: [true, false], - // "groupId": "a3cbdec54a9b6ed473fd950415626f7e76f9d1b90f8c768faab572175a355a17", - // "latency": { - // "max": 0.01, - // "min": 0.01 - // }, - noiseSuppression: [true, false] - // "sampleRate": { - // "max": 48000, - // "min": 48000 - // }, - // "sampleSize": { - // "max": 16, - // "min": 16 - /// } - }; - } - - try { - if (track0.getSettings) { - session.currentAudioConstraints = track0.getSettings(); - - if (!session.stereo) { - try { - delete session.currentAudioConstraints.channelCount; - delete session.audioConstraints.channelCount; - } catch (e) { } - } else if (session.audioInputChannels && session.audioInputChannels == 1) { - // this is pretty hacky, but it gets around not being able to actually set 1-channel. Not sure why. - session.currentAudioConstraints.channelCount = 1; - } - } - } catch (e) { - errorlog(e); - } - - ////// - if (ii == 0) { - for (var webAudio in session.webAudios) { - if (session.webAudios[webAudio].gainNode) { - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - var div = document.createElement("div"); - var label = document.createElement("label"); - var i = "masterGain"; - //label.id = "label_" + i; - label.htmlFor = "constraints_" + i; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block;"; - - var input = document.createElement("input"); - input.min = 0; - input.max = 200; - - input.dataset.deviceid = track0.id; // pointless - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = label.innerHTML; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - - input.value = session.webAudios[webAudio].gainNode.gain.value * 100; - //label.innerHTML += " " + parseInt(session.webAudios[webAudio].gainNode.gain.value * 100); - input.title = parseInt(input.value); - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.dataset.deviceid = track0.id; - manualInput.dataset.labelname = label.innerHTML; - manualInput.value = session.webAudios[webAudio].gainNode.gain.value * 100; - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMainGain(e.target.value); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMainGain(e.target.value); - e.target.title = e.target.value; - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMainGain(e.target.value); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(div); - div.appendChild(label); - div.appendChild(manualInput); - div.appendChild(input); - break; - } - } - } - - if (session.micDelay !== false && ii == 0) { - // ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node) - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - var label = document.createElement("label"); - var i = "micDelay"; - label.htmlFor = "constraints_" + i; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + " (ms):"; - - var input = document.createElement("input"); - input.min = 0; - input.max = 500; - - input.dataset.deviceid = track0.id; // pointless, for now - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = label.innerHTML; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - - for (var webAudio in session.webAudios) { - if (session.webAudios[webAudio].micDelay) { - // session.webAudios[waid].micDelay.delayTime.setValueAtTime - input.value = session.webAudios[webAudio].micDelay.delayTime.value * 1000; - label.innerHTML += " " + parseInt(session.webAudios[webAudio].micDelay.delayTime.value * 1000); - input.title = input.value; - break; - } - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.dataset.labelname = label.innerHTML; - manualInput.value = parseFloat(input.value); - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMicDelay(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMicDelay(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMicDelay(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(manualInput); - getById("popupSelector_constraints_audio").appendChild(input); - } - - // Mic Panning - local settings UI - if (session.micPanning !== false && ii == 0) { - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - var label = document.createElement("label"); - var i = "micPanning"; - label.htmlFor = "constraints_" + i; - label.innerText = "Mic Pan:"; - - var input = document.createElement("input"); - input.min = 0; - input.max = 180; - - input.dataset.deviceid = track0.id; - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = label.innerHTML; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - - input.value = session.micPanning !== false ? session.micPanning : 90; - label.innerHTML += " " + parseInt(input.value); - input.title = input.value + " (0=L, 90=C, 180=R)"; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.dataset.deviceid = track0.id; - manualInput.dataset.labelname = label.innerHTML; - manualInput.value = parseInt(input.value); - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMicPanning(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMicPanning(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMicPanning(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(manualInput); - getById("popupSelector_constraints_audio").appendChild(input); - } - - if (session.lowcut && ii == 0) { - // ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node) - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - var label = document.createElement("label"); - var i = "Low_Cut"; - label.htmlFor = "constraints_" + i; - label.innerText = "Low Cut:"; - - var input = document.createElement("input"); - input.min = 50; - input.max = 400; - - input.dataset.deviceid = track0.id; // pointless - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = label.innerHTML; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - - for (var webAudio in session.webAudios) { - if (session.webAudios[webAudio].lowcut1) { - input.value = session.webAudios[webAudio].lowcut1.frequency.value; - label.innerHTML += " " + session.webAudios[webAudio].lowcut1.frequency.value; - input.title = input.value; - break; - } - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.dataset.labelname = label.innerHTML; - manualInput.value = parseFloat(input.value); - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeLowCut(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeLowCut(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeLowCut(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(manualInput); - getById("popupSelector_constraints_audio").appendChild(input); - } - - if (session.equalizer && ii == 0) { - // ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node) - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - var label = document.createElement("label"); - var i = "Low_EQ"; - //label.id = "label_" + i; - label.htmlFor = "constraints_" + i; - label.innerHTML = "Low EQ:"; - - var input = document.createElement("input"); - input.min = -50; - input.max = 50; - - input.dataset.deviceid = track0.id; // pointless - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = label.innerHTML; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - - for (var webAudio in session.webAudios) { - if (session.webAudios[webAudio].lowEQ) { - input.value = session.webAudios[webAudio].lowEQ.gain.value; - label.innerHTML += " " + session.webAudios[webAudio].lowEQ.gain.value; - input.title = input.value; - } - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.dataset.labelname = label.innerHTML; - manualInput.value = parseFloat(input.value); - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeLowEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeLowEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeLowEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(manualInput); - getById("popupSelector_constraints_audio").appendChild(input); - // - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - var label = document.createElement("label"); - var i = "Mid_EQ"; - //label.id = "label_" + i; - label.htmlFor = "constraints_" + i; - label.innerHTML = "Mid EQ:"; - - var input = document.createElement("input"); - input.min = -50; - input.max = 50; - - input.dataset.deviceid = track0.id; // pointless - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = label.innerHTML; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - - for (var webAudio in session.webAudios) { - if (session.webAudios[webAudio].midEQ) { - input.value = session.webAudios[webAudio].midEQ.gain.value; - label.innerHTML += " " + session.webAudios[webAudio].midEQ.gain.value; - input.title = input.value; - } - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.dataset.labelname = label.innerHTML; - manualInput.value = parseFloat(input.value); - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMidEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMidEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeMidEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(manualInput); - getById("popupSelector_constraints_audio").appendChild(input); - // - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - var label = document.createElement("label"); - var i = "High_EQ"; - //label.id = "label_" + i; - label.htmlFor = "constraints_" + i; - label.innerHTML = "High EQ:"; - - var input = document.createElement("input"); - input.min = -50; - input.max = 50; - - input.dataset.deviceid = track0.id; // pointless - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = label.innerHTML; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - - for (var webAudio in session.webAudios) { - if (session.webAudios[webAudio].highEQ) { - input.value = session.webAudios[webAudio].highEQ.gain.value; - label.innerHTML += " " + session.webAudios[webAudio].highEQ.gain.value; - input.title = input.value; - } - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.dataset.labelname = label.innerHTML; - manualInput.value = parseFloat(input.value); - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeHighEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeHighEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - changeHighEQ(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(label); - getById("popupSelector_constraints_audio").appendChild(manualInput); - getById("popupSelector_constraints_audio").appendChild(input); - } - - if (session.noisegate !== false && ii == 0) { - for (var webAudio in session.webAudios) { - if (session.webAudios[webAudio].gatingNode) { - var div = document.createElement("div"); - var label = document.createElement("label"); - - var i = "noiseGating"; - - label.id = "label_" + i + "_" + ii; - label.htmlFor = "constraints_" + i + "_" + ii; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block;"; - label.dataset.keyname = i; - label.title = "This will reduce the gain ~80% when there is no one talking loudly"; - var input = document.createElement("select"); - var c = document.createElement("option"); - - input.dataset.deviceid = track0.id; - - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - if (session.noisegate) { - opt.selected = "true"; - } - input.options.add(opt); - - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - input.id = "constraints_" + i + "_" + ii; - input.className = "constraintCameraInput"; - input.name = "constraints_" + i + "_" + ii; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - if (e.target.value == "false") { - session.noisegate = null; - } else if (e.target.value == "true") { - session.noisegate = true; - } else { - session.noisegate = e.target.value; - } - if (!session.noisegate) { - changeGatingGain(100); - changeGatingGain(100, 3100); - } - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(div); - div.appendChild(label); - div.appendChild(input); - break; - } - } - } - - //////// - if (tracks.length > 1) { - var label = document.createElement("h4"); - label.innerHTML = track0.label; - label.style = "text-shadow: 0 0 10px #fff3;margin:0px 0 10px 0"; - if (ii > 0) { - label.style = "text-shadow: 0 0 10px #fff3;margin:40px 0 10px 0"; - } - getById("popupSelector_constraints_audio").appendChild(label); - } - - for (var i in session.audioConstraints) { - try { - log(i); - log(session.audioConstraints[i]); - - if (typeof session.audioConstraints[i] === "object" && session.audioConstraints[i] !== null && "max" in session.audioConstraints[i] && "min" in session.audioConstraints[i]) { - if (i === "aspectRatio") { - continue; - } else if (i === "width") { - continue; - } else if (i === "height") { - continue; - } else if (i === "frameRate") { - continue; - } else if (i === "latency") { - // continue; - } else if (i === "sampleRate") { - //continue; - } else if (i === "sampleSize") { - //continue; - } else if (i === "channelCount") { - if (!(session.stereo && session.stereo != 3)) { - // not stereo - continue; - } - } else if (!session.disableWebAudio && i === "volume") { - continue; - } - - var label = document.createElement("label"); - //label.id = "label_" + i + "_"+ii; - label.htmlFor = "constraints_" + i + "_" + ii; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - - var input = document.createElement("input"); - input.min = session.audioConstraints[i].min; - input.max = session.audioConstraints[i].max; - - input.dataset.deviceid = track0.id; - - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - - if ("step" in session.audioConstraints[i]) { - input.step = session.audioConstraints[i].step; - manualInput.step = session.audioConstraints[i].step; - } else if ("volume" == i) { - input.step = 0.01; - manualInput.step = 0.01; - } - - if (i in session.currentAudioConstraints) { - input.value = parseFloat(session.currentAudioConstraints[i]); - label.title = "Previously was: " + session.currentAudioConstraints[i]; - input.title = "Previously was: " + session.currentAudioConstraints[i]; - } - - if (i === "height" || i === "width") { - input.title = "Hold CTRL (or cmd) to lock width and height together when changing them"; - input.min = 16; - } else if (i == "sampleRate") { - label.title = "Audio typically gets resampled to 48-kHz"; - } - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.track = ii; - - input.id = "constraints_" + i + "_" + ii; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i + "_" + ii; - - manualInput.dataset.keyname = i; - manualInput.dataset.track = ii; - manualInput.dataset.deviceid = track0.id; - - manualInput.className = "manualInput"; - manualInput.id = "label_" + i + "_" + ii; - manualInput.max = session.audioConstraints[i].max; - manualInput.min = session.audioConstraints[i].min; - - manualInput.value = parseFloat(session.currentAudioConstraints[i]); - - manualInput.onchange = function (e) { - try { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track).value = parseFloat(e.target.value); - applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - } catch (e) { - errorlog(e); - } - }; - - input.onchange = function (e) { - try { - getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track).value = parseFloat(e.target.value); - applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - } catch (e) { - errorlog(e); - } - }; - - // not sure if I should include "oninput" as well? Probably not needed. - - var div = document.createElement("div"); - if (parseFloat(input.min) == parseFloat(input.max)) { - manualInput.disabled = true; - manualInput.title = "Only one option available, so can't be changed"; - label.title = "Only one option available, so can't be changed"; - div.appendChild(label); - div.appendChild(manualInput); - getById("popupSelector_constraints_audio").appendChild(div); - } else { - div.appendChild(label); - div.appendChild(manualInput); - div.appendChild(input); - getById("popupSelector_constraints_audio").appendChild(div); - } - } else if (typeof session.audioConstraints[i] === "object" && session.audioConstraints[i] !== null) { - if (i == "resizeMode") { - continue; - } - - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i + "_" + ii; - label.htmlFor = "constraints_" + i + "_" + ii; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block;"; - label.dataset.keyname = i; - - var input = document.createElement("select"); - var c = document.createElement("option"); - - if (session.audioConstraints[i].length == 2) { - for (var opts in session.audioConstraints[i]) { - log(opts); - if (session.audioConstraints[i][opts] === true) { - var opt = new Option("On", session.audioConstraints[i][opts]); - } else if (session.audioConstraints[i][opts] === false) { - var opt = new Option("Off", session.audioConstraints[i][opts]); - } else { - var opt = new Option(session.audioConstraints[i][opts], session.audioConstraints[i][opts]); - } - input.options.add(opt); - if (i in session.currentAudioConstraints) { - if (session.audioConstraints[i][opts] == session.currentAudioConstraints[i]) { - opt.selected = "true"; - } - } - } - } else if (session.audioConstraints[i].length > 1) { - for (var opts in session.audioConstraints[i]) { - log(opts); - var opt = new Option(session.audioConstraints[i][opts], session.audioConstraints[i][opts]); - input.options.add(opt); - if (i in session.currentAudioConstraints) { - if (session.audioConstraints[i][opts] == session.currentAudioConstraints[i]) { - opt.selected = "true"; - } - } - } - } else if (i.toLowerCase() == "torch") { - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - input.options.add(opt); - } else { - continue; - } - - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - input.id = "constraints_" + i + "_" + ii; - input.className = "constraintCameraInput"; - input.name = "constraints_" + i + "_" + ii; - input.dataset.deviceid = track0.id; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid); - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - getById("popupSelector_constraints_audio").appendChild(div); - div.appendChild(label); - div.appendChild(input); - } else if (typeof session.audioConstraints[i] === "boolean") { - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i + "_" + ii; - label.htmlFor = "constraints_" + i + "_" + ii; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block;"; - label.dataset.keyname = i; - var input = document.createElement("select"); - var c = document.createElement("option"); - - input.dataset.deviceid = track0.id; - - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - input.options.add(opt); - - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - - input.id = "constraints_" + i + "_" + ii; - input.className = "constraintCameraInput"; - input.name = "constraints_" + i + "_" + ii; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; - applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid); - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - getById("popupSelector_constraints_audio").appendChild(div); - div.appendChild(label); - div.appendChild(input); - } - } catch (e) { - errorlog(e); - } - } - - if (tracks.length > 1) { - for (var webAudio in session.webAudios) { - if (session.webAudios[webAudio].subGainNodes && track0.id in session.webAudios[webAudio].subGainNodes) { - if (getById("popupSelector_constraints_audio").style.display == "none") { - getById("advancedOptionsAudio").style.display = "inline-flex"; - } - var div = document.createElement("div"); - var label = document.createElement("label"); - var i = "Gain"; - label.id = "label_" + i + "_" + track0.id; - label.htmlFor = "constraints_" + i + "_" + track0.id; - label.innerText = "Gain:"; - label.style = "display:inline-block; padding:0;margin-top: 15px"; - - var input = document.createElement("input"); - input.min = 0; - input.max = 200; - - input.dataset.deviceid = track0.id; // pointless - - input.type = "range"; - input.dataset.keyname = i; - input.dataset.labelname = label.innerHTML; - input.id = "constraints_" + i + "_" + track0.id; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i + "_" + track0.id; - - input.value = session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100; - //label.innerText += " " + parseInt(session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100); - input.title = parseInt(input.value); - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - manualInput.dataset.deviceid = track0.id; - manualInput.dataset.labelname = label.innerHTML; - manualInput.value = session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100; - manualInput.className = "manualInput"; - manualInput.id = "manualInput_" + i + "_" + track0.id; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value); - //getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value); - getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value); - changeSubGain(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - //getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value); - getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value); - changeSubGain(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - }; - - input.onchange = function (e) { - //getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value); - getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value); - changeSubGain(e.target.value, e.target.dataset.deviceid); - e.target.title = e.target.value; - pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_audio").appendChild(div); - div.appendChild(label); - div.appendChild(manualInput); - div.appendChild(input); - break; - } - } - } - } -} - -function applyAudioHack(constraint, value = null, deviceid = "default") { - if (value == parseFloat(value)) { - value = parseFloat(value); - if (constraint == "channelCount") { - session.audioInputChannels = value; - } - value = { - exact: value - }; - } else if (value == "true") { - value = true; - } else if (value == "false") { - value = false; - } - - try { - var tracks = session.streamSrc.getAudioTracks(); - if (!tracks.length) { - warnlog("session.streamSrc contains no audio tracks"); - return; - } - - var track0 = tracks[0]; - for (var ii = 0; ii < tracks.length; ii++) { - if (tracks[ii].id == deviceid) { - track0 = tracks[ii]; - break; - } - } - - if (track0.getCapabilities) { - session.audioConstraints = track0.getCapabilities(); - } else if (Firefox) { - // Firefox fallback - session.audioConstraints = { - autoGainControl: [true, false], - deviceId: deviceid, - echoCancellation: [true, false], - noiseSuppression: [true, false] - }; - } - log(session.audioConstraints); - - if (track0.getSettings) { - session.currentAudioConstraints = track0.getSettings(); - } - } catch (e) { - warnlog("Error getting audio track info"); - errorlog(e); - return; - } - - var new_constraints = Object.assign({}, session.currentAudioConstraints, { - [constraint]: value - }); - new_constraints = { - audio: new_constraints, - video: false - }; - log("new constraints"); - log(new_constraints); - activatedPreview = false; - - enumerateDevices() - .then(gotDevices2) - .then(function () { - grabAudio("#audioSource3", null, new_constraints, false, saveAudioResult); - }); -} - -// saveAudioResult is disabled but keeping structure for potential future use -function saveAudioResult() { - return false; // DISABLED: we can't load audio settings, so no point in saving them - - /* Future implementation when audio settings can be loaded: - if (!session.streamSrc) { - return; - } - var tracks = session.streamSrc.getAudioTracks(); - if (!tracks.length) { - return; - } - var track0 = tracks[0]; - session.currentAudioConstraints = track0.getSettings(); - if (session.currentAudioConstraints.deviceId) { - setStorage("audio_" + session.currentAudioConstraints.deviceId, session.currentAudioConstraints); - } - */ -} - -function listCameraSettings() { - getById("popupSelector_constraints_video").innerHTML = ""; - - if (session.controlRoomBitrate === true) { - session.controlRoomBitrate = session.totalRoomBitrate; - } - - if (session.roomid && session.view !== "" && session.controlRoomBitrate !== false) { - log("LISTING OPTION FOR BITRATE CONTROL"); - var i = "Room Video Bitrate (kbps)"; - var label = document.createElement("label"); - - label.htmlFor = "constraints_" + i; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.title = "If you're on a slow network, you can improve frame rate and audio quality by reducing the amount of video data that others send you"; - - var input = document.createElement("input"); - input.min = 0; - input.max = parseInt(session.totalRoomBitrate * 1.5); - - if (getById("popupSelector_constraints_video").style.display == "none") { - getById("advancedOptionsCamera").style.display = "inline-flex"; - } - - input.value = parseInt(session.controlRoomBitrate); - label.innerHTML = i + ": "; - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.value = session.controlRoomBitrate; - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - input.type = "range"; - input.dataset.keyname = i; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - input.title = "If you're on a slow network, you can improve frame rate and audio quality by reducing the amount of video data that others send you"; - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - if (e.target.value > session.totalRoomBitrate) { - return; - } else { - session.controlRoomBitrate = parseInt(e.target.value); - } - updateMixer(); - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - if (e.target.value > session.totalRoomBitrate) { - return; - } else { - session.controlRoomBitrate = parseInt(e.target.value); - } - updateMixer(); - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - getById("popupSelector_constraints_video").appendChild(label); - getById("popupSelector_constraints_video").appendChild(manualInput); - getById("popupSelector_constraints_video").appendChild(input); - } - try { - var track0 = session.streamSrc.getVideoTracks(); - if (track0.length) { - track0 = track0[0]; - if (track0.getCapabilities) { - session.cameraConstraints = track0.getCapabilities(); - } else { - session.cameraConstraints = {}; // probably firefox... - } - log(session.cameraConstraints); - } - } catch (e) { - errorlog(e); - return; - } - - try { - if (track0.getSettings) { - session.currentCameraConstraints = track0.getSettings(); - if (session.mobile) { - if (screen && screen.orientation && screen.orientation.type) { - if (!screen.orientation.type.includes("portrait")) { - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - } else if (!window.matchMedia("(orientation: portrait)").matches) { - if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - } - } else { - session.currentCameraConstraints = {}; - } - } catch (e) { - errorlog(e); - } - - var orderedConstraints = {}; - if (session.cameraConstraints.torch) { - orderedConstraints.torch = session.cameraConstraints.torch; - } - if (session.cameraConstraints.aspectRatio) { - orderedConstraints.aspectRatio = session.cameraConstraints.aspectRatio; - } - if (session.cameraConstraints.width) { - orderedConstraints.width = session.cameraConstraints.width; - } - if (session.cameraConstraints.height) { - orderedConstraints.height = session.cameraConstraints.height; - } - if (session.cameraConstraints.zoom) { - orderedConstraints.zoom = session.cameraConstraints.zoom; - } - for (var key in session.cameraConstraints) { - if (session.cameraConstraints.hasOwnProperty(key) && key !== "width" && key !== "height") { - orderedConstraints[key] = session.cameraConstraints[key]; - } - } - session.cameraConstraints = orderedConstraints; - - for (var i in session.cameraConstraints) { - try { - log(i); - log(session.cameraConstraints[i]); - - if (i === "focusMode") { - continue; // I'll handle this with FocusDistance instead - } else if (i === "whiteBalanceMode") { - continue; // I'll handle this elsewhere - } else if (i === "exposureMode") { - continue; // I'll handle this elsewhere - } - - if (typeof session.cameraConstraints[i] === "object" && session.cameraConstraints[i] !== null && "max" in session.cameraConstraints[i] && "min" in session.cameraConstraints[i]) { - var manualMode = false; - var manualLabel = false; - if (i === "exposureTime") { - if (session.currentCameraConstraints["exposureMode"]) { - manualMode = document.createElement("input"); - manualMode.type = "checkbox"; - manualMode.id = "manual_" + i; - manualMode.dataset.keyname = "exposureMode"; - manualMode.onchange = function (e) { - var value = "manual"; - if (e.target.checked) { - value = "continuous"; - } - if (CtrlPressed) { - updateCameraConstraints(e.target.dataset.keyname, value, true, false); - } else { - updateCameraConstraints(e.target.dataset.keyname, value, false, false); - } - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value }); - }; - manualLabel = document.createElement("label"); - manualLabel.htmlFor = manualMode.id; - manualLabel.innerHTML = "Auto: "; - manualLabel.style.marginLeft = "20px"; - if (session.currentCameraConstraints["exposureMode"] == "continuous") { - manualMode.checked = true; - } - } - } else if (i === "focusDistance") { - if (session.currentCameraConstraints["focusMode"]) { - manualMode = document.createElement("input"); - manualMode.type = "checkbox"; - manualMode.id = "manual_" + i; - manualMode.dataset.keyname = "focusMode"; - manualMode.onchange = function (e) { - var value = "manual"; - if (e.target.checked) { - value = "continuous"; - } - if (CtrlPressed) { - updateCameraConstraints(e.target.dataset.keyname, value, true, false); - } else { - updateCameraConstraints(e.target.dataset.keyname, value, false, false); - } - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value }); - }; - manualLabel = document.createElement("label"); - manualLabel.htmlFor = manualMode.id; - manualLabel.innerHTML = "Auto: "; - manualLabel.style.marginLeft = "20px"; - if (session.currentCameraConstraints["focusMode"] == "continuous") { - manualMode.checked = true; - } - } - } else if (i === "colorTemperature") { - if (session.currentCameraConstraints["whiteBalanceMode"]) { - manualMode = document.createElement("input"); - manualMode.type = "checkbox"; - manualMode.id = "manual_" + i; - manualMode.dataset.keyname = "whiteBalanceMode"; - manualMode.onchange = function (e) { - var value = "manual"; - if (e.target.checked) { - value = "continuous"; - } - if (CtrlPressed) { - updateCameraConstraints(e.target.dataset.keyname, value, true, false); - } else { - updateCameraConstraints(e.target.dataset.keyname, value, false, false); - } - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value }); - }; - manualLabel = document.createElement("label"); - manualLabel.htmlFor = manualMode.id; - manualLabel.innerHTML = "Auto: "; - manualLabel.style.marginLeft = "20px"; - if (session.currentCameraConstraints["whiteBalanceMode"] == "continuous") { - manualMode.checked = true; - } - } - } - - var label = document.createElement("label"); - label.htmlFor = "constraints_" + i; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - - var input = document.createElement("input"); - input.min = parseFloat(session.cameraConstraints[i].min); - - if (i === "aspectRatio") { - input.max = 5; - input.min = 0.2; - } else if (i === "exposureTime") { - input.min = parseFloat(session.cameraConstraints[i].min); - input.max = Math.min(parseFloat(session.cameraConstraints[i].max), 2000); - } else { - input.min = parseFloat(session.cameraConstraints[i].min); - input.max = parseFloat(session.cameraConstraints[i].max); - } - - if (parseFloat(input.min) == parseFloat(input.max)) { - continue; - } - - if (getById("popupSelector_constraints_video").style.display == "none") { - getById("advancedOptionsCamera").style.display = "inline-flex"; - } - - var manualInput = document.createElement("input"); - manualInput.type = "number"; - manualInput.dataset.keyname = i; - - manualInput.className = "manualInput"; - manualInput.id = "label_" + i; - - if ("step" in session.cameraConstraints[i]) { - input.step = session.cameraConstraints[i].step; - manualInput.step = session.cameraConstraints[i].step; - } else if (i === "aspectRatio") { - input.step = 0.000001; - manualInput.step = 0.005; - } - - if (i in session.currentCameraConstraints) { - input.value = parseFloat(session.currentCameraConstraints[i]); - //label.innerHTML = i + ": " + session.currentCameraConstraints[i]; - manualInput.value = parseFloat(session.currentCameraConstraints[i]); - label.title = "Previously was: " + session.currentCameraConstraints[i]; - input.title = "Previously was: " + session.currentCameraConstraints[i]; - } else { - label.innerHTML = i; - } - if (i === "height" || i === "width") { - input.title = "Hold CTRL (or cmd) to lock width and height together when changing them"; - input.min = 16; - } - - input.type = "range"; - input.dataset.keyname = i; - input.id = "constraints_" + i; - input.style = "display:block; width:100%;"; - input.name = "constraints_" + i; - - // on manualInput.change = .. update the input field! gotta riprocate - - manualInput.onchange = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false); - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - input.oninput = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - if (CtrlPressed) { - updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false); - } else { - updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false); - } - }; - - input.onchange = function (e) { - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - if (CtrlPressed) { - updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false); - } else { - updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false); - } - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - - getById("popupSelector_constraints_video").appendChild(label); - getById("popupSelector_constraints_video").appendChild(manualInput); - if (manualMode && manualLabel) { - getById("popupSelector_constraints_video").appendChild(manualLabel); - getById("popupSelector_constraints_video").appendChild(manualMode); - } - - if (i === "aspectRatio") { - var preSelectButton = document.createElement("button"); - preSelectButton.value = 16 / 9.0; - preSelectButton.innerText = "16:9"; - preSelectButton.dataset.keyname = i; - preSelectButton.className = "preSelectButton"; - preSelectButton.onclick = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false); - }; - getById("popupSelector_constraints_video").appendChild(preSelectButton); - var preSelectButton = document.createElement("button"); - preSelectButton.value = 9 / 16.0; - preSelectButton.innerText = "9:16"; - preSelectButton.className = "preSelectButton"; - preSelectButton.dataset.keyname = i; - preSelectButton.onclick = function (e) { - getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); - updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false); - }; - getById("popupSelector_constraints_video").appendChild(preSelectButton); - } - - getById("popupSelector_constraints_video").appendChild(input); - } else if (typeof session.cameraConstraints[i] === "object" && session.cameraConstraints[i] !== null) { - if (i == "resizeMode") { - continue; - } - - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i; - label.htmlFor = "constraints_" + i; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block;"; - label.dataset.keyname = i; - var input = document.createElement("select"); - - if (session.cameraConstraints[i].length > 1) { - var included = false; - for (var opts in session.cameraConstraints[i]) { - log(opts); - var opt = new Option(session.cameraConstraints[i][opts], session.cameraConstraints[i][opts]); - input.options.add(opt); - if (i in session.currentCameraConstraints) { - if (session.cameraConstraints[i][opts] == session.currentCameraConstraints[i]) { - opt.selected = "true"; - included = true; - } - } - } - if (!included) { - if (i in session.currentCameraConstraints) { - var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]); - input.options.add(opt); - opt.selected = "true"; - } - } - } else if (i.toLowerCase() == "torch") { - warnlog("TORCH"); - var opt = new Option("Off", false); - input.options.add(opt); - opt = new Option("On", true); - input.options.add(opt); - try { - if (session.currentCameraConstraints[i]) { - opt.selected = "selected"; - } - } catch (e) { } - } else if (session.cameraConstraints[i].length && "continuous" == session.cameraConstraints[i][0]) { - var opt = new Option("continuous", "continuous"); - input.options.add(opt); - if (i in session.currentCameraConstraints) { - if ("continuous" == session.currentCameraConstraints[i]) { - opt.selected = "true"; - var opt = new Option("manual", "manual"); - input.options.add(opt); - var opt = new Option("none", "none"); - input.options.add(opt); - } else { - var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]); - input.options.add(opt); - opt.selected = "true"; - if (session.currentCameraConstraints[i] == "none") { - var opt = new Option("manual", "manual"); - input.options.add(opt); - } else { - var opt = new Option("none", "none"); - input.options.add(opt); - } - } - } else { - opt.selected = "true"; - var opt = new Option("manual", "manual"); - input.options.add(opt); - var opt = new Option("none", "none"); - input.options.add(opt); - } - } else if (session.cameraConstraints[i].length && "manual" == session.cameraConstraints[i][0]) { - var opt = new Option("manual", "manual"); - input.options.add(opt); - if (i in session.currentCameraConstraints) { - if ("manual" == session.currentCameraConstraints[i]) { - opt.selected = "true"; - var opt = new Option("continuous", "continuous"); - input.options.add(opt); - var opt = new Option("none", "none"); - input.options.add(opt); - } else { - var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]); - input.options.add(opt); - opt.selected = "true"; - if (session.currentCameraConstraints[i] == "none") { - var opt = new Option("continuous", "continuous"); - input.options.add(opt); - } else { - var opt = new Option("none", "none"); - input.options.add(opt); - } - } - } else { - opt.selected = "true"; - var opt = new Option("continuous", "continuous"); - input.options.add(opt); - var opt = new Option("none", "none"); - input.options.add(opt); - } - } else { - continue; - } - - if (getById("popupSelector_constraints_video").style.display == "none") { - getById("advancedOptionsCamera").style.display = "inline-flex"; - } - - input.id = "constraints_" + i; - input.className = "constraintCameraInput"; - input.name = "constraints_" + i; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; - if (CtrlPressed) { - updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false); - } else { - updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false); - } - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - getById("popupSelector_constraints_video").appendChild(div); - div.appendChild(label); - div.appendChild(input); - } else if (typeof session.cameraConstraints[i] === "boolean") { - var div = document.createElement("div"); - var label = document.createElement("label"); - label.id = "label_" + i; - label.htmlFor = "constraints_" + i; - label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; - label.style = "display:inline-block;"; - label.dataset.keyname = i; - var input = document.createElement("select"); - - var opt = new Option("Off", "false"); - input.options.add(opt); - - opt = new Option("On", "true"); - input.options.add(opt); - if (session.currentCameraConstraints[i]) { - opt.selected = "true"; - } - - if (getById("popupSelector_constraints_video").style.display == "none") { - getById("advancedOptionsCamera").style.display = "inline-flex"; - } - - input.id = "constraints_" + i; - input.className = "constraintCameraInput"; - input.name = "constraints_" + i; - input.style = "display:inline; padding:2px;"; - input.dataset.keyname = i; - input.dataset.chosen = input.value; - input.onchange = function (e) { - this.dataset.chosen = this.value; - //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; - if (CtrlPressed) { - updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false); - } else { - updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false); - } - pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); - }; - getById("popupSelector_constraints_video").appendChild(div); - div.appendChild(label); - div.appendChild(input); - } - } catch (e) { - errorlog(e); - } - } - - if (session.currentCameraConstraints.deviceId) { - if (getStorage("camera_" + session.currentCameraConstraints.deviceId)) { - var button = document.createElement("button"); - button.innerHTML = "Reset video settings to default"; - button.style.display = "block"; - button.style.padding = "10px 5px"; - button.dataset.deviceId = session.currentCameraConstraints.deviceId; - button.onclick = function () { - var deviceId = this.dataset.deviceId; - var cameraSettings = getStorage("camera_" + deviceId); - var constraints = {}; - var resetResolution = false; - var failed = false; - if (cameraSettings["default"]) { - if (cameraSettings["current"]) { - for (var i in cameraSettings["default"]) { - if (i == "groupId") { - continue; - } else if (i === "aspectRatio") { - // do not load from storage; causes issues - continue; - } else if (i === "width") { - // continue; - } else if (i === "height") { - // continue; - } else { - // if I include any of these, it will complain about mixing types and fail - if (i in cameraSettings["current"]) { - if (cameraSettings["current"][i] != cameraSettings["default"][i]) { - track0 - .applyConstraints({ - advanced: [{ [i]: cameraSettings["default"][i] }] - }) - .then(() => { }) - .catch(e => { - errorlog("Failed to reset to defaults"); - failed = true; - }); - } - } - continue; - } - if (i in cameraSettings["current"]) { - if (cameraSettings["current"][i] != cameraSettings["default"][i]) { - if (i in session.cameraConstraints) { - if ("min" in session.cameraConstraints[i]) { - if (session.cameraConstraints[i].min > cameraSettings["default"][i]) { - continue; - } - } - if ("max" in session.cameraConstraints[i]) { - if (session.cameraConstraints[i].max < cameraSettings["default"][i]) { - continue; - } - } - } - constraints[i] = cameraSettings["default"][i]; - if (i == "width" || i == "height" || i == "aspectRatio") { - resetResolution = true; - } - } - } - } - } - } - warnlog(constraints); - if (Object.keys(constraints).length) { - track0 - .applyConstraints({ - advanced: [constraints] - }) - .then(() => { - if (!failed) { - removeStorage("camera_" + deviceId); - } - listCameraSettings(); - if (resetResolution) { - session.setResolution(); // this will reset scaling for all viewers of this stream - } - }) - .catch(e => { - errorlog("Failed to reset to defaults"); - errorlog(e); - }); - } else if (!failed) { - removeStorage("camera_" + deviceId); - listCameraSettings(); - } - }; - - getById("popupSelector_constraints_video").appendChild(button); - } - } -} - -// Audio settings application -function applySavedAudioSettings(track0) { - if (!track0?.getSettings) return; - - log("applySavedAudioSettings"); - session.currentAudioConstraints = track0.getSettings(); - - const deviceId = session.currentAudioConstraints.deviceId; - if (!deviceId) return; - - const audioSettings = getStorage("audio_" + deviceId); - if (!audioSettings?.deviceId) return; - - const constraints = {}; - const allowedProps = ["autoGainControl", "echoCancellation", "noiseSuppression"]; - - for (const prop in session.currentAudioConstraints) { - if (audioSettings[prop] !== undefined && - audioSettings[prop] !== session.currentAudioConstraints[prop] && - allowedProps.includes(prop)) { - constraints[prop] = audioSettings[prop]; - warnlog("DIFF: " + prop); - } - } - - warnlog(constraints); - if (!Object.keys(constraints).length) return; - - track0.applyConstraints({ advanced: [constraints] }) - .then(() => warnlog("audio settings updated for deviceId:" + deviceId)) - .catch(e => errorlog("Failed to reset to audio defaults")); -} - -// Video settings application -function applySavedVideoSettings(track0) { - if (!track0?.getSettings) return; - - session.currentCameraConstraints = track0.getSettings(); - - // Handle mobile orientation - if (session.mobile) { - const isPortrait = (screen?.orientation?.type?.includes("portrait")) || - window.matchMedia("(orientation: portrait)").matches; - if (!isPortrait && session.currentCameraConstraints?.aspectRatio) { - session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; - } - } - - const deviceId = session.currentCameraConstraints.deviceId; - if (!deviceId) return; - - const cameraSettings = getStorage("camera_" + deviceId); - if (!cameraSettings?.current) return; - - const constraints = {}; - const skipProps = ["groupId"]; - const urlOverrides = { - aspectRatio: session.forceAspectRatio, - whiteBalanceMode: session.whiteBalance, - colorTemperature: session.whiteBalance, - exposureTime: session.exposure, - exposureMode: session.exposure, - zoom: session.zoom, - saturation: session.saturation, - sharpness: session.sharpness, - contrast: session.contrast, - brightness: session.brightness - }; - - for (const prop in session.currentCameraConstraints) { - if (!cameraSettings.current[prop] || - cameraSettings.current[prop] === session.currentCameraConstraints[prop] || - skipProps.includes(prop)) continue; - - if (urlOverrides[prop]) { - log(`${prop} is manually set via URL`); - continue; - } - - constraints[prop] = cameraSettings.current[prop]; - warnlog("DIFF: " + prop); - } - - warnlog(constraints); - if (!Object.keys(constraints).length) return; - - track0.applyConstraints({ advanced: [constraints] }) - .then(() => warnlog("video settings updated for deviceId:" + deviceId)) - .catch(e => errorlog("Failed to reset to defaults")); -} - -// Camera constraints update state -var updateCameraConstraintsBusy = false; -var updateCameraConstraintsNext = false; - -// Main camera constraints update function -async function updateCameraConstraints(constraint, value = null, ctrl = false, UUID = false, save = true) { - if (constraint === "zoom" && value === 0) { - log("can't zoom to zero"); - return; - } - - log("updateCameraConstraintsBusy?"); - - if (updateCameraConstraintsBusy) { - updateCameraConstraintsNext = [constraint, value, ctrl, UUID, save]; - return; - } - - updateCameraConstraintsBusy = true; - updateCameraConstraintsNext = false; - - try { - const track0 = session.streamSrc?.getVideoTracks()?.[0]; - - if (!track0 || track0.readyState !== "live" || !track0.enabled) { - if (!save) { - errorlog("TRACK IS NOT ENABLED"); - updateCameraConstraintsBusy = false; - updateCameraConstraintsNext = false; - } - return; - } - - // Parse value - if (value == parseFloat(value)) { - value = parseFloat(value); - } else if (value === "true") { - value = true; - } else if (value === "false") { - value = false; - } - - log({ advanced: [{ [constraint]: value }] }); - - // Get current settings and prepare storage - let cameraSettings = {}; - if (track0.getSettings) { - session.currentCameraConstraints = track0.getSettings(); - - if (session.currentCameraConstraints.deviceId) { - const storageKey = "camera_" + session.currentCameraConstraints.deviceId; - const stored = getStorage(storageKey); - - if (!stored) { - cameraSettings.default = JSON.parse(JSON.stringify(session.currentCameraConstraints)); - log(cameraSettings.default); - } else { - cameraSettings = stored; - } - } - } - - // Build constraints - const constraints = await buildConstraints(constraint, value, ctrl, track0); - - // Handle mobile orientation for constraints - if (session.mobile) { - adjustConstraintsForMobileOrientation(constraints); - } - - log("20788"); - log(constraints); - - // Apply constraints - await track0.applyConstraints({ advanced: [constraints] }) - .then(() => { - log("applied constraint"); - - if (save) { - saveConstraintSettings(track0, cameraSettings, constraint, UUID); - } - - if (updateCameraConstraintsNext) { - setTimeout(() => { - updateCameraConstraintsBusy = false; - updateCameraConstraints(...updateCameraConstraintsNext); - }, 30); - } else { - updateCameraConstraintsBusy = false; - } - }) - .catch(e => { - errorlog(e.message); - errorlog("couldn't save defaults"); - window.focus(); - updateCameraConstraintsBusy = false; - updateCameraConstraintsNext = false; - }); - - } catch (e) { - errorlog(e); - updateCameraConstraintsBusy = false; - updateCameraConstraintsNext = false; - return e; - } -} - -// Helper to build constraints based on type -async function buildConstraints(constraint, value, ctrl, track0) { - const current = session.currentCameraConstraints; - let constraints = {}; - - switch (constraint) { - case "width": - constraints.width = value; - if (current?.frameRate) constraints.frameRate = current.frameRate; - if (!ctrl && current?.height) constraints.height = current.height; - break; - - case "height": - constraints.height = value; - if (current?.frameRate) constraints.frameRate = current.frameRate; - if (!ctrl && current?.width) constraints.width = current.width; - break; - - case "frameRate": - if (!ctrl) { - constraints.frameRate = value; - if (current?.height && current?.width) { - constraints.height = current.height; - constraints.width = current.width; - } - } else { - constraints.frameRate = value; - } - break; - - case "exposureMode": - if (value === "manual") { - await applyCurrentSetting(track0, "exposureTime", current); - constraints = buildManualModeConstraints(constraint, value, "exposureTime", current); - } else { - constraints[constraint] = value; - } - break; - - case "exposureTime": - constraints[constraint] = value; - constraints.exposureMode = "manual"; - break; - - case "focusMode": - if (value === "manual") { - await applyCurrentSetting(track0, "focusDistance", current); - constraints = buildManualModeConstraints(constraint, value, "focusDistance", current); - } else { - constraints[constraint] = value; - } - break; - - case "focusDistance": - constraints[constraint] = value; - constraints.focusMode = "manual"; - break; - - case "whiteBalanceMode": - if (value === "manual") { - await applyCurrentSetting(track0, "colorTemperature", current); - constraints = buildWhiteBalanceConstraints(constraint, value, current); - } else if (value === "continuous") { - constraints[constraint] = value; - if (session.mobile && ChromiumVersion) { - constraints.colorTemperature = 5000; - } - } else { - constraints[constraint] = value; - } - break; - - case "colorTemperature": - constraints[constraint] = value; - constraints.whiteBalanceMode = "manual"; - break; - - case "aspectRatio": - constraints[constraint] = value; - if (current?.frameRate) constraints.frameRate = current.frameRate; - if (session.mobile) { - const isPortrait = (screen?.orientation?.type?.includes("portrait")) || - window.matchMedia("(orientation: portrait)").matches; - if (isPortrait && constraints.aspectRatio) { - constraints.aspectRatio = 1 / constraints.aspectRatio; - } - } - break; - - default: - constraints[constraint] = value; - } - - return constraints; -} - -// Helper for manual mode constraints -function buildManualModeConstraints(constraint, value, dependentProp, current) { - const constraints = { [constraint]: value }; - - if (current?.height && current?.width) { - constraints.height = current.height; - constraints.width = current.width; - } - - if (current?.[dependentProp]) { - constraints[dependentProp] = current[dependentProp]; - } - - return constraints; -} - -// Helper for white balance constraints -function buildWhiteBalanceConstraints(constraint, value, current) { - const constraints = { [constraint]: value }; - - if (current?.height && current?.width) { - constraints.height = current.height; - constraints.width = current.width; - } - - const colorTempConstraints = session.cameraConstraints?.colorTemperature; - if (colorTempConstraints?.max && colorTempConstraints?.min) { - if (current?.colorTemperature) { - constraints.colorTemperature = current.colorTemperature; - } else if (5000 >= colorTempConstraints.min && 5000 <= colorTempConstraints.max) { - constraints.colorTemperature = 5000; - } else { - constraints.colorTemperature = colorTempConstraints.max; - } - } - - return constraints; -} - -// Helper to apply current setting -async function applyCurrentSetting(track0, prop, current) { - if (!current?.[prop]) return; - - const tempConstraints = { [prop]: current[prop] }; - await track0.applyConstraints({ advanced: [tempConstraints] }); - session.currentCameraConstraints = track0.getSettings(); -} - -// Helper to adjust constraints for mobile orientation -function adjustConstraintsForMobileOrientation(constraints) { - const isPortrait = (screen?.orientation?.type?.includes("portrait")) || - window.matchMedia("(orientation: portrait)").matches; - - if (!isPortrait) return; - - if (constraints.width && constraints.height) { - [constraints.width, constraints.height] = [constraints.height, constraints.width]; - } else if (constraints.width) { - constraints.height = constraints.width; - delete constraints.width; - if (!constraints.aspectRatio && session.currentCameraConstraints?.height) { - constraints.width = session.currentCameraConstraints.height; - } - } else if (constraints.height) { - constraints.width = constraints.height; - delete constraints.height; - if (!constraints.aspectRatio && session.currentCameraConstraints?.width) { - constraints.height = session.currentCameraConstraints.width; - } - } -} - -// Helper to save constraint settings -function saveConstraintSettings(track0, cameraSettings, constraint, UUID) { - if (!track0.getSettings || !session.currentCameraConstraints.deviceId) return; - - session.currentCameraConstraints = track0.getSettings(); - cameraSettings.current = session.currentCameraConstraints; - setStorage("camera_" + session.currentCameraConstraints.deviceId, cameraSettings); - - if (toggleSettingsState === true) { - listCameraSettings(); - } - - if (UUID) { - const data = { - UUID: UUID, - videoOptions: listVideoSettingsPrep() - }; - sendMediaDevices(data.UUID); - session.sendMessage(data, data.UUID); - } - - if (["width", "height", "aspectRatio"].includes(constraint)) { - session.setResolution(); - } -} - -function setupSharpnessTool() { - var promise; - const worker = new Worker("./thirdparty/focus_worker.js", { type: "module" }); - worker.onerror = event => { - errorlog(event); - promise.reject(event); - }; - worker.onmessage = messageEvent => { - log("Sharpness score: " + messageEvent.data.score.avg_edge_width_perc); - promise.resolve(messageEvent.data.score.avg_edge_width_perc); - }; - - measureBlur = imageData => { - worker.postMessage({ imageData }); - }; - - const canvas = document.createElement("canvas"); - // document.getElementById("header").appendChild(canvas); - - async function getSharpness(x = 50, y = 50) { - if (session.videoElement) { - log("XY"); - log(x + " : " + y); - canvas.width = session.videoElement.videoWidth / 5; - canvas.height = session.videoElement.videoHeight / 5; - - if (x < 10) { - x = 10; - } - if (y < 10) { - y = 10; - } - if (x > 90) { - x = 90; - } - if (y > 90) { - y = 90; - } - - var sx = (session.videoElement.videoWidth / 100) * (x - 10); - var sy = (session.videoElement.videoHeight / 100) * (y - 10); - var sw = session.videoElement.videoWidth * 0.2; - var sh = session.videoElement.videoHeight * 0.2; - - canvas.getContext("2d").filter = "blur(3px)"; // denoise - canvas.getContext("2d").drawImage(session.videoElement, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); // for drawing the video element on the canvas - - const canvasData = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height); - - var res, rej; - promise = new Promise((resolve, reject) => { - res = resolve; - rej = reject; - }); - promise.resolve = res; - promise.reject = rej; - - measureBlur(canvasData); - - return promise; - } - return null; - } - - return getSharpness; -} -var sharpnessToolActive = false; -var sharpnessTool = false; -async function tapToFocus(x, y, force = false) { - if (isNaN(x) || isNaN(y)) { - return; - } - - if (sharpnessToolActive) { - return; - } - - if (!session.streamSrc) { - checkBasicStreamsExist(); - return; - } - - //var bestFocus = -1; - var track0 = session.streamSrc.getVideoTracks(); - if (!track0.length) { - log("No video tracks"); - return; - } - track0 = track0[0]; - if (!track0.getCapabilities) { - log("Track lacks advanced features. Firefox?"); - return; - } - - var capabilities = track0.getCapabilities(); - if (!("focusDistance" in capabilities)) { - log("Track doesn't support focusing"); - return; - } - - var settings = track0.getSettings(); - if ("focusMode" in settings) { - if (!force && settings.focusMode !== "manual") { - log("Need to be in manual focus mode"); - return; - } - } - - if (!sharpnessTool) { - sharpnessTool = setupSharpnessTool(); - } - - var bestFocus = -1; - var bestSharpness = 999; - sharpnessToolActive = true; - - try { - log("Current focus distance: " + capabilities.focusDistance); - await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: capabilities.focusDistance.min }] }); - await sleep(250); - - var stepping = capabilities.focusDistance.step || 0.1; - - if ((capabilities.focusDistance.max - capabilities.focusDistance.min) / stepping > 100) { - stepping = parseInt((capabilities.focusDistance.max - capabilities.focusDistance.min) / 100); - } - if (!stepping) { - stepping = 0.1; - } - for (var i = capabilities.focusDistance.min; i <= capabilities.focusDistance.max; i += stepping) { - await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: i }] }); - await sleep(120); // wait long enough for a new frame and focus to adjust. - log("focus: " + i + ", " + x + "x" + y); - var response = await sharpnessTool(x, y); - if (response && response < bestSharpness) { - bestSharpness = response; - bestFocus = i; - } else if (response === null) { - return; - } - - log(response + " " + bestSharpness + " " + bestFocus + " " + i + " " + capabilities.focusDistance.max); - } - if (bestFocus !== -1) { - log("Setting focus now to: " + bestFocus); - await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: bestFocus }] }); - } - } catch (e) { - errorlog(e); - } - sharpnessToolActive = false; -} - -session.remoteFocus = async function (focusDistance, absolute = false) { - try { - var track0 = session.streamSrc.getVideoTracks()[0]; - if (!track0?.getCapabilities) return; - - var capabilities = track0.getCapabilities(); - if (!capabilities.focusDistance) { - warnlog("No Focus supported on this device"); - return; - } - - const focusRange = capabilities.focusDistance; - if (!("min" in focusRange)) return; - - if (session.focusDistance === false || session.focusDistance === undefined) { - const settings = track0.getSettings(); - session.focusDistance = settings.focusDistance || focusRange.min; - } - - let newFocusDistance; - if (absolute) { - newFocusDistance = focusRange.min + focusDistance * (focusRange.max - focusRange.min); - } else { - const range = focusRange.max - focusRange.min; - const step = focusRange.step || 0.01; - const change = Math.max(Math.abs(range * focusDistance), step); - newFocusDistance = session.focusDistance + (focusDistance > 0 ? change : -change); - } - - newFocusDistance = Math.min(Math.max(newFocusDistance, focusRange.min), focusRange.max); - - const step = focusRange.step || 0.01; - const steps = Math.round((newFocusDistance - focusRange.min) / step); - newFocusDistance = focusRange.min + (steps * step); - - // Use updateCameraConstraints with save=false to avoid debouncing - await updateCameraConstraints("focusDistance", newFocusDistance, false, false, false); - session.focusDistance = newFocusDistance; - - return session.focusDistance; - } catch (e) { - errorlog(e); - return null; - } -}; - -session.setRemoteAutofocus = async function (enabled) { - try { - var mode = enabled ? "continuous" : "manual"; - await updateCameraConstraints("focusMode", mode, false, false, false); - session.focusDistance = false; // Reset stored focus distance - log("Autofocus set to: " + mode); - } catch (e) { - errorlog(e); - } -}; - -session.remoteZoom = async function (zoom, absolute = false) { - try { - var track0 = session.streamSrc.getVideoTracks()[0]; - if (!track0?.getCapabilities) return; - - var capabilities = track0.getCapabilities(); - if (!capabilities.zoom) { - warnlog("No zoom supported on this device"); - return; - } - - const zoomRange = capabilities.zoom; - if (!("min" in zoomRange) || !("max" in zoomRange) || zoomRange.max === zoomRange.min) { - warnlog("Zoom not adjustable on this device"); - return; - } - - if (session.zoom === false || session.zoom === undefined) { - const settings = track0.getSettings(); - session.zoom = settings.zoom || zoomRange.min; - } - - let newZoom; - if (absolute) { - newZoom = zoomRange.min + zoom * (zoomRange.max - zoomRange.min); - } else { - const range = zoomRange.max - zoomRange.min; - const step = zoomRange.step || 1; - const change = Math.max(Math.abs(range * zoom), step); - newZoom = session.zoom + (zoom > 0 ? change : -change); - } - - newZoom = Math.min(Math.max(newZoom, zoomRange.min), zoomRange.max); - - const step = zoomRange.step || 1; - const steps = Math.round((newZoom - zoomRange.min) / step); - newZoom = zoomRange.min + (steps * step); - - // Use updateCameraConstraints with save=false - await updateCameraConstraints("zoom", newZoom, false, false, false); - session.zoom = newZoom; - - return session.zoom; - } catch (e) { - errorlog(e); - return null; - } -}; - -session.remotePan = async function (pan, absolute = false) { - try { - var track0 = session.streamSrc.getVideoTracks()[0]; - if (!track0?.getCapabilities) return; - - var capabilities = track0.getCapabilities(); - if (!capabilities.pan) { - warnlog("No pan supported on this device"); - return; - } - - const panRange = capabilities.pan; - if (!("min" in panRange) || !("max" in panRange) || panRange.max === panRange.min) { - warnlog("Pan not adjustable on this device"); - return; - } - - if (session.pan === false || session.pan === undefined) { - const settings = track0.getSettings(); - session.pan = settings.pan || (panRange.min + panRange.max) / 2; - } - - let newPan; - if (absolute) { - const range = panRange.max - panRange.min; - newPan = panRange.min + ((pan + 1) / 2) * range; - } else { - const range = panRange.max - panRange.min; - const step = panRange.step || 1; - const change = Math.max(Math.abs(range * pan), step); - newPan = session.pan + (pan > 0 ? change : -change); - } - - newPan = Math.min(Math.max(newPan, panRange.min), panRange.max); - - const step = panRange.step || 1; - const steps = Math.round((newPan - panRange.min) / step); - newPan = panRange.min + (steps * step); - - // Use updateCameraConstraints with save=false - await updateCameraConstraints("pan", newPan, false, false, false); - session.pan = newPan; - - return session.pan; - } catch (e) { - errorlog(e); - return null; - } -}; - -session.remoteTilt = async function (tilt, absolute = false) { - try { - var track0 = session.streamSrc.getVideoTracks()[0]; - if (!track0?.getCapabilities) return; - - var capabilities = track0.getCapabilities(); - if (!capabilities.tilt) { - warnlog("No tilt supported on this device"); - return; - } - - const tiltRange = capabilities.tilt; - if (!("min" in tiltRange) || !("max" in tiltRange) || tiltRange.max === tiltRange.min) { - warnlog("Tilt not adjustable on this device"); - return; - } - - if (session.tilt === false || session.tilt === undefined) { - const settings = track0.getSettings(); - session.tilt = settings.tilt || (tiltRange.min + tiltRange.max) / 2; - } - - let newTilt; - if (absolute) { - const range = tiltRange.max - tiltRange.min; - newTilt = tiltRange.min + ((tilt + 1) / 2) * range; - } else { - const range = tiltRange.max - tiltRange.min; - const step = tiltRange.step || 1; - const change = Math.max(Math.abs(range * tilt), step); - newTilt = session.tilt + (tilt > 0 ? change : -change); - } - - newTilt = Math.min(Math.max(newTilt, tiltRange.min), tiltRange.max); - - const step = tiltRange.step || 1; - const steps = Math.round((newTilt - tiltRange.min) / step); - newTilt = tiltRange.min + (steps * step); - - // Use updateCameraConstraints with save=false - await updateCameraConstraints("tilt", newTilt, false, false, false); - session.tilt = newTilt; - - return session.tilt; - } catch (e) { - errorlog(e); - return null; - } -}; - -session.remoteExposure = async function (exposure, absolute = false) { - try { - var track0 = session.streamSrc.getVideoTracks()[0]; - if (!track0?.getCapabilities) return; - - var capabilities = track0.getCapabilities(); - var settings = track0.getSettings(); - - if (!capabilities.exposureMode || !capabilities.exposureTime) { - warnlog("Exposure control not supported on this device"); - return; - } - - // Ensure manual mode - if (settings.exposureMode !== 'manual') { - await updateCameraConstraints("exposureMode", "manual", false, false, false); - } - - const exposureRange = capabilities.exposureTime; - - if (session.exposure === false || session.exposure === undefined) { - session.exposure = settings.exposureTime || exposureRange.min; - } - - let newExposure; - if (absolute) { - newExposure = exposureRange.min + exposure * (exposureRange.max - exposureRange.min); - } else { - const range = exposureRange.max - exposureRange.min; - const step = exposureRange.step || 1; - const change = Math.max(Math.abs(range * exposure), step); - newExposure = session.exposure + (exposure > 0 ? change : -change); - } - - newExposure = Math.min(Math.max(newExposure, exposureRange.min), exposureRange.max); - - // Use updateCameraConstraints with save=false - await updateCameraConstraints("exposureTime", newExposure, false, false, false); - session.exposure = newExposure; - - log(`Applied new exposure time: ${session.exposure}`); - - return session.exposure; - } catch (e) { - errorlog(e); - return null; - } -}; - -function toggleAudioUser(ele) { - if (!ele) { - ele = ele || getById("advancedOptionsAudio"); - ele.style.display = "inline-flex"; - if (getById("popupSelector_constraints_audio").style.display == "block") { - toggleSettings(); - } else { - getById("popupSelector_constraints_audio").style.display = "block"; - ele.classList.add("highlight"); - if (!toggleSettingsState) { - toggleSettings(); - } - } - } else { - ele = ele || getById("advancedOptionsAudio"); - toggle(getById("popupSelector_constraints_audio"), false, false); - ele.classList.toggle("highlight"); - } - - getById("popupSelector_constraints_loading").style.visibility = "visible"; - getById("popupSelector_constraints_video").style.display = "none"; - getById("popupSelector_user_settings").style.display = "none"; -} -function toggleVideoUser(ele) { - if (!ele) { - ele = ele || getById("advancedOptionsCamera"); - ele.style.display = "inline-flex"; - if (getById("popupSelector_constraints_video").style.display == "block") { - toggleSettings(); - } else { - getById("popupSelector_constraints_video").style.display = "block"; - ele.classList.add("highlight"); - if (!toggleSettingsState) { - toggleSettings(); - } - } - } else { - ele = ele || getById("advancedOptionsCamera"); - toggle(getById("popupSelector_constraints_video"), false, false); - ele.classList.toggle("highlight"); - } - - getById("popupSelector_constraints_loading").style.visibility = "visible"; - getById("popupSelector_constraints_audio").style.display = "none"; - getById("popupSelector_user_settings").style.display = "none"; -} -function toggleUserUser(ele) { - ele = ele || getById("advancedOptionsGeneral"); - if (!toggleSettingsState) { - toggleSettings(); - } - ele.classList.toggle("highlight"); - toggle(getById("popupSelector_user_settings"), false, false); - getById("popupSelector_user_settings").style.visibility = "visible"; - getById("popupSelector_constraints_video").style.display = "none"; - getById("popupSelector_constraints_audio").style.display = "none"; -} - -async function requestBasicPermissions(constraint = { video: true, audio: true }, callback = setupWebcamSelection, miconly = false) { - if (session.taintedSession === null) { - log("STILL WAITING ON HASH TO VALIDATE"); - setTimeout( - function (constraint, callback, miconly) { - requestBasicPermissions(constraint, callback, miconly); - }, - 1000, - constraint, - callback, - miconly - ); - return null; - } else if (session.taintedSession === true) { - warnlog("HASH FAILED; PASSWORD NOT VALID"); - return false; - } else { - log("NOT TAINTED 1"); - } - setTimeout(function () { - getById("getPermissions").style.display = "none"; - getById("gowebcam").style.display = ""; - }, 0); - log("REQUESTING BASIC PERMISSIONS"); - - try { - - if (!navigator.mediaDevices) { - throw new Error("navigator.mediaDevices not found - check your security / browser settings."); - } - - var timerBasicCheck = null; - if (!session.cleanOutput) { - log("Setting Timer for getUserMedia"); - timerBasicCheck = setTimeout(function () { - if (!session.cleanOutput) { - if (session.mobile) { - warnUser("Notice: Camera timed out\n\nDid you accept the camera permissions?\n\nThis error may also appear if you are in a phone call or another app is already using the camera or microphone."); - } else { - warnUser("Camera Access Request Timed Out\n\nDid you accept camera permissions? Please do so first.\n\nIf you have NDI Tools installed, try uninstalling that.\n\nPlease also ensure that your camera and audio devices are correctly connected and not already in use. Bypassing USB hubs or using different USB cables can sometimes help.\n\nYou may also just need to restart the computer"); - } - } - }, 10000); - } - - let modifiedConstraint = { ...constraint }; - - try { - const videoPermission = await navigator.permissions.query({ name: "camera" }); - const audioPermission = await navigator.permissions.query({ name: "microphone" }); - - // If video is denied but audio is allowed, adjust the constraint - if (videoPermission.state === "denied" && constraint.video) { - warnlog("Video permissions are denied"); - if (constraint.audio) { - // Keep audio if it was originally requested - modifiedConstraint.video = false; - } else { - // If no audio was requested, this will likely fail - throw new Error("Video permissions denied"); - } - } - - // If audio is denied but video is allowed, adjust the constraint - if (audioPermission.state === "denied" && constraint.audio) { - warnlog("Audio permissions are denied"); - if (constraint.video) { - // Keep video if it was originally requested - modifiedConstraint.audio = false; - } else { - // If no video was requested, this will likely fail - throw new Error("Audio permissions denied"); - } - } - } catch (permissionError) { - log("Permissions API check failed:", permissionError); - } - - if (session.audioInputChannels) { - if (modifiedConstraint.audio === true) { - modifiedConstraint.audio = {}; - modifiedConstraint.audio.channelCount = session.audioInputChannels; - } else if (modifiedConstraint.audio) { - modifiedConstraint.audio.channelCount = session.audioInputChannels; - } - } - - if (session.micSampleRate) { - if (modifiedConstraint.audio === true) { - modifiedConstraint.audio = {}; - modifiedConstraint.audio.sampleRate = parseInt(session.micSampleRate); - } else if (modifiedConstraint.audio) { - modifiedConstraint.audio.sampleRate = parseInt(session.micSampleRate); - } - } - if (session.micSampleSize) { - if (modifiedConstraint.audio === true) { - modifiedConstraint.audio = {}; - modifiedConstraint.audio.sampleSize = parseInt(session.micSampleSize); - } else if (modifiedConstraint.audio) { - modifiedConstraint.audio.sampleSize = parseInt(session.micSampleSize); - } - } - - if (!modifiedConstraint.audio && !modifiedConstraint.video) { - if (miconly) { - warnUser("We couldn't find a microphone.\n\nPlease ensure you have granted the microphone permissions."); - } else { - warnUser("We couldn't find a microphone or camera.\n\nPlease ensure you have granted the microphone and camera permissions."); - } - // return null; - } - - if (session.safemode) { - if (modifiedConstraint.video) { - modifiedConstraint.video = true; - } - if (modifiedConstraint.audio) { - modifiedConstraint.audio = true; - } - } - getUserMediaRequestID += 1; - var gumID = getUserMediaRequestID; - log("CONSTRAINT"); - log(modifiedConstraint); - var timeoutStart = 0; - if (Firefox) { - timeoutStart = 500; - } - log("timeoutStart :" + timeoutStart); - setTimeout( - async function (gumID, constraint, timerBasicCheck, callback, miconly) { - log("gumID: " + gumID); - log(constraint); - var removeAudio = false; - if (!constraint.audio && !constraint.video) { - constraint.audio = true; - removeAudio = true; - } - - // Permissions API is not supported in all browsers, so we use a try-catch block - let videoPermission = "prompt"; - let audioPermission = "prompt"; - - if (Firefox && Firefox >= 132) { - console.warn("😱 see: https://bugzilla.mozilla.org/show_bug.cgi?id=1924572#c1"); - } else { - try { - const videoStatus = await navigator.permissions.query({ name: "camera" }); - videoPermission = videoStatus.state; - const audioStatus = await navigator.permissions.query({ name: "microphone" }); - audioPermission = audioStatus.state; - log("audioPermission: " + audioPermission); - } catch (e) { - warnlog("Permissions API is not fully supported in this browser."); - } - - const safariPermissionBug = SafariVersion && SafariVersion > 18 && (iOS || iPad); - - if (videoPermission === "granted" && !safariPermissionBug) { - constraint.video = false; - } - if (audioPermission === "granted" && !safariPermissionBug) { - constraint.audio = false; - } - } - - if (!constraint.audio && !constraint.video) { - warnlog("bypassing navigator.mediaDevices.getUserMedia; permissions granted already?"); - clearTimeout(timerBasicCheck); - if (getUserMediaRequestID !== gumID) { - warnlog("GET USER MEDIA CALL HAS EXPIRED 3a"); - return; - } - closeModal(); - if (callback) { - callback(miconly); - } - return; - } - - if (Firefox) { - constraint = toFirefoxConstraint(constraint); - } - - warnlog("navigator.mediaDevices.getUserMedia starting..."); - navigator.mediaDevices - .getUserMedia(constraint) - .then(function (stream) { - // Apple needs thi to happen before I can access EnumerateDevices. - if (removeAudio) { - constraint.audio = false; // this seeems pointless? - stream.getTracks().forEach(function (track) { - stream.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - } - log("got first stream"); - clearTimeout(timerBasicCheck); - if (getUserMediaRequestID !== gumID) { - warnlog("GET USER MEDIA CALL HAS EXPIRED 3"); - stream.getTracks().forEach(function (track) { - stream.removeTrack(track); - track.stop(); - log("stopping old track"); - }); - return; - } - closeModal(); - log(stream.getTracks()); - session.streamSrc = stream; - checkBasicStreamsExist(); - updateRenderOutpipe(); - if (callback) { - callback(miconly); - } - }) - .catch(function (err) { - clearTimeout(timerBasicCheck); - warnlog("some error with GetUSERMEDIA"); - console.warn(err); /* handle the error */ - if (err.name == "NotFoundError" || err.name == "DevicesNotFoundError") { - //required track is missing - } else if (err.name == "NotReadableError" || err.name == "TrackStartError") { - //webcam or mic are already in use - } else if (err.name == "OverconstrainedError" || err.name == "ConstraintNotSatisfiedError") { - //constraints can not be satisfied by avb. devices - } else if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") { - //permission denied in browser - if (isIFrame) { - console.error('Make sure that this IFRAME has the correct permissions allowed, ie:\n\niframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;geolocation;screen-wake-lock;";'); - } - if (!session.cleanOutput) { - setTimeout(function () { - if (window.obsstudio) { - warnUser("Permissions denied.\n\nTo access the camera or microphone from within OBS, please refer to:\ndocs.vdo.ninja/guides/share-webcam-from-inside-obs.", false, false); - } else if (ChromiumVersion && !session.mobile) { - warnUser("

    Camera/mic permissions denied

    \nPlease ensure you have allowed the mic/camera permissions in your browser, such as like:\n\n\n\nFor further help on how to resolve this issue, please refer to:\n\nhttps://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions.", false, false); - } else if (Firefox && session.mobile) { - warnUser( - "

    Camera/mic permission denied

    \nPlease allow mic/camera access.\n\n\ - If not prompted, go to Settings -> Site permissions -> exceptions (at bottom) -> vdo.ninja, and then manually enable the permissions.\n\n\ - If Firefox still gives you issues, try in incognito mode or a different browser.\ - For further help, please refer to:\n\nhttps://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions.", - false, - false - ); - } else { - warnUser("Permission access to the camera or microphone was denied.\n\nPlease ensure you have allowed the mic/camera permissions in your browser.\n\nFor guides on how to resolve this issue, please refer to:\n\nhttps://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions.", false, false); - } - }, 1); - } - return; - } else if (err.name == "TypeError" || err.name == "TypeError") { - //empty constraints object - } else { - //permission denied in browser - if (!session.cleanOutput) { - setTimeout( - function (err) { - warnUser(err); - }, - 1, - err - ); - } - } - warnlog("trying to list webcam again"); - if (callback) { - callback(miconly); - } - }); - }, - timeoutStart, - gumID, - modifiedConstraint, - timerBasicCheck, - callback, - miconly - ); - } catch (e) { - console.warn(e); - if (!session.cleanOutput) { - if (window.isSecureContext) { - warnUser("An error has occured when trying to access the webcam or microphone. The reason is not known."); - } else if (iOS || iPad) { - warnUser("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported."); - } else { - warnUser("Error acessing camera or microphone.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia"); - } - } - } - return null; -} - -function awaitInboundCall() { - if (session.twilio) { - return; - } - loadScript("./thirdparty/twilio.min.js?v1", async function () { - if (!session.twilio) { - session.twilio = {}; - } - - class MyAudioProcessor { - constructor(audioContext) { - console.log("constructor"); - this.audioContext = session.audioCtxOutbound; - session.twilio.mixer = session.audioCtxOutbound.createGain(); // This serves as our mixer node - this.destination = session.twilio.destination = session.audioCtxOutbound.createMediaStreamDestination(); // Destination node - this.background = this.audioContext.createMediaElementSource(document.createElement("video")); - } - - async createProcessedStream(stream) { - const source = this.audioContext.createMediaStreamSource(stream); - const gain = this.audioContext.createGain(); - gain.gain.value = 0; - source.connect(gain); - gain.connect(session.twilio.mixer); - // session.twilio.micSources.push(stream); - - session.twilio.mixer.connect(this.destination); - return this.destination.stream; - } - - async destroyProcessedStream(stream) { - console.log("destroyProcessedStream called"); - console.log(stream); - } - } - - const response = await fetch("https://call.vdo.ninja:8443/token2"); - session.twilio.data = await response.json(); - - session.twilio.device = new Twilio.Device(session.twilio.data.token); - - const audioProcessor = new MyAudioProcessor(); - await session.twilio.device.audio.addProcessor(audioProcessor); - - session.twilio.device.register(); - - warnUser("Dial in access code: " + session.twilio.data.dialInNumber); - - session.twilio.device.on("registered", function () { - console.log("Twilio.Device is ready to receive incoming calls!"); - }); - - async function refresh() { - console.log("refreshing token"); - const response = await fetch("https://call.vdo.ninja:8443/refresh"); - session.twilio.data = await response.json(); - session.twilio.device.updateToken(session.twilio.data.token); - } - - session.twilio.refreshInterval = setInterval(refresh, 50 * 60 * 1000); - - session.twilio.micSources = []; - - session.twilio.updateMixer = async function (UUID = false) { - try { - console.log("update mixer"); - session.twilio.micSources.forEach(src => { - src.disconnect(); - }); - - if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getAudioTracks().length) { - var micSource = session.audioCtxOutbound.createMediaStreamSource(session.videoElement.srcObject); - } else if (session.streamSrc && session.streamSrc.getAudioTracks().length) { - var micSource = session.audioCtxOutbound.createMediaStreamSource(session.streamSrc); - } else { - var micSource = false; - } - - for (var uuid in session.rpcs) { - try { - if (session.rpcs[uuid].videoElement && session.rpcs[uuid].videoElement.srcObject) { - var guestSource = session.audioCtxOutbound.createMediaStreamSource(session.rpcs[uuid].videoElement.srcObject); - if (guestSource) { - guestSource.connect(session.twilio.mixer); - session.twilio.micSources.push(guestSource); - console.log(uuid + " added to twiliio"); - } - } - } catch (e) { - errorlog(e); - } - } - - if (micSource) { - micSource.connect(session.twilio.mixer); - session.twilio.micSources.push(micSource); - console.log("micSource added to twiliio"); - } - } catch (err) { - console.error("Could not get microphone audio:", err); - } - }; - - session.twilio.device.on("incoming", call => { - session.twilio.call = call; - console.log("Incoming call from: " + session.twilio.call.parameters.From); - - session.twilio.call.on("reject", () => { - console.log("rejected"); - }); - session.twilio.call.on("disconnect", () => { - console.log("disconnected"); - }); - session.twilio.call.on("cancel", () => { - console.log("cancelled"); - }); - session.twilio.call.on("accept", () => { - console.log("accpted"); - }); - session.twilio.call.on("audio", e => { - session.twilio.element = e; - console.log("audio stream available"); - outboundAudioPipeline(session.streamSrc); - }); - - window.focus(); - confirmAlt("Incoming call from: " + session.twilio.call.parameters.From + "\n" + getTranslation("accept-inbound-caller")).then(res => { - if (res) { - if (session.audioCtxOutbound.state === "suspended") { - session.audioCtxOutbound.resume().then(() => { - console.log("AudioContext resumed"); - }); - } - session.twilio.updateMixer(); - session.twilio.call.accept(); - } else { - session.twilio.call.reject(); - } - }); - }); - }); -} - -function joinConference(roomid, mute = true) { - // not used - loadScript("./thirdparty/twilio.min.js", function () { - fetch("https://call.vdo.ninja:8443/token") - .then(response => response.json()) - .then(async data => { - const device = new Twilio.Device(data.token); - call = await device.connect({ params: { To: roomid } }); - if (mute) { - const checkForRemoteStream = setInterval(() => { - if (call.getRemoteStream && call.getRemoteStream()) { - if (call.getRemoteStream().getTracks && call.getRemoteStream().getTracks().length) { - clearInterval(checkForRemoteStream); - call.getRemoteStream().getTracks()[0].enabled = false; - console.log("call muted"); - } - } - }, 500); - } - }) - .catch(error => { - console.error("Error fetching token: ", error); - }); - }); -} - -function listenWebsocket(roomid) { - // not used - var callSocket = null; - var connecting = false; - var streams = {}; - function connectAudioInbound() { - clearTimeout(connecting); - if (callSocket) { - if (callSocket.readyState === callSocket.OPEN) { - return; - } - try { - callSocket.close(); - } catch (e) { } - } - callSocket = new WebSocket("wss://call.vdo.ninja:8443/" + roomid); - callSocket.onclose = function () { - clearTimeout(connecting); - connecting = setTimeout(function () { - connectAudioInbound(); - }, 100); - }; - callSocket.onopen = function () { - send({ join: true }); - }; - callSocket.addEventListener("message", function (event) { - var msg = JSON.parse(event.data); - if (msg.event && msg.event === "media" && msg.media && msg.media.payload && msg.streamSid) { - playAudioChunk(msg.media.payload, msg.streamSid); - if (!streams[msg.streamSid]) { - console.log("stream starting"); - streams[msg.streamSid] = true; - //audioStreams.innerHTML = ""; - //for (stream in streams){ - // audioStreams.innerHTML += stream+"
    "; - //} - console.log(streams); - } - } else if (msg.event && msg.event === "stop" && msg.streamSid) { - console.log("stream stopping"); - delete streams[msg.streamSid]; - //audioStreams.innerHTML = ""; - //for (stream in streams){ - // audioStreams.innerHTML += stream+"
    "; - //} - console.log(streams); - } // else { - // console.log(msg); - // console.log("stream misc"); - // console.log(streams); - //} - }); - } - - function base64ToUint8Array(base64) { - const binaryString = window.atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } - function muLawToPCM(muLawData, gainFactor = 2.0, threshold = 30000, ratio = 2.0) { - const pcmData = new Int16Array(muLawData.length); - for (let i = 0; i < muLawData.length; i++) { - let muLawByte = muLawData[i]; - muLawByte = ~muLawByte; - let sign = muLawByte & 0x80; - muLawByte &= ~0x80; - let exponent = (muLawByte >> 4) & 0x07; - let data = muLawByte & 0x0f; - data |= 0x10; - data <<= 1; - data += 1; - data <<= exponent + 2; - data -= 0x84; - if (sign === 0) data = -data; - let amplifiedData = data * gainFactor; - if (Math.abs(amplifiedData) > threshold) { - const overThreshold = Math.abs(amplifiedData) - threshold; - const compressedData = threshold + overThreshold / ratio; - amplifiedData = amplifiedData > 0 ? compressedData : -compressedData; - } - pcmData[i] = amplifiedData; - } - return pcmData; - } - function playAudioChunk(base64Chunk, sid) { - const muLawData = base64ToUint8Array(base64Chunk); - const pcmData = muLawToPCM(muLawData); - const audioBuffer = session.audioCtx.createBuffer(1, pcmData.length, 8000); - const bufferSource = session.audioCtx.createBufferSource(); - const channelData = audioBuffer.getChannelData(0); - for (let i = 0; i < pcmData.length; i++) { - channelData[i] = pcmData[i] / 32768.0; - } - bufferSource.buffer = audioBuffer; - bufferSource.connect(session.audioCtx.destination); - bufferSource.start(); - } - function send(cmd) { - callSocket.send(JSON.stringify(cmd)); - } - connectAudioInbound(); -} - -function setupWebcamSelection(miconly = false) { - log("setupWebcamSelection();"); - - checkBasicStreamsExist(); - - try { - return enumerateDevices() - .then(function (dInfo) { - return gotDevices(dInfo, miconly); - }) - .then(function () { - if (getById("webcamquality").elements && parseInt(getById("webcamquality").elements.namedItem("resolution").value) == 3) { - // this is junk?? - if (session.maxframeRate === false) { - session.maxframeRate = 30; - session.maxframeRate_q2 = true; - } - } else if (session.maxframeRate_q2) { - session.maxframeRate = false; - session.maxframeRate_q2 = false; - } - - var audioSelect = getById("audioSource"); - var videoSelect = getById("videoSourceSelect"); - var outputSelect = getById("outputSource"); - - if (audioSelect.tagName == "UL") { - audioSelect.onchange = function () { - if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").disabled = true; - document.getElementById("gowebcam").dataset.audioready = "false"; - //document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD"; - document.getElementById("gowebcam").style.fontWeight = "normal"; - document.getElementById("gowebcam").innerHTML = "Waiting for mic to load"; - miniTranslate(document.getElementById("gowebcam"), "waiting-for-mic-to-load"); - } - activatedPreview = false; - grabAudio(); - }; - } - videoSelect.onchange = function () { - if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").disabled = true; - document.getElementById("gowebcam").dataset.ready = "false"; - //document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD"; - document.getElementById("gowebcam").style.fontWeight = "normal"; - document.getElementById("gowebcam").innerHTML = "Waiting for Camera to load"; - miniTranslate(document.getElementById("gowebcam"), "waiting-for-camera-to-load"); - } - warnlog("video source changed"); - - activatedPreview = false; - if (session.quality !== false) { - grabVideo(session.quality); - } else if (document.getElementById("webcamquality")) { - session.quality_wb = parseInt(document.getElementById("webcamquality").elements.namedItem("resolution").value); - session.quality_room = session.quality_wb; - grabVideo(session.quality_wb); - } - }; - - if (Firefox && !session.mobile) { - outputSelect.onclick = function () { - log("on click"); - if (outputSelect.options[outputSelect.selectedIndex].value === "others") { - navigator.mediaDevices.selectAudioOutput().then(device => { - if (device.kind == "audiooutput") { - session.sink = device.deviceId; - try { - var matched = false; - outputSelect.childNodes.forEach(ele => { - if (ele.value === device.deviceId) { - matched = true; - ele.selected = true; - } - }); - if (!matched) { - var option = document.createElement("option"); - option.value = device.deviceId; - option.text = device.label; - outputSelect.appendChild(option); - option.selected = true; - } - saveSettings(); // we're saving because there was an explicit action to change devices - } catch (e) { - errorlog(e); - } - if (!session.sink) { - return; - } // Not sure this would ever happen, but whatever. - resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist. - } - }); - } - }; - } - - outputSelect.onchange = function () { - if (iOS || iPad) { - return; - } - if (Firefox && !session.mobile) { - if (outputSelect.options[outputSelect.selectedIndex].value === "others") { - return; - } - } - - try { - session.sink = outputSelect.options[outputSelect.selectedIndex].value; - saveSettings(); // we're saving because there was an explicit action to change devices - } catch (e) { - errorlog(e); - } - - if (!session.sink) { - return; - } // Not sure this would ever happen, but whatever. - - resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist. - }; - - getById("webcamquality").onchange = function () { - if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").disabled = true; - document.getElementById("gowebcam").dataset.ready = "false"; - // document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD"; - document.getElementById("gowebcam").style.fontWeight = "normal"; - document.getElementById("gowebcam").innerHTML = "Waiting for Camera to load"; - miniTranslate(document.getElementById("gowebcam"), "waiting-for-camera-to-load"); - } - - if (parseInt(getById("webcamquality").elements.namedItem("resolution").value) == 2) { - if (session.maxframeRate === false) { - session.maxframeRate = 30; - session.maxframeRate_q2 = true; - } - } else if (session.maxframeRate_q2) { - session.maxframeRate = false; - session.maxframeRate_q2 = false; - } - - activatedPreview = false; - session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); - session.quality_room = session.quality_wb; - grabVideo(session.quality_wb); - }; - - if (session.safemode) { - if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").disabled = false; - //document.getElementById("gowebcam").innerHTML = getTranslation("start"); - miniTranslate(document.getElementById("gowebcam"), "start"); - document.getElementById("gowebcam").dataset.audioready = "true"; - document.getElementById("gowebcam").dataset.ready = "true"; - document.getElementById("gowebcam").focus(); - setTimeout(function () { - updateForceRotate(); - }, 1000); - if (session.autostart) { - publishWebcam(); // no need to mirror as there is no video... - } - return; - } - } - - if (session.audioDevice !== 0) { - // change from Auto to Selected Audio Device - log("SETTING AUDIO DEVICE!!"); - activatedPreview = false; - grabAudio(); - } else if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").dataset.audioready = "true"; - } - - if (session.videoDevice === 0 || miconly) { - if (session.autostart) { - publishWebcam(); // no need to mirror as there is no video... - return; - } else if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").dataset.ready = "true"; - if (document.getElementById("gowebcam").dataset.audioready == "true") { - document.getElementById("gowebcam").disabled = false; - miniTranslate(document.getElementById("gowebcam"), "start"); - document.getElementById("gowebcam").focus(); - //document.getElementById("gowebcam").innerHTML = getTranslation("start"); - } - } - } else { - log("GRabbing video: " + session.quality); - activatedPreview = false; - if (session.quality !== false) { - grabVideo(session.quality); - } else if (document.getElementById("webcamquality")) { - session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); - session.quality_room = session.quality_wb; - grabVideo(session.quality_wb); - } - } - - if (!(iOS || iPad || session.mobile)) { - try { - if (outputSelect.selectedIndex >= 0) { - session.sink = outputSelect.options[outputSelect.selectedIndex].value; - saveSettings(); - if (session.videoElement && !session.videoElement.paused) { - resetupAudioOut(); - } - } - } catch (e) { - errorlog(e); - } - } - }) - .catch(e => { - errorlog(e); - }); - } catch (e) { - errorlog(e); - } -} - -Promise.wait = function (ms) { - return new Promise(function (resolve) { - setTimeout(resolve, ms); - }); -}; - -Promise.prototype.timeout = function (ms) { - return Promise.race([ - this, - Promise.wait(ms).then(function () { - if (iOS || iPad) { - var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nIf using an iPhone or iPad, try fully closing your browser and open it again; Safari sometimes jams up the camera."); - errormsg.name = "timedOut"; - errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nIf using an iPhone or iPad, try fully closing your browser and open it again; Safari sometimes jams up the camera."; - throw errormsg; - } else if (session.mobile) { - var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nMake sure no other application is using the camera already and that you are using a compatible browser. If issues persist, maybe try the official native mobile app."); - errormsg.name = "timedOut"; - errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nMake sure no other application is using the camera already and that you are using a compatible browser. If issues persist, maybe try the official native mobile app."; - throw errormsg; - } else { - var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling it.\n\nPlease also ensure your camera and audio device are correctly connected and not already in use. You may also need to refresh the page."); - errormsg.name = "timedOut"; - errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling it.\n\nPlease also ensure your camera and audio device are correctly connected and not already in use. You may also need to refresh the page."; - throw errormsg; - } - }) - ]); -}; - -async function shareWebsite(autostart = false, evt = false) { - if (session.iframeSrc) { - if (!session.cleanOutput) { - getById("websitesharebutton2").classList.remove("hidden"); - } - - if (evt && (evt.ctrlKey || evt.metaKey)) { - if (getById("websitesharebutton2").classList.contains("green")) { - var actionMsg = {}; - actionMsg.infocus = false; - - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowIframe === true) { - session.sendMessage(actionMsg, UUID); - } - } - - getById("websitesharebutton2").classList.remove("green"); - getById("websitesharebutton2").ariaPressed = "false"; - getById("websitesharebutton2").title = "Hold CTRL (or CMD) and click to spotlight this shared website"; - } else { - if (session.streamID) { - var actionMsg = {}; - actionMsg.infocus = session.streamID; - - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowIframe === true) { - session.sendMessage(actionMsg, UUID); - } - } - - getById("websitesharebutton2").classList.add("green"); - getById("websitesharebutton2").ariaPressed = "true"; - getById("websitesharebutton2").title = "Video is currently spotlighted"; - } - } - return; - } - getById("websitesharebutton2").classList.remove("green"); - getById("websitesharebutton2").ariaPressed = "false"; - session.iframeSrc = false; - - if (session.director) { - clearDirectorSettings(); - //setStorage("directorWebsiteShare", {"website":session.iframeSrc, "roomid":session.roomid}); - } else if (document.getElementById("container_iframe") || session.iframeEle) { - if (session.iframeEle) { - session.iframeEle.remove(); - session.iframeEle = false; - } - if (document.getElementById("container_iframe")) { - document.getElementById("container_iframe").remove(); - } - - updateMixer(); - } - getById("websitesharebutton2").classList.add("hidden"); - getById("websitesharebutton").classList.remove("hidden"); - - var data = {}; - data.iframeSrc = false; - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowIframe === true) { - session.sendMessage(data, UUID); - } - } - getById("websitesharebutton2").title = "Hold CTRL (or CMD) and click to spotlight this shared website"; - return; - } - getById("websitesharebutton2").classList.remove("green"); - getById("websitesharebutton2").ariaPressed = "false"; - - if (autostart === false) { - window.focus(); - var iframeURL = await promptAlt(getTranslation("enter-website"), false, false, session.defaultIframeSrc); - } else { - var iframeURL = autostart; - } - if (!iframeURL) { - return; - } - if (iframeURL == session.iframeSrc) { - return; - } - session.defaultIframeSrc = iframeURL; - - warnlog(iframeURL); - - session.iframeSrc = parseURL4Iframe(iframeURL); - - if (session.director && !autostart) { - setStorage("directorWebsiteShare", { website: session.iframeSrc, roomid: session.roomid }); - } else if (session.iframeEle) { - session.iframeEle.src = session.iframeSrc; - if (session.iframeSrc.startsWith("https://www.youtube.com/")) { - // special handler. - setTimeout( - function (iframe_id) { - YoutubeListen(iframe_id); - }, - 1000, - iframe.id - ); - } - } else if (session.iFramesAllowed) { - - var iframe = document.createElement("iframe"); - iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location - iframe.src = session.iframeSrc; - iframe.id = "iframe_source"; - iframe.setAttribute("allowtransparency", "true"); - iframe.setAttribute("crossorigin", "anonymous"); - iframe.setAttribute("credentialless", "true"); - iframe.loadedYoutubeListen = false; - session.iframeEle = iframe; - - var container = document.createElement("div"); - iframe.container = container; - container.id = "container_iframe"; - - if (session.iframeSrc.startsWith("https://www.youtube.com/")) { - // special handler. - setTimeout( - function (iframe_id) { - YoutubeListen(iframe_id); - }, - 1000, - iframe.id - ); - } - updateMixer(); - } - - getById("websitesharebutton2").classList.remove("hidden"); - getById("websitesharebutton").classList.add("hidden"); - - var data = {}; - data.iframeSrc = session.iframeSrc; - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowIframe === true) { - session.sendMessage(data, UUID); - } - } -} - -function screenshareTypeDecider(sstype = 1) { - if (session.screenshareType) { - sstype = session.screenshareType; - } - if (sstype == 1) { - toggleScreenShare(); - } else if (sstype == 2) { - createIframePopup(); - } else if (sstype == 3) { - createSecondStream(); - } -} - -function createScreenShareURL(transparent = true) { - // iframe.src = - if (session.screenShareElement) { - var iFrameID = session.streamID.substring(0, 12) + "_" + session.generateStreamID(5); - } else if (session.screenshareid) { - var iFrameID = session.screenshareid; - } else { - var iFrameID = session.streamID.substring(0, 12) + "_" + session.generateStreamID(5); - } - - if (session.exclude) { - session.exclude.push(iFrameID); - } else { - session.exclude = []; - session.exclude.push(iFrameID); - } - - var extras = ""; - if (session.password) { - extras += "&password=" + session.password; // encodeURIComponent( - } - - if (session.privacy) { - extras += "&privacy"; - } - - if (session.meshcast) { - extras += "&meshcast"; - } - - if (session.token) { - extras += "&token=" + session.token; - } - - if (session.remote) { - if (session.remote === true) { - extras += "&remote"; - } else { - extras += "&remote=" + session.remote; - } - } - if (session.salt !== location.hostname) { - // this is default. - extras += "&salt=" + session.salt; - } - if (session.whipOutVideoBitrate) { - extras += "&wovb=" + session.whipOutVideoBitrate; - } - if (session.whipOutScreenShareBitrate) { - extras += "&wossbitrate=" + session.whipOutScreenShareBitrate; - } - - if (!session.notifyScreenShare) { - extras += "&smallshare"; - } - - if (session.screenshareContentHint) { - extras += "&sshint=" + session.screenshareContentHint; - } else if (session.contentHint) { - extras += "&sshint=" + session.contentHint; - } - - if (session.audioContentHint) { - extras += "&audiohint=" + session.audioContentHint; - } - - if (session.whipOutScreenShareCodec) { - extras += "&whipoutcodec=" + session.whipOutScreenShareCodec; - } else if (session.whipOutCodec) { - extras += "&whipoutcodec=" + session.whipOutCodec; - } - - if (session.screensharequality !== false) { - extras += "&q=" + session.screensharequality; // &quality works here, since only thing we are doing - } else if (session.quality !== false) { - extras += "&q=" + session.quality; - } else if (session.quality_ss !== false) { - extras += "&q=" + session.quality_ss; - } else { - extras += "&q=0"; - } - - if (session.screenShareLabel !== false) { - if (session.screenShareLabel) { - extras += "&label=" + encodeURIComponent(session.screenShareLabel); - } else if (session.label) { - extras += "&label=" + encodeURIComponent(session.label); - } - } - - if (session.screensharefps !== false) { - extras += "&maxframeRate=" + parseInt(session.screensharefps * 100) / 100.0; - } - if (session.screenshareAEC) { - extras += "&aec=1"; - } - if (session.screenshareDenoise) { - extras += "&denoise=1"; - } - if (session.screenshareAutogain) { - extras += "&autogain=1"; - } - if (session.screenshareStereo !== false) { - extras += "&stereo=" + session.screenshareStereo; - } - if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) { - extras += "&ar=" + session.forceAspectRatio; - } else if (session.forceScreenShareAspectRatio) { - extras += "&ar=" + session.forceScreenShareAspectRatio; - } - if (session.muted) { - extras += "&muted"; - } - - if (session.recordLocal) { - extras += "&record=" + session.recordLocal; - } - - if (session.autorecordlocal) { - extras += "&autorecordlocal"; - } - - if (transparent) { - extras += "&transparent&cleanish"; - } - // manual, since I don't want to use the auto-mixer. - return "?manual&audiodevice=1&screenshare&cb=0&nvb&nsb&autostart&view&room=" + session.roomid + "&push=" + iFrameID + extras; -} - -function createIframePopup() { - if (session.screenShareElement) { - postMessageIframe(session.screenShareElement, { close: true }); - session.screenShareElement.parentNode.removeChild(session.screenShareElement); - session.screenShareElement = false; - updateMixer(); - getById("screenshare2button").classList.remove("green"); - getById("screenshare2button").ariaPressed = "false"; - return; - } - - if ((session.queue && !session.transferred) || (session.screenShareState && !session.queue && session.transferred)) { - // if (session.queue || session.transferred){ - //getById("screenshare2button").classList.add("hidden"); - //getById("screensharebutton").classList.remove("hidden"); - toggleScreenShare(); - return; - } // can't secondary-screen share if in a queue. - - //if (!session.iFramesAllowed){errorlog("Can't create iFRAME - security is tainted due to possible CSS injection");return;} // allow because we are doing &sstype=2; not anything else. - var iframe = document.createElement("iframe"); - iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location - iframe.src = "./" + createScreenShareURL(); - iframe.setAttribute("allowtransparency", "true"); - iframe.setAttribute("crossorigin", "anonymous"); - iframe.setAttribute("credentialless", "true"); - iframe.style.width = "100%"; - iframe.style.height = "100%"; - iframe.style.overflow = "hidden"; - iframe.id = "screensharesource"; - iframe.dataset.sid = "#screensharesource"; - iframe.style.zIndex = "0"; - - session.screenShareElement = iframe; - session.screenShareElement.dataset.doNotMove = true; - - document.getElementById("main").appendChild(iframe); - - if (session.screenShareElementHidden) { - session.screenShareElement.style.display = "none"; - } - - updateMixer(); - - getById("screenshare2button").classList.add("green"); - getById("screenshare2button").ariaPressed = "true"; - - return; // ignore the rest. -} - -function previewWebcam(miconly = false) { - if (session.taintedSession === null) { - log("STILL WAITING ON HASH TO VALIDATE"); - setTimeout( - function (miconly) { - previewWebcam(miconly); - }, - 1000, - miconly - ); - return; - } else if (session.taintedSession === true) { - warnlog("HASH FAILED; PASSWORD NOT VALID"); - return; - } else { - log("NOT TAINTED"); - } - - if (activatedPreview == true) { - log("activeated preview return 1"); - return; - } - activatedPreview = true; - - if (miconly) { - // this just shares the preview section with the mic-only and video+mic modes - if (!getById("add_camera_inner").cloned) { - getById("add_camera_inner").cloned = true; - insertAfter(getById("add_camera_inner"), getById("add_microphone")); - document.getElementById("videoSourceSelect").innerHTML = ""; - } - } else if (getById("add_camera_inner").cloned) { - getById("add_camera_inner").cloned = false; - insertAfter(getById("add_camera_inner"), getById("add_camera")); - document.getElementById("videoSourceSelect").innerHTML = ""; - } - - if (session.audioDevice === 0) { - // OFF - var constraint = { - audio: false - }; - } else if ((session.echoCancellation !== false) && (session.autoGainControl !== false) && (session.noiseSuppression !== false) && (session.voiceIsolation !== true)) { - // AUTO - var constraint = { - audio: true - }; - } else { - // Disable Echo Cancellation and stuff for the PREVIEW (DEFAULT CAM/MIC) - var constraint = { - audio: {} - }; - if (session.echoCancellation !== false) { - // if not disabled, we assume it's on - constraint.audio.echoCancellation = true; - } else { - constraint.audio.echoCancellation = false; - if (!session.cleanoutput) { - getById("headphoneTip1").classList.remove("hidden"); - miniTranslate(getById("headphoneTipContext1"), "headphones-tip"); - //getById("headphoneTipContext1").innerHTML = getTranslation("headphones-tip"); - } - } - if (session.autoGainControl !== false) { - constraint.audio.autoGainControl = true; - } else { - constraint.audio.autoGainControl = false; - } - if (session.noiseSuppression !== false) { - constraint.audio.noiseSuppression = true; - } else { - constraint.audio.noiseSuppression = false; - } - - if (session.voiceIsolation === true) { - constraint.audio.voiceIsolation = true; - } - } - - if (session.videoDevice === 0 || miconly) { - constraint.video = false; - } else { - constraint.video = true; - } - - window.onorientationchange = function () { - if (Firefox) { - updateForceRotate(true); - } - setTimeout(async function () { - if (session.forceAspectRatio) { - await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - } - if (session.effect && session.effect === "7") { - digitalZoom(); - } - updateForceRotate(); - }, 200); - }; - - if (constraint.video === false && constraint.audio === false) { - if (session.autostart) { - publishWebcam(false, miconly); // no need to mirror as there is no video... - return; - } else { - getById("getPermissions").style.display = "none"; - if (document.getElementById("gowebcam")) { - document.getElementById("gowebcam").dataset.ready = "true"; - document.getElementById("gowebcam").dataset.audioready = "true"; - document.getElementById("gowebcam").disabled = false; - //document.getElementById("gowebcam").innerHTML = getTranslation("start"); - miniTranslate(document.getElementById("gowebcam"), "start"); - document.getElementById("gowebcam").focus(); - } - } - return; - } - - enumerateDevices() - .then(function (devices) { - log("enumeratated"); - log(devices); - var vtrue = false; - var atrue = false; - devices.forEach(function (device) { - if (device.kind === "audioinput") { - atrue = true; - } else if (device.kind === "videoinput") { - vtrue = true; - } - }); - if (atrue === false) { - constraint.audio = false; - } - if (vtrue === false) { - constraint.video = false; - } - setTimeout( - function (constraint, miconly) { - requestBasicPermissions(constraint, setupWebcamSelection, miconly); - }, - 0, - constraint, - miconly - ); - }) - .catch(error => { - log("enumeratated failed. Seeking permissions."); - setTimeout( - function (constraint, miconly) { - requestBasicPermissions(constraint, setupWebcamSelection, miconly); - }, - 0, - constraint, - miconly - ); - }); -} - -function copyFunction(copyText, evt = false) { - if (evt) { - if ("buttons" in evt) { - if (evt.buttons !== 0) { - return; - } - } else if ("which" in evt) { - if (evt.which !== 0) { - return; - } - } - popupMessage(evt); - evt.preventDefault(); - evt.stopPropagation(); - } - - try { - copyText.select(); - copyText.setSelectionRange(0, 99999); - document.execCommand("copy"); - } catch (e) { - var dummy = document.createElement("input"); - document.body.appendChild(dummy); - dummy.value = copyText; - dummy.select(); - document.execCommand("copy"); - document.body.removeChild(dummy); - } - return false; -} - -function generateQRPage() { - var pass = sanitizePassword(getById("invite_password").value); - if (pass.length) { - return generateHash(pass + session.salt, 4) - .then(function (hash) { - generateQRPageCallback(hash); - }) - .catch(errorlog); - } else { - generateQRPageCallback(""); - } -} - -async function updateLinkWelcome(arg, input) { - if (input.checked) { - var response = await promptAlt("Enter the message you'd like the guests to see"); - response = encodeURIComponent(response); - var param = input.dataset.param.split("=")[0]; - input.dataset.param = param + "=" + response; - } - updateLink(arg, input); -} - -function updateLinkWebP(arg, input) { - if (input.checked) { - if (!(getById("director_block_" + arg).dataset.raw.includes("&broadcast") || getById("director_block_" + arg).dataset.raw.includes("?broadcast"))) { - getById("broadcastSlider").checked = true; - updateLink(arg, getById("broadcastSlider")); - } - } - updateLink(arg, input); -} - -var soloLinkAppended = ""; -function updateLink(arg, input, solo = false) { - log("updateLink: " + input.dataset.param); - if (input.checked) { - getById("director_block_" + arg).dataset.raw += input.dataset.param; - - if (solo) { - soloLinkAppended += input.dataset.param; - } - - var string = getById("director_block_" + arg).dataset.raw; - - if (arg == 1 && getById("obfuscate_director_" + arg).checked) { - string = obfuscateURL(string); - } - - getById("director_block_" + arg).href = string; - getById("director_block_" + arg).innerText = string; - } else { - var string = getById("director_block_" + arg).dataset.raw + "&"; - string = string.replace(input.dataset.param + "&", "&"); - string = string.substring(0, string.length - 1); - getById("director_block_" + arg).dataset.raw = string; - - if (solo) { - soloLinkAppended += "&"; - soloLinkAppended = soloLinkAppended.replace(input.dataset.param + "&", "&"); - soloLinkAppended = soloLinkAppended.substring(0, soloLinkAppended.length - 1); - } - - if (arg == 1 && getById("obfuscate_director_" + arg).checked) { - string = obfuscateURL(string); - } - - // document.querySelector("soloLink") - // soloLink - - getById("director_block_" + arg).href = string; - getById("director_block_" + arg).innerText = string; - } - if (solo) { - document.querySelectorAll("a.soloLink").forEach(ele => { - try { - var href = ele.getAttribute("value") + soloLinkAppended; - ele.href = href; - ele.innerHTML = href; - } catch (e) { - errorlog(e); - } - }); - } - - // Update all solo links with universal token if in auth mode - if (session.authMode && session.universalViewToken) { - updateAllSoloLinks(); - } - - saveDirectorSettings(); -} - -function changeURL(changeURL) { - window.focus(); - if (session.consent) { - hangup(false); - window.location.href = changeURL; - } else { - confirmAlt(getTranslation("director-redirect-1") + changeURL + getTranslation("director-redirect-2")).then(res => { - if (res) { - hangup(false); - window.location.href = changeURL; - } - }); - } -} - -function updateLinkInverse(arg, input) { - log("updateLinkInverse"); - log(input.dataset.param); - if (!input.checked) { - getById("director_block_" + arg).dataset.raw += input.dataset.param; - - var string = getById("director_block_" + arg).dataset.raw; - - if (arg == 1 && getById("obfuscate_director_" + arg).checked) { - string = obfuscateURL(string); - } - - getById("director_block_" + arg).href = string; - getById("director_block_" + arg).innerText = string; - } else { - var string = getById("director_block_" + arg).dataset.raw + "&"; - string = string.replace(input.dataset.param + "&", "&"); - string = string.substring(0, string.length - 1); - getById("director_block_" + arg).dataset.raw = string; - - if (arg == 1 && getById("obfuscate_director_" + arg).checked) { - string = obfuscateURL(string); - } - - getById("director_block_" + arg).href = string; - getById("director_block_" + arg).innerText = string; - } -} - -function updateLinkScene(arg, input) { - log("updateLinkScene"); - var string = getById("director_block_" + arg).dataset.raw || ""; - - if (input.checked) { - string = changeParam(string, "scene", "0"); - } else { - string = changeParam(string, "scene", "1"); - } - getById("director_block_" + arg).dataset.raw = string; - - if (arg == 1 && getById("obfuscate_director_" + arg).checked) { - string = obfuscateURL(string); - } - - getById("director_block_" + arg).href = string; - getById("director_block_" + arg).innerText = string; -} - -function fullscreenPageToggle(state = null) { - try { - if (!document.fullscreenElement) { - // not currently full screen - if (state !== false) { - // if state is false, we are already not full screen - if (document.documentElement.requestFullscreen) { - document.documentElement.requestFullscreen(); - } else if (document.documentElement.webkitRequestFullscreen) { - document.documentElement.webkitRequestFullscreen(); - } - } - } else if (document.exitFullscreen) { - if (!state) { - // if toggle mode or state=false - document.exitFullscreen(); - } - } else if (document.webkitExitFullscreen) { - if (!state) { - // if toggle mode or state=false - document.webkitExitFullscreen(); - } - } - //updateMixer(); // we will do this on the event for this instead - } catch (e) { - errorlog(e); - } -} - -session.pipWindow = false; -async function PictureInPicturePageToggle(state = null) { - try { - if (typeof documentPictureInPicture === "undefined") { - return; - } - if (session.pipWindow) { - getById("testtone").parentNode.insertBefore(session.pipWindow.document.getElementById("gridlayout"), getById("testtone")); - session.pipWindow.close(); - session.pipWindow = null; - updateMixer(); - getById("PictureInPicturePage").classList.remove("green"); - } else { - session.pipWindow = await documentPictureInPicture.requestWindow({ width: 614, height: 344 }); // 360 + 30px for the window header - - session.pipWindow.addEventListener("pagehide", event => { - if (session.pipWindow) { - getById("testtone").parentNode.insertBefore(session.pipWindow.document.getElementById("gridlayout"), getById("testtone")); - session.pipWindow.close(); - session.pipWindow = null; - updateMixer(); - getById("PictureInPicturePage").classList.remove("green"); - } - }); - - var pipWindowHead = 'Pop-out Window'; - pipWindowHead += ''; - - session.pipWindow.document.body.className = "main"; - session.pipWindow.document.head.innerHTML = pipWindowHead; - session.pipWindow.document.body.style = document.body.style; - session.pipWindow.document.title = "Pop-out Window"; - session.pipWindow.document.body.append(getById("gridlayout")); - session.pipWindow.onresize = updateMixer; - updateMixer(); // just in case onresize doesn't trigger - getById("PictureInPicturePage").classList.add("green"); - } - } catch (e) { - errorlog(e); - } -} - -function resetGen() { - getById("gencontent").style.display = "block"; - getById("gencontent2").style.display = "none"; - getById("gencontent2").className = ""; //container-inner - getById("gencontent").className = "container-inner"; // - getById("gencontent2").innerHTML = ""; - getById("videoname4").focus(); -} - -function generateQRPageCallback(hash) { - try { - var title = getById("videoname4").value; - if (title.length) { - title = title.replace(/[\W]+/g, "_").replace(/_+/g, "_"); // but not what others might get. TODO: allow for non-alphanumeric characters; santitize, then URL encode instead, - title = "&label=" + title; - } - var sid = session.generateStreamID(); - - var viewstr = ""; - var sendstr = ""; - - if (getById("invite_bitrate").checked) { - viewstr += "&bitrate=20000"; - } - if (getById("invite_vp9").checked) { - viewstr += "&codec=vp9"; - } - if (getById("invite_stereo").checked) { - viewstr += "&stereo"; - sendstr += "&stereo"; - } - if (getById("invite_automic").checked) { - sendstr += "&audiodevice=1"; - } - if (getById("invite_automic").checked) { - sendstr += "&audiodevice=1"; - } - if (getById("invite_effects").checked) { - sendstr += "&effects"; - } - - if (getById("invite_remotecontrol").checked) { - // - var remote_gen_id = session.generateStreamID(); - sendstr += "&remote=" + remote_gen_id; // security - viewstr += "&remote=" + remote_gen_id; - } - - if (getById("invite_joinroom").value.trim().length) { - sendstr += "&ssid&room=" + getById("invite_joinroom").value.trim(); - viewstr += "&solo&room=" + getById("invite_joinroom").value.trim(); - } - - if (getById("invite_password").value.trim().length) { - sendstr += "&hash=" + hash; - viewstr += "&password=" + sanitizePassword(getById("invite_password").value.trim()); - } - - if (session.token) { - sendstr += "&token=" + session.token; - viewstr += "&token=" + session.token; - } - - if (getById("invite_group_chat_type").value) { - // 0 is default - if (getById("invite_group_chat_type").value == 1) { - // no video - sendstr += "&novideo"; - } else if (getById("invite_group_chat_type").value == 2) { - // no view or audio - sendstr += "&view"; - } - } - - if (getById("invite_quality").value) { - if (getById("invite_quality").value == 0) { - sendstr += "&quality=0"; - } else if (getById("invite_quality").value == 1) { - sendstr += "&quality=1"; - } else if (getById("invite_quality").value == 2) { - sendstr += "&quality=2"; - } - } - - var wss = ""; - - if (session.wssSetViaUrl) { - if (session.customWSS && session.customWSS !== true) { - wss = "&pie=" + session.customWSS; - } else if (session.customWSS == true) { - wss = "&wss=" + session.wss; - } else { - wss = "&wss2=" + session.wss; - } - } - - var hoststr = ""; - if (getById("invite_hostlink").checked) { - hoststr = "https://" + location.host + location.pathname + "?push=" + sid + "_hostlink" + "&view=" + sid + sendstr + "&bitrate=500" + title + wss; - sendstr = "https://" + location.host + location.pathname + "?push=" + sid + "&view=" + sid + "_hostlink" + sendstr + "&bitrate=1200" + title + wss; - } else { - sendstr = "https://" + location.host + location.pathname + "?push=" + sid + sendstr + title + wss; - } - - if (getById("invite_obfuscate").checked) { - sendstr = obfuscateURL(sendstr); - } - - viewstr = "https://" + location.host + location.pathname + "?view=" + sid + viewstr + title + wss; - getById("gencontent").style.display = "none"; - getById("gencontent").className = ""; // - getById("gencontent2").style.display = "block"; - getById("gencontent2").className = "container-inner"; - - getById("gencontent2").innerHTML = - '
    \ -

    Guest Invite Link:

    \ - ' + - sendstr + - '

    \ -

    and don\'t forget the

    OBS Browser Source Link:

    ' + - viewstr + - ' \ - '; - - if (hoststr) { - getById("gencontent2").innerHTML += '

    Host Chat Link:

    ' + hoststr + ' '; - } - - getById("gencontent2").innerHTML += - '

    \ - \ -
  • This invite link and OBS ingestion link are reusable.
  • \ -
  • Only one person may use a specific invite at a time.
  • \ -
  • The stream ID can be changed manually to something else; keep it unique and alphanumeric.
  • \ -
  • Nothing is stored server-side; links do not expire, nor is there anything to delete.
  • \ -


    \ - '; - - var qrcode = new QRCode(getById("qrcode"), { - width: 300, - height: 300, - colorDark: "#000000", - colorLight: "#FFFFFF", - useSVG: false - }); - qrcode.makeCode(sendstr); - getById("qrcode").title = ""; - setTimeout(function () { - getById("qrcode").title = ""; - if (getById("qrcode").getElementsByTagName("img").length) { - getById("qrcode").getElementsByTagName("img")[0].style.cursor = "none"; - getById("qrcode").getElementsByTagName("img")[0].style.margin = "0 auto"; - } - }, 100); // i really hate the title overlay that the qrcode function makes - } catch (e) { - errorlog(e); - } -} - -function initSceneList(UUID) { - Object.keys(session.sceneList).forEach((scene, index) => { - if (getById("container_" + UUID).querySelectorAll('[data-scene="' + scene + '"]').length) { - return; - } // already exists. - var newScene = document.createElement("div"); - newScene.innerHTML = '"; - newScene.classList.add("customScene"); - - var added = false; - getById("container_" + UUID) - .querySelectorAll(".customScene>[data-scene]") - .forEach(ele => { - log(ele); - if (!added && ele.dataset.scene > scene + "") { - ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); - added = true; - } - }); - if (!added) { - getById("container_" + UUID).appendChild(newScene); - } - }); -} - -function updateSceneList(scene) { - // custom scenes only. - if (!session.director) { - return; - } - if (scene in session.sceneList) { - return; - } - - if (parseInt(scene) + "" === scene) { - if (parseInt(scene) >= 0 && parseInt(scene) <= session.maxScene) { - return; - } - } - - session.sceneList[scene] = true; - for (var UUID in session.rpcs) { - var newScene = document.createElement("div"); - newScene.innerHTML = '"; - newScene.classList.add("customScene"); - var added = false; - getById("container_" + UUID) - .querySelectorAll(".customScene>[data-scene]") - .forEach(ele => { - log(ele); - if (!added && ele.dataset.scene > scene + "") { - ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); - added = true; - } - }); - if (!added) { - getById("container_" + UUID).appendChild(newScene); - } - } - - if (session.showDirector) { - if (document.getElementById("container_director")) { - var newScene = document.createElement("div"); - newScene.innerHTML = '"; - newScene.classList.add("customScene"); - //getById("container_director").appendChild(newScene); - - var added = false; - getById("container_director") - .querySelectorAll(".customScene>[data-scene]") - .forEach(ele => { - if (!added && ele.dataset.scene > scene + "") { - ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); - added = true; - } - }); - if (!added) { - getById("container_director").appendChild(newScene); - } - } - } -} - -var vis = (function () { - var stateKey, - eventKey, - keys = { - hidden: "visibilitychange", - webkitHidden: "webkitvisibilitychange", - mozHidden: "mozvisibilitychange", - msHidden: "msvisibilitychange" - }; - for (stateKey in keys) { - if (stateKey in document) { - eventKey = keys[stateKey]; - break; - } - } - return function (c) { - if (c) { - document.addEventListener(eventKey, c); - //document.addEventListener("blur", c); - //document.addEventListener("focus", c); - } - return !document[stateKey]; - }; -})(); - -function unPauseVideo(videoEle, update = true) { - try { - if (!videoEle) { - return; - } else if (!(videoEle.dataset.UUID in session.rpcs)) { - return; - } else if (!("prePausedBandwidth" in session.rpcs[videoEle.dataset.UUID])) { - return; - } // not paused; useless to have, but might as well - session.rpcs[videoEle.dataset.UUID].manualBandwidth = false; - //session.rpcs[videoEle.dataset.UUID].manualAudioBandwidth = false; - - if (session.rpcs[videoEle.dataset.UUID].videoElement) { - session.rpcs[videoEle.dataset.UUID].videoElement.play(); - } - - delete session.rpcs[videoEle.dataset.UUID].prePausedBandwidth; - session.requestRateLimit(false, videoEle.dataset.UUID, false); // passing a bitrate of false forces the saved existing bitrate to be requested. - videoEle.classList.remove("paused"); - videoEle.classList.remove("partialFadeout"); - if (update) { - updateMixer(); - } - } catch (e) { - errorlog(e); - } -} - -function pauseVideo(videoEle, update = true) { - if (!videoEle) { - return; - } else if (!(videoEle.dataset.UUID in session.rpcs)) { - return; - } - session.rpcs[videoEle.dataset.UUID].prePausedBandwidth = session.rpcs[videoEle.dataset.UUID].manualBandwidth; // useless, but whatever - session.rpcs[videoEle.dataset.UUID].manualBandwidth = 0; - - if (session.rpcs[videoEle.dataset.UUID].videoElement) { - session.rpcs[videoEle.dataset.UUID].videoElement.pause(); - } - //session.rpcs[videoEle.dataset.UUID].manualAudioBandwidth = 0; - session.requestRateLimit(false, videoEle.dataset.UUID, true); // passing a bitrate of false forces the saved existing bitrate to be requested. - videoEle.classList.add("paused"); - videoEle.classList.add("partialFadeout"); - if (update) { - updateMixer(); - } -} - -(function rightclickmenuthing() { - // right click menu - "use strict"; - - function clickInsideElement(e, value = "menu") { - var el = e.srcElement || e.target; - if (el.dataset && value in el.dataset) { - return el; - } else { - while ((el = el.parentNode)) { - if (el.dataset && value in el.dataset) { - return el; - } - } - } - return false; - } - - function getPosition(event2) { - var posx = 0; - var posy = 0; - - if (!event2) { - var event = window.event; - } - - if (event2.pageX || event2.pageY) { - posx = event2.pageX; - posy = event2.pageY; - } else if (event2.clientX || event2.clientY) { - posx = event2.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - posy = event2.clientY + document.body.scrollTop + document.documentElement.scrollTop; - } - - return { x: posx, y: posy }; - } - - var taskItemInContext; - var clickCoordsX; - var clickCoordsY; - var menu; - var menuState = 0; - var lastMenu = false; - var menuWidth; - var menuHeight; - var windowWidth; - var windowHeight; - - function contextListener() { - document.addEventListener("contextmenu", function (e) { - - if (!session.cleanish && session.cleanOutput) { - e.preventDefault(); - e.stopPropagation(); - return; - } - - if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - if (e && !e.ctrlKey && !e.metaKey) { - return; - } - } else if (e && (e.ctrlKey || e.metaKey)) { - return; - } // allow for development ease - - taskItemInContext = clickInsideElement(e, "menu"); - - if (taskItemInContext) { - e.preventDefault(); - e.stopPropagation(); - if (taskItemInContext.dataset && taskItemInContext.dataset.menu) { - toggleMenuOn(taskItemInContext.dataset.menu, taskItemInContext); - } else { - toggleMenuOn(); - } - positionMenu(e); - return false; - } else { - taskItemInContext = null; - toggleMenuOff(); - } - }); - } - - function menuClickListener(e) { - var clickeElIsLink = clickInsideElement(e, "action"); - if (clickeElIsLink) { - e.preventDefault(); - e.stopPropagation(); - menuItemListener(clickeElIsLink, false, e); - return false; - } else { - var button = e.which || e.button; - if (button === 1) { - toggleMenuOff(); - } - } - } - - function handleInputElement(e) { - // for the input range slider version - var clickeElIsLink = clickInsideElement(e, "action"); - if (clickeElIsLink) { - e.preventDefault(); - e.stopPropagation(); - menuItemListener(clickeElIsLink, e.srcElement, e); - return false; - } else { - var button = e.which || e.button; - if (button === 1) { - toggleMenuOff(); - } - } - } - - function toggleMenuOn(menutype = false, ele = false) { - if (lastMenu && lastMenu !== menutype) { - try { - menuState = 0; - getById(lastMenu).classList.remove("context-menu--active"); - - document.removeEventListener("click", menuClickListener); - menu.querySelectorAll("input").forEach(ele => { - ele.removeEventListener("input", handleInputElement); - }); - } catch (e) { } - } - menu = getById(menutype || "context-menu"); - menuItemSyncState(menu); - if (menuState !== 1) { - menuState = 1; - menu.classList.add("context-menu--active"); - document.addEventListener("click", menuClickListener); - menu.querySelectorAll("input").forEach(ele => { - ele.addEventListener("input", handleInputElement); - }); - } - - if (ele && ele.classOptions) { - menu.classList.add(ele.classOptions); - } - lastMenu = menutype || "context-menu"; - } - - function toggleMenuOff() { - if (menuState !== 0) { - menuState = 0; - menu.classList.remove("context-menu--active"); - - document.removeEventListener("click", menuClickListener); - menu.querySelectorAll("input").forEach(ele => { - ele.removeEventListener("input", handleInputElement); - }); - } - lastMenu = false; - } - - function positionMenu(e) { - try { - var clickCoords = getPosition(e); - clickCoordsX = clickCoords.x; - clickCoordsY = clickCoords.y; - } catch (e) { - errorlog(e); - return; - } - - menuWidth = menu.offsetWidth + 4; - menuHeight = menu.offsetHeight + 4; - - windowWidth = window.innerWidth; - windowHeight = window.innerHeight; - - if (windowWidth - clickCoordsX < menuWidth) { - menu.style.left = windowWidth - menuWidth + "px"; - } else { - menu.style.left = clickCoordsX + "px"; - } - - if (windowHeight - clickCoordsY < menuHeight) { - menu.style.top = windowHeight - menuHeight + "px"; - } else { - menu.style.top = clickCoordsY + "px"; - } - - // Handle submenu edge positioning - var submenus = menu.querySelectorAll('.context-menu__submenu'); - submenus.forEach(function(submenu) { - submenu.classList.remove('context-menu__submenu--left'); - var parentRect = submenu.parentElement.getBoundingClientRect(); - var submenuWidth = 200; // Width defined in CSS - if (parentRect.right + submenuWidth > windowWidth) { - submenu.classList.add('context-menu__submenu--left'); - } - }); - } - - async function menuItemListener(link, inputElement = false, e = false) { - - if (link.getAttribute("data-action") === "Open") { - window.open(taskItemInContext.href); - } else if (link.getAttribute("data-action") === "Copy") { - copyFunction(taskItemInContext.href); - } else if (link.getAttribute("data-action") === "Mirror") { - if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { - session.mirrored = !session.mirrored; - applyMirror(session.mirrorExclude); - log("session.mirrored"); - } else { - if ("mirror" in taskItemInContext) { - taskItemInContext.mirror = !taskItemInContext.mirror; - applyMirrorGuest(taskItemInContext.mirror, taskItemInContext); - } else { - taskItemInContext.mirror = true; - applyMirrorGuest(taskItemInContext.mirror, taskItemInContext); - } - } - } else if (link.getAttribute("data-action") === "Rotate") { - if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { - session.rotate = ((session.rotate || 0) + 90) % 360; - if (Firefox && session.mobile) { - updateForceRotate(true); - } else { - updateForceRotate(false); - } - log("session.rotate"); - setTimeout(function () { - updateMixer(); - }, 1); - } else { - if ("manualRotate" in taskItemInContext) { - taskItemInContext.manualRotate = ((taskItemInContext.manualRotate || 0) + 90) % 360; - taskItemInContext.rotated = taskItemInContext.manualRotate; - } else { - taskItemInContext.manualRotate = ((taskItemInContext.rotated || 0) + 90) % 360; - taskItemInContext.rotated = taskItemInContext.manualRotate; - } - - if (taskItemInContext.dataset) { - taskItemInContext.dataset.rotated = taskItemInContext.rotated || 0; - } - updateVideoTransform(taskItemInContext); - setTimeout(function () { - updateMixer(); - }, 1); - } - } else if (link.getAttribute("data-action") === "FullWindow") { - if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { - session.infocus = true; - } else { - session.infocus = taskItemInContext.dataset.UUID; - } - updateMixer(); - } else if (link.getAttribute("data-action") === "ShrinkWindow") { - session.infocus = false; - updateMixer(); - } else if (link.getAttribute("data-action") === "Pause") { - pauseVideo(taskItemInContext); - } else if (link.getAttribute("data-action") === "UnPause") { - unPauseVideo(taskItemInContext); - } else if (link.getAttribute("data-action") === "PiP") { - togglePictureInPicture(taskItemInContext); - } else if (link.getAttribute("data-action") === "PiP2") { - PictureInPicturePageToggle(); - } else if (link.getAttribute("data-action") === "Record") { - if (taskItemInContext.stopWriter || taskItemInContext.recording) { - } else if (taskItemInContext.startWriter) { - taskItemInContext.startWriter(); - } else { - var videoKbps = session.recordDefault; - if (session.recordLocal !== false) { - videoKbps = session.recordLocal; - } - recordLocalVideo(null, videoKbps, taskItemInContext); - } - } else if (link.getAttribute("data-action") === "StopRecording") { - if (taskItemInContext.stopWriter) { - taskItemInContext.stopWriter(); - } else if (taskItemInContext.recording) { - recordLocalVideo("stop", null, taskItemInContext); - } - } else if (link.getAttribute("data-action") === "CopyFrameAsImage") { - copyVideoFrameToClipboard(taskItemInContext, e); - } else if (link.getAttribute("data-action") === "SaveFrameToDisk") { - saveVideoFrameToDisk(taskItemInContext, e); - } else if (link.getAttribute("data-action") === "DrawOnVideo") { - if (taskItemInContext.clearDrawOnVideo) { - taskItemInContext.clearDrawOnVideo(); - taskItemInContext.clearDrawOnVideo = null; - } else { - taskItemInContext.clearDrawOnVideo = drawOnThis(taskItemInContext); - } - } else if (link.getAttribute("data-action") === "ChangeBuffer") { - toggleBufferSettings(taskItemInContext.dataset.UUID); - } else if (link.getAttribute("data-action") === "Cast") { - //copyFunction(taskItemInContext.href); - } else if (link.getAttribute("data-action") === "Controls") { - - //getById("main").classList.add("forcecontrols"); // adds an annoying shadow to the bar area - //taskItemInContext.showControlBar = true; - //checkVideoControlBar(taskItemInContext); - //taskItemInContext.controls = false; - //ele.focus(); - taskItemInContext.removeAttribute("controls"); - taskItemInContext.setAttribute("controls", ""); - taskItemInContext.controls = true; - - } else if (link.getAttribute("data-action") === "HideControls") { - - //taskItemInContext.showControlBar = false; - taskItemInContext.controls = false; - taskItemInContext.removeAttribute("controls"); - - } else if (link.getAttribute("data-action") === "Edit") { - //copyFunction(taskItemInContext.href); - var response = await promptAlt("Please note, manual edits to the URL may conflict with the toggles", false, false, taskItemInContext.href); - if (response) { - taskItemInContext.href = response; - taskItemInContext.dataset.raw = response; - taskItemInContext.innerHTML = response; - } - } else if (link.getAttribute("data-action") === "QRCode") { - warnUser("Loading QR Code"); - loadQR(function tt(url) { - getById("alertModalMessage").innerHTML = ""; - var qrcode = new QRCode(getById("alertModalMessage"), { - width: 300, - height: 300, - colorDark: "#000000", - colorLight: "#FFFFFF", - useSVG: false - }); - qrcode.makeCode(url); - getById("alertModalMessage").title = ""; - setTimeout(function () { - getById("alertModalMessage").title = ""; - if (getById("alertModalMessage").getElementsByTagName("img").length) { - getById("alertModalMessage").getElementsByTagName("img")[0].style.cursor = "none"; - } - }, 100); - }, taskItemInContext.href); - } else if (link.getAttribute("data-action") === "ShowStats") { - if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { - var [menu, innerMenu] = statsMenuCreator(); - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); - printMyStats(innerMenu); - } else if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, taskItemInContext.dataset.UUID); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, taskItemInContext.dataset.UUID); - } - } else if (link.getAttribute("data-action") === "OutputAudio") { - enumerateDevices().then(function (deviceInfo) { - var ele = getById(taskItemInContext.id); - - var deviceListElement = gotDevices3(deviceInfo, ele); - if (deviceListElement) { - warnUser("Select the audio playback destination for this media:\n\n"); - getById("alertModalMessage").appendChild(deviceListElement); - } else { - warnUser("No output devices available"); - } - }); - - // - } else if (link.getAttribute("data-action") === "RemoteHangup") { - if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { - var confirmHangup = confirm(getTranslation("confirm-disconnect-user")); - if (confirmHangup) { - var msg = {}; - msg.hangup = true; - msg.remote = session.remote; - msg = await session.encodeRemote(msg); - session.sendRequest(msg, taskItemInContext.dataset.UUID); - pokeIframeAPI("hungup", "remote", taskItemInContext.dataset.UUID); - } - } - } else if (link.getAttribute("data-action") === "RemoteReload") { - if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { - var confirmReload = confirm(getTranslation("confirm-reload-user")); - if (confirmReload) { - var msg = {}; - msg.reload = true; - msg.remote = session.remote; - msg = await session.encodeRemote(msg); - session.sendRequest(msg, taskItemInContext.dataset.UUID); - pokeIframeAPI("reload", "remote", taskItemInContext.dataset.UUID); - } - } - } else if (link.getAttribute("data-action") === "PTZControls") { - // Requires MUTUAL remote: both local viewer AND remote peer must have &remote - if (session.remote && session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { - togglePTZControls(taskItemInContext.dataset.UUID); - } - } else if (link.getAttribute("data-action") === "ResetAutofocus") { - // Requires MUTUAL remote: both local viewer AND remote peer must have &remote - if (session.remote && session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { - session.requestAutofocusChange(true, taskItemInContext.dataset.UUID, session.remote); - } - } else if (link.getAttribute("data-action") === "RemoteControlsParent") { - return; // Don't close menu on submenu parent click - } else if (link.getAttribute("data-action") === "SSNewTab") { - var URL = "https://" + window.location.hostname + location.pathname + createScreenShareURL(false); - log(URL); - window.open(URL, "_blank").focus(); - } else if (link.getAttribute("data-action") === "pip-clock") { - popOutClock(taskItemInContext.children[0]); - } else if (link.getAttribute("data-action") === "Publish") { - var URL = taskItemInContext.href; - URL += "&clean&chroma=000&ssar=landscape&nosettings&prefercurrenttab&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&whippush&whippushtoken&q=1"; - var win = window.open(URL, "targetWindow", "toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720"); - win.focus(); - win.resizeTo(1280, 720); - } else if (link.getAttribute("data-action") === "RecordWindow") { - var URL = taskItemInContext.href; - URL += "&clean&chroma=000&ssar=landscape&nosettings&prefercurrenttab&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&autorecordlocal"; - var win = window.open(URL, "targetWindow", "toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720"); - win.focus(); - win.resizeTo(1280, 720); - } else if (link.getAttribute("data-action") === "SendTip") { - var UUID = taskItemInContext.dataset.UUID; - if (UUID && session.rpcs[UUID] && session.rpcs[UUID].acceptsTips) { - if (typeof openTipModal === 'function') { - openTipModal(UUID); - } - } else if (session.pcs && session.pcs[UUID] && session.pcs[UUID].acceptsTips) { - if (typeof openTipModal === 'function') { - openTipModal(UUID); - } - } - } - - if (inputElement === false) { - log("Task ID - " + taskItemInContext + ", Task action - " + link.getAttribute("data-action")); - toggleMenuOff(); - } - } - - function menuItemSyncState(menu) { - var items = menu.querySelectorAll("[data-action]"); - for (var i = 0; i < items.length; i++) { - if (items[i].getAttribute("data-action") === "FullWindow") { - if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { - if (session.infocus === true) { - items[i].parentNode.classList.add("hidden"); - } else { - items[i].parentNode.classList.remove("hidden"); - } - } else if (taskItemInContext.dataset.UUID === session.infocus) { - items[i].parentNode.classList.add("hidden"); - } else { - items[i].parentNode.classList.remove("hidden"); - } - } else if (items[i].getAttribute("data-action") === "ShrinkWindow") { - if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { - if (session.infocus === true) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (taskItemInContext.dataset.UUID === session.infocus) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "Pause") { - if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) { - if ("prePausedBandwidth" in session.rpcs[taskItemInContext.dataset.UUID]) { - items[i].parentNode.classList.add("hidden"); - } else { - items[i].parentNode.classList.remove("hidden"); - } - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "UnPause") { - if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) { - if ("prePausedBandwidth" in session.rpcs[taskItemInContext.dataset.UUID]) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "Record") { - if (taskItemInContext.stopWriter || taskItemInContext.recording) { - items[i].parentNode.classList.add("hidden"); - } else { - items[i].parentNode.classList.remove("hidden"); - } - } else if (items[i].getAttribute("data-action") === "StopRecording") { - if (taskItemInContext.stopWriter || taskItemInContext.recording) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "CopyFrameAsImage") { - if (taskItemInContext.srcObject && taskItemInContext.srcObject.getVideoTracks().length) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "SaveFrameToDisk") { - if (taskItemInContext.srcObject && taskItemInContext.srcObject.getVideoTracks().length) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "Controls") { - if (taskItemInContext.controls) { - items[i].parentNode.classList.add("hidden"); - } else { - items[i].parentNode.classList.remove("hidden"); - } - } else if (items[i].getAttribute("data-action") === "HideControls") { - if (taskItemInContext.controls) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "PiP2") { - if (typeof documentPictureInPicture !== "undefined") { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "RemoteControlsParent") { - // Show/hide the entire Remote Controls submenu - // Requires MUTUAL remote: both local viewer AND remote peer must have &remote - if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { - items[i].parentNode.classList.add("hidden"); - } else if (session.remote && session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "ChangeBuffer") { - if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { - items[i].parentNode.classList.add("hidden"); - } else if (session.rpcs[taskItemInContext.dataset.UUID]) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "TipRightClick") { - if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - items[i].parentNode.classList.add("hidden"); - } else { - items[i].parentNode.classList.remove("hidden"); - } - } else if (items[i].getAttribute("data-action") === "SendTip") { - // Show tip option only if: - // 1. Video source accepts tips (publisher opt-in) - // 2. Viewer has opted in with &showtips (viewer opt-in) - // 3. Not in clean mode - // 4. Not in Electron - // 5. Performer has completed Stripe setup (validated) - var UUID = taskItemInContext.dataset.UUID; - var acceptsTips = false; - var tipId = null; - var tipServer = null; - if (UUID) { - if (session.rpcs && session.rpcs[UUID] && session.rpcs[UUID].acceptsTips) { - acceptsTips = true; - tipId = session.rpcs[UUID].tipId; - tipServer = session.rpcs[UUID].tipServer; - } else if (session.pcs && session.pcs[UUID] && session.pcs[UUID].acceptsTips) { - acceptsTips = true; - tipId = session.pcs[UUID].tipId; - tipServer = session.pcs[UUID].tipServer; - } - } - // Check if performer is validated (use cache if available) - var performerValid = false; - if (tipId) { - tipServer = tipServer || session.tipServer || "https://ninjabacker.com"; - var cacheKey = tipServer + "/" + tipId; - performerValid = tipPerformerCache[cacheKey] === true; - } - if (acceptsTips && performerValid && session.showTips && !session.cleanOutput && navigator.userAgent.toLowerCase().indexOf(" electron/") === -1) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "Publish") { - if (taskItemInContext.classList.contains("publish")) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } else if (items[i].getAttribute("data-action") === "RecordWindow") { - if (taskItemInContext.classList.contains("publish")) { - items[i].parentNode.classList.remove("hidden"); - } else { - items[i].parentNode.classList.add("hidden"); - } - } - } - } - contextListener(); -})(); - -function checkVideoControlBar(ele) { // this is aggressive. Lets not use it unless required. - if (ele) { - if (ele.showControlBar) { - if (ele.showControlBarInterval) { - clearTimeout(ele.showControlBarInterval); - } - ele.focus(); - ele.removeAttribute("controls"); - ele.setAttribute("controls", ""); - ele.focus(); - ele.showControlBarInterval = setTimeout(function (ele) { - checkVideoControlBar(ele); - }, 100, ele); - } - } -} -function gotDevices3(deviceInfos, vid) { - var audioEle = document.createElement("select"); - log(deviceInfos); - if (!deviceInfos.length) { - return false; - } - for (let i = 0; i !== deviceInfos.length; ++i) { - if (deviceInfos[i].kind === "audiooutput") { - var opt = document.createElement("option"); - opt.innerText = deviceInfos[i].label; - opt.value = deviceInfos[i].deviceId; - audioEle.appendChild(opt); - audioEle.videoTarget = vid; - if (vid.sinkId) { - if (vid.sinkId == deviceInfos[i].deviceId) { - opt.selected = true; - } - } else if (vid.manualSink) { - if (vid.manualSink == deviceInfos[i].deviceId) { - opt.selected = true; - } - } else if (session.sink) { - if (session.sink == deviceInfos[i].deviceId) { - opt.selected = true; - } - } - } - } - audioEle.onchange = function () { - vid.manualSink = this.options[this.selectedIndex].value; - if (this.videoTarget && this.videoTarget.dataset.UUID) { - session.audioEffects = true; - updateIncomingAudioElement(this.videoTarget.dataset.UUID); - } - resetupAudioOut(this.videoTarget); - }; - return audioEle; -} - -function popupMessage(e, message = "Copied to Clipboard") { - // right click menu - - var posx = 0; - var posy = 0; - - if (!e) var e = window.event; - - if (e.pageX || e.pageY) { - posx = e.pageX; - posy = e.pageY; - } else if (e.clientX || e.clientY) { - posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; - } - - posx += 10; - - var menu = getById("messagePopup"); - menu.innerHTML = "
    " + message + "
    "; - var menuState = 0; - var menuWidth; - var menuHeight; - var menuPosition; - var menuPositionX; - var menuPositionY; - - var windowWidth; - var windowHeight; - - if (menuState !== 1) { - menuState = 1; - menu.classList.add("context-menu--active"); - } - - menuWidth = menu.offsetWidth + 4; - menuHeight = menu.offsetHeight + 4; - - windowWidth = window.innerWidth; - windowHeight = window.innerHeight; - - if (windowWidth - posx < menuWidth) { - menu.style.left = windowWidth - menuWidth + "px"; - } else { - menu.style.left = posx + "px"; - } - - if (windowHeight - posy < menuHeight) { - menu.style.top = windowHeight - menuHeight + "px"; - } else { - menu.style.top = posy + "px"; - } - - function toggleMenuOff() { - if (menuState !== 0) { - menuState = 0; - menu.classList.remove("context-menu--active"); - } - } - menu.classList.remove("fadeout"); - - var showlength = message.length * 50 || 500; - - setTimeout(function () { - menu.classList.add("fadeout"); - }, showlength); - - setTimeout(function () { - toggleMenuOff(); - }, showlength + 1000); -} - -function timeSince(date) { - var seconds = Math.floor((new Date() - date) / 1000); - - var interval = seconds / 31536000; - - if (interval > 1) { - return Math.floor(interval) + " years"; - } - interval = seconds / 2592000; - if (interval > 1) { - return Math.floor(interval) + " months"; - } - interval = seconds / 86400; - if (interval > 1) { - return Math.floor(interval) + " days"; - } - interval = seconds / 3600; - if (interval > 1) { - return Math.floor(interval) + " hours"; - } - interval = seconds / 60; - if (interval > 1) { - return Math.floor(interval) + " minutes"; - } - return "Seconds ago"; -} - -var messageList = []; -function sendChatMessage(chatMsg = false, bc = false) { - // filtered + visual - var data = {}; - if (chatMsg === false) { - var msg = document.getElementById("chatInput").value; - } else { - var msg = chatMsg; - } - //msg = sanitizeChat(msg); - if (msg == "") { - return false; - } - - msg = convertShortcodes(msg); - - var label = ""; - if (session.label) { - if (session.director) { - label = "" + session.label + ": "; - } else { - label = "" + session.label + ": "; - } - } else if (session.director) { - label = "Director: "; - } - - if (msg.trim() === "/list") { - var listMsg = null; - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].label) { - listMsg = UUID + ": " + session.rpcs[UUID].label; - } else if (session.directorList.indexOf(UUID) >= 0) { - listMsg = UUID + ": Director"; - } else { - listMsg = UUID + ": Unknown User"; - } - var data = {}; - data.msg = listMsg; - data.label = false; - data.type = "alert"; - data.time = Date.now(); - messageList.push(data); - } - for (var UUID in session.pcs) { - if (UUID in session.rpcs) { - continue; - } - if (session.pcs[UUID].label) { - listMsg = UUID + "; " + session.pcs[UUID].label; - } else if (session.directorList.indexOf(UUID) >= 0) { - listMsg = UUID + "; Director"; - } else { - listMsg = UUID + "; Unknown User"; - } - var data = {}; - data.msg = listMsg; - data.label = false; - data.type = "alert"; - data.time = Date.now(); - messageList.push(data); - } - if (listMsg === null) { - data.msg = "No other users are connected to you"; - data.label = false; - data.type = "alert"; - data.time = Date.now(); - messageList.push(data); - } - } else if (msg.startsWith("/msg ")) { - var msg = msg.split("/msg ")[1]; - msg = msg.split(" "); - uid = msg.shift().toLowerCase(); - msg = msg.join(" "); - if (msg == "") { - return false; - } - var sent = false; - for (var UUID in session.rpcs) { - if (UUID.startsWith(uid)) { - sendChat(msg, UUID); // send message to peers - var data = {}; - data.time = Date.now(); - data.msg = sanitizeChat(msg); // this is what the other person should see - data.label = label; - data.type = "sent"; - messageList.push(data); - sent = true; - } else if (session.rpcs[UUID].label && session.rpcs[UUID].label.toLowerCase().startsWith(uid)) { - sendChat(msg, UUID); // send message to peers - var data = {}; - data.time = Date.now(); - data.msg = sanitizeChat(msg); // this is what the other person should see - data.label = label; - data.type = "sent"; - messageList.push(data); - sent = true; - } else if (session.directorList.indexOf(UUID) >= 0 && "director".startsWith(uid)) { - sendChat(msg, UUID); // send message to peers - var data = {}; - data.time = Date.now(); - data.msg = sanitizeChat(msg); // this is what the other person should see - data.label = label; - data.type = "sent"; - messageList.push(data); - sent = true; - } - } - for (var UUID in session.pcs) { - if (UUID in session.rpcs) { - continue; - } - if (UUID.startsWith(uid)) { - sendChat(msg, UUID); // send message to peers - var data = {}; - data.time = Date.now(); - data.msg = sanitizeChat(msg); // this is what the other person should see - data.label = label; - data.type = "sent"; - messageList.push(data); - sent = true; - } else if (session.pcs[UUID].label && session.pcs[UUID].label.toLowerCase().startsWith(uid)) { - sendChat(msg, UUID); // send message to peers - var data = {}; - data.time = Date.now(); - data.msg = sanitizeChat(msg); // this is what the other person should see - data.label = label; - data.type = "sent"; - messageList.push(data); - sent = true; - } else if (session.directorList.indexOf(UUID) >= 0 && "director".startsWith(uid)) { - sendChat(msg, UUID); // send message to peers - var data = {}; - data.time = Date.now(); - data.msg = sanitizeChat(msg); // this is what the other person should see - data.label = label; - data.type = "sent"; - messageList.push(data); - sent = true; - } - } - if (sent == false) { - var data = {}; - data.msg = "No user found. Message not sent."; - data.label = false; - data.type = "alert"; - data.time = Date.now(); - messageList.push(data); - updateMessages(); - return false; - } - } else if (msg.startsWith("/")) { - data.msg = "Unknown command. Try '/list' or '/msg username message'."; - data.label = false; - data.type = "alert"; - data.time = Date.now(); - messageList.push(data); - updateMessages(); - return false; - } else if (session.directorChat === true) { - if (session.directorList.length) { - for (var i = 0; i < session.directorList.length; i++) { - sendChat(msg, session.directorList[i]); // send message to peers - } - var data = {}; - data.time = Date.now(); - data.msg = sanitizeChat(msg); // this is what the other person should see - data.label = label; - data.type = "sent"; - messageList.push(data); - } - } else { - sendChat(msg); // send message to peers - data.time = Date.now(); - data.msg = sanitizeChat(msg); // this is what the other person should see - data.label = label; - data.type = "sent"; - messageList.push(data); - } - document.getElementById("chatInput").value = ""; - - messageList = messageList.slice(-100); - if (!bc && session.broadcastChannel !== false) { - log(session.broadcastChannel); - session.broadcastChannel.postMessage(data); - } - updateMessages(); - - if (isIFrame) { - parent.postMessage( - { - chat: data - }, - session.iframetarget - ); - } - - var apiBlob = {}; - apiBlob.time = data.time; - apiBlob.msg = msg; - apiBlob.label = session.label; - apiBlob.type = data.type; - pokeAPI("chat", apiBlob); - - return true; -} - -function disableQualityDirector(UUID) { - // lets revert back to the director's quality settings after viewing the scene - try { - var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.add("disable"); - elements[0].ariaPressed = "false"; - elements[0].classList.remove("pressed"); - elements[0].disabled = "true"; - elements[0].title = getTranslation("preview-meshcast-disabled"); - } - var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.add("disable"); - elements[0].ariaPressed = "false"; - elements[0].classList.remove("pressed"); - elements[0].disabled = "true"; - elements[0].title = getTranslation("preview-meshcast-disabled"); - } - var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.add("disable"); - elements[0].ariaPressed = "false"; - elements[0].classList.remove("pressed"); - elements[0].disabled = "true"; - elements[0].title = getTranslation("preview-meshcast-disabled"); - } - } catch (e) { - errorlog(e); - } -} - -function applyQualityDirector(uuid = false) { - // lets revert back to the director's quality settings after viewing the scene - if (uuid) { - var eles = document.querySelectorAll('#guestFeeds button.pressed[data-action-type="change-quality1"][data--u-u-i-d="' + uuid + '"],#guestFeeds button.pressed[data-action-type="change-quality2"][data--u-u-i-d="' + uuid + '"],#guestFeeds button.pressed[data-action-type="change-quality3"][data--u-u-i-d="' + uuid + '"]'); - eles.forEach(ele => { - ele.click(); - }); - } else { - var eles = document.querySelectorAll('#guestFeeds button.pressed[data-action-type="change-quality1"],#guestFeeds button.pressed[data-action-type="change-quality2"],#guestFeeds button.pressed[data-action-type="change-quality3"]'); - eles.forEach(ele => { - ele.click(); - }); - } -} - -function toggleQualityDirector(bitrate, UUID, ele) { - // ele is specific to the button in the director's room - var eles = ele.parentNode.childNodes; - for (var i = 0; i < eles.length; i++) { - eles[i].className = ""; - } - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - session.requestRateLimit(bitrate, UUID); -} - -var clockOverlayTimer = null; -function zpadTime(number) { - var output = "" + number; - while (output.length < 2) { - output = "0" + output; - } - return output; -} -function showClock() { - getById("overlayClockContainer").classList.remove("hidden"); -} -function hideClock() { - getById("overlayClockContainer").classList.add("hidden"); -} -function setClock(initial = false, color = "#000") { - if (initial !== false) { - initial = parseInt(initial); - getById("overlayClockContainer").dataset.initial = initial; - } else { - initial = parseInt(getById("overlayClockContainer").dataset.initial); - } - - if (initial < 0) { - initial = 0; - } - - updateClock(initial, color); -} -function stopClock() { - var clock = document.getElementById("overlayClock"); - //clock.ctx = null; - //clock.canvas = null; - - //if (document.pictureInPictureElement && clock.video) { - // if (document.pictureInPictureElement == clock.video){ - // document.exitPictureInPicture(); - // pokeIframeAPI('picture-in-picture', false); - // } - //clock.video.remove; - //} - clock.innerHTML = ""; - clearInterval(clockOverlayTimer); - //setClock(); - updateClock("0", "#444"); -} -function pauseClock() { - clearInterval(clockOverlayTimer); - var current = Date.now() - parseInt(getById("overlayClockContainer").dataset.timestamp); - - if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) { - current = parseInt(Math.round(current / 1000)); - } else { - current = parseInt(getById("overlayClockContainer").dataset.initial) - parseInt(Math.round(current / 1000)); - } - - getById("overlayClockContainer").dataset.current = current; - - updateClock(current, "#00F"); -} -function resumeClock() { - if ("current" in getById("overlayClockContainer").dataset) { - startClock(parseInt(getById("overlayClockContainer").dataset.current)); - } -} -function startClock(restart = true) { - clearInterval(clockOverlayTimer); - if (restart === true) { - getById("overlayClockContainer").dataset.timestamp = Date.now(); - } else if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) { - getById("overlayClockContainer").dataset.timestamp = Date.now() - parseInt(getById("overlayClockContainer").dataset.current * 1000); - } else { - getById("overlayClockContainer").dataset.timestamp = Date.now() - (parseInt(getById("overlayClockContainer").dataset.initial) * 1000 - parseInt(getById("overlayClockContainer").dataset.current * 1000)); - } - stepClock(); - var clock = document.getElementById("overlayClock"); - if (clock && clock.video) { - clock.innerHTML = ""; - clock.appendChild(clock.video); - clock.video.play(); - } - - clockOverlayTimer = setInterval(function () { - stepClock(); - }, 999); -} -function stepClock() { - var current = Date.now() - parseInt(getById("overlayClockContainer").dataset.timestamp); - if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) { - current = parseInt(Math.round(current / 1000)); - } else { - current = parseInt(getById("overlayClockContainer").dataset.initial) - parseInt(Math.round(current / 1000)); - } - - if (session.directorList.length) { - var msg = {}; - msg.timer = current; - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - } - if (current < 0 && current % 2) { - updateClock(0, "#F00"); - } else if (current < 0) { - updateClock(0, "#000"); - } else { - updateClock(current, "#000"); - } -} - -function updateClock(timeleft, color = "#000") { - var minutes = Math.floor(timeleft / 60); - var seconds = timeleft % 60; - - var clock = document.getElementById("overlayClock"); - if (clock.ctx) { - clock.ctx.beginPath(); - clock.ctx.rect(0, 0, 230, 40); - clock.ctx.fillStyle = color; - clock.ctx.fill(); - clock.ctx.fillStyle = "#FFF"; - clock.ctx.textAlign = "center"; - clock.ctx.font = "50px monospace"; - clock.ctx.fillText(zpadTime(minutes) + ":" + zpadTime(seconds), 115, 37); - } else { - clock.innerHTML = zpadTime(minutes) + ":" + zpadTime(seconds); - clock.style.backgroundColor = color + "9"; - } -} -function popOutClock(clock) { - if (!clock.ctx) { - var canvas = document.createElement("canvas"); - canvas.width = "230"; - canvas.height = "40"; - var ctx = canvas.getContext("2d"); - clock.canvas = canvas; - clock.ctx = ctx; - - ctx.beginPath(); - ctx.rect(0, 0, 230, 40); - ctx.fillStyle = "#000"; - ctx.fill(); - ctx.fillStyle = "#FFF"; - ctx.font = "50px monospace"; - ctx.textAlign = "center"; - ctx.fillText(clock.innerHTML, 115, 37); - clock.video = document.createElement("video"); - clock.innerHTML = ""; - clock.appendChild(clock.video); - clock.video.onloadedmetadata = function () { - togglePictureInPicture(clock.video); - }; - - clock.video.srcObject = canvas.captureStream(); - clock.video.play(); - //clock.video.dataset.menu = "context-menu-clock"; - } else { - clock.innerHTML = ""; - clock.appendChild(clock.video); - clock.video.play(); - togglePictureInPicture(clock.video); - } -} - -session.popupChat = null -async function createPopoutChat() { - if (session.popupChat && !session.popupChat.closed) { - session.popupChat.focus(); - return; - } - - if (session.broadcastChannelID === false) { - session.broadcastChannelID = session.generateStreamID(8); - log(session.broadcastChannelID); - - session.broadcastChannel = new BroadcastChannel(session.broadcastChannelID); - session.broadcastChannel.onmessage = function (e) { - if ("loaded" in e.data) { - session.broadcastChannel.postMessage({ - messageList: messageList - }); - } else if ("msg" in e.data) { - sendChatMessage(e.data.msg, true); - } - }; - - session.broadcastChannel.onmessageerror = function (e) { - errorlog(e); - }; - } - - let params = { - broadcastChannelID: session.broadcastChannelID, - room: session.roomid || false, - view: session.view_set ? [...session.view_set, session.streamID].join(",") : (session.roomid ? false : session.streamID), - label: session.label || false, - password: session.password - }; - - function encrypt(text, key) { - const textEncoder = new TextEncoder(); - const encodedText = textEncoder.encode(text); - const encodedKey = textEncoder.encode(key); - - const encrypted = encodedText.map((byte, i) => - byte ^ encodedKey[i % encodedKey.length] - ); - - return btoa(String.fromCharCode.apply(null, encrypted)); - } - - async function generateSecureUrl(params) { - const ENCRYPTION_KEY = 'your32characterlongencryptionkey!!'; - - const filteredParams = Object.fromEntries( - Object.entries(params).filter(([_, v]) => v != null && v !== undefined) - ); - const paramsString = JSON.stringify(filteredParams); - const encrypted = encrypt(paramsString, ENCRYPTION_KEY); - return `./popout.html?id=${session.broadcastChannelID}&data=${encodeURIComponent(encrypted)}`; - } - - let srcString = await generateSecureUrl(params); - log(srcString); - - session.popupChat = window.open(srcString, "popup", "width=600,height=480,toolbar=no,menubar=no,resizable=yes"); - - session.popupChat.document.body.style.margin = "0"; - session.popupChat.document.body.style.backgroundColor = "#000"; - session.popupChat.document.body.style.padding = "0"; - session.popupChat.document.body.style.overflow = "hidden"; - session.popupChat.document.title = "Chat pop-out"; - - const style = session.popupChat.document.createElement('style'); - style.textContent = ` - @keyframes pulse { - 0% { background-color: #000; } - 50% { background-color: #333; } - 100% { background-color: #000; } - } - body { - animation: pulse 2s ease-in-out infinite; - } - `; - session.popupChat.document.head.appendChild(style); - - return false; -} - -function replaceURLs(message) { - if (!message) return; - var urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g; - return message.replace(urlRegex, function (url) { - url = url.replace(//g, ">").replace(/["']/g, ""); // try to sanitize things, just in case. - - var punc = ""; - while (url[url.length - 1] === ".") { - url = url.slice(0, -1); - punc += "."; - } - while (url[url.length - 1] === ";") { - url = url.slice(0, -1); - punc += ";"; - } - while (url[url.length - 1] === ",") { - url = url.slice(0, -1); - punc += ","; - } - while (url[url.length - 1] === "!") { - url = url.slice(0, -1); - punc += "!"; - } - while (url[url.length - 1] === ":") { - url = url.slice(0, -1); - punc += ":"; - } - while (url[url.length - 1] === "*") { - url = url.slice(0, -1); - punc += "*"; - } - while (url[url.length - 1] === ")") { - url = url.slice(0, -1); - punc += ")"; - } - while (url[url.length - 1] === "?") { - url = url.slice(0, -1); - punc += "?"; - } - - var hyperlink = url; - if (!hyperlink.match("^https?://")) { - hyperlink = "http://" + hyperlink; - } - if (url.length > 35) { - url = url.substring(0, 35) + "..."; - } - return '' + url + "" + punc; - }); -} - -function getChatMessage(msg, label = false, director = false, overlay = false, UUID = false) { - msg = sanitizeChat(msg); // keep it clean. - if (msg == "") { - return; - } - - data = {}; - data.time = Date.now(); - - var apiBlob = {}; - apiBlob.time = data.time; - apiBlob.msg = msg; - apiBlob.label = label; - apiBlob.type = "recv"; - - if (UUID !== false) { - apiBlob.UUID = UUID; - if (UUID in session.rpcs) { - apiBlob.streamID = session.rpcs[UUID].streamID || false; - } - } - - const streamFallbackLabel = (!label && session.director && UUID && session.rpcs[UUID] && session.rpcs[UUID].streamID) - ? getPeerDisplayName(UUID, false, false) - : false; - - if (label) { - label = sanitizeLabel(label); - } - - data.msg = msg; - if (label) { - data.label = label; - if (director) { - data.label = "" + data.label + ": "; - } else { - data.label = "" + data.label + ": "; - } - label = "" + label + ":"; // label+":"; - } else if (director) { - data.label = "Director: "; - label = "Director:"; - } else { - if (session.director) { - const fallback = streamFallbackLabel || "Someone"; - data.label = "" + fallback + ": "; - if (streamFallbackLabel) { - label = "" + fallback + ":"; - } else { - label = ""; - } - } else { - data.label = ""; - label = ""; - } - } - data.type = "recv"; - - if (overlay) { - if (!(session.cleanOutput && session.cleanish == false)) { - var textOverlay = getById("overlayMsgs"); - if (textOverlay) { - if (overlay == 2) { - // Clear previous persistent messages - while (textOverlay.querySelector('.persistent-overlay')) { - textOverlay.removeChild(textOverlay.querySelector('.persistent-overlay')); - } - var spanOverlay = document.createElement("span"); - spanOverlay.className = 'persistent-overlay'; - spanOverlay.innerHTML = "" + label + " " + msg + "
    "; - textOverlay.appendChild(spanOverlay); - textOverlay.style.display = "block"; - } else { - var spanOverlay = document.createElement("span"); - spanOverlay.innerHTML = "" + label + " " + msg + "
    "; - textOverlay.appendChild(spanOverlay); - textOverlay.style.display = "block"; - var showtime = msg.length * 200 + 3000; - if (showtime > 8000) { - showtime = 8000; - } - setTimeout( - function (ele) { - try { - ele.parentNode.removeChild(ele); - } catch (e) { } - }, - showtime, - spanOverlay - ); - } - } - } - } - - if (isIFrame) { - parent.postMessage( - { - gotChat: data, // deprecated - chat: data - }, - session.iframetarget - ); - } - - pokeAPI("chat", apiBlob); - - if (session.chatbutton === false) { - return; - } // messages can still appear as overlays ^ - - messageList.push(data); - messageList = messageList.slice(-100); - - if (session.beepToNotify) { - playtone(); - showNotification("new message", msg); - } - updateMessages(); - - if (session.chat == false) { - getById("chattoggle").className = "las la-comments toggleSize pulsate"; - getById("chatbutton").className = "float"; - - if (getById("chatNotification").value) { - getById("chatNotification").value = getById("chatNotification").value + 1; - } else { - getById("chatNotification").value = 1; - } - getById("chatNotification").classList.add("notification", "red"); - } - - if (session.broadcastChannel !== false) { - session.broadcastChannel.postMessage(data); /* send */ - } -} - -function rainbow(step, colours) { - var r, g, b; - var h = 1 - step / colours; - var i = ~~(h * 6); - var f = h * 6 - i; - var q = 1 - f; - switch (i % 6) { - case 0: - (r = 1), (g = f), (b = 0); - break; - case 1: - (r = q), (g = 1), (b = 0); - break; - case 2: - (r = 0), (g = 1), (b = f); - break; - case 3: - (r = 0), (g = q), (b = 1); - break; - case 4: - (r = f), (g = 0), (b = 1); - break; - case 5: - (r = 1), (g = 0), (b = q); - break; - } - var c = "#" + ("00" + (~~(r * 200 + 35)).toString(16)).slice(-2) + ("00" + (~~(g * 200 + 35)).toString(16)).slice(-2) + ("00" + (~~(b * 200 + 35)).toString(16)).slice(-2); - return c; -} - -function getColorFromName(str, colorseed = false, totalcolors = false) { - var out = 0, - len = str.length; - if (len > 6) { - len = 6; - } - - var seed = 26; - if (colorseed) { - seed = colorseed || 1; - } - - for (var pos = 0; pos < len; pos++) { - out += (str.charCodeAt(pos) - 64) * Math.pow(seed, len - pos - 1); - } - - var colours = 167772; - - if (totalcolors) { - colours = totalcolors; - if (colours > 167772) { - colours = 167772; - } else if (colours < 1) { - colours = 1; - } - } - - out = parseInt(out % colours); // get modulus - - if (colours === 1) { - return "#F00"; - } else if (colours === 2) { - switch (out) { - case 0: - return "#F00"; - case 1: - return "#00ABFA"; - } - } else if (colours === 3) { - switch (out) { - case 0: - return "#F00"; - case 1: - return "#00A800"; - case 2: - return "#00ABFA"; - } - } else if (colours === 4) { - switch (out) { - case 0: - return "#F00"; - case 1: - return "#FFA500"; - case 2: - return "#00A800"; - case 3: - return "#00ABFA"; - } - } else if (colours === 5) { - switch (out) { - case 0: - return "#F00"; - case 1: - return "#FFA500"; - case 2: - return "#00A800"; - case 3: - return "#00ABFA"; - case 4: - return "#FF39C5"; - } - } else { - out = rainbow(out, colours); - } - return out; -} - -function updateClosedCaptions(msg, label, UUID) { - - if (!session.rpcs[UUID].color && session.ccColored) { - session.rpcs[UUID].color = getColorFromName(UUID); - } - - msg.counter = parseInt(msg.counter); - var temp = document.createElement("div"); - temp.innerText = msg.transcript; - temp.innerText = temp.innerHTML; - var transcript = temp.textContent || temp.innerText || ""; - - if (transcript == "") { - return; - } - - transcript = transcript.charAt(0).toUpperCase() + transcript.slice(1); - //transcript = transcript.substr(-1, 5000); // keep it from being too long - - if (session.nocaptionlabels) { - label = ""; - } else if (label && !(session.view && !session.view_set)) { - label = sanitizeLabel(label); - label = "" + label + ": "; - } else { - label = ""; - } - - var textOverlay = getById("overlayMsgs"); - if (textOverlay) { - if (document.getElementById(UUID + "_" + msg.counter)) { - var spanOverlay = document.getElementById(UUID + "_" + msg.counter); - } else { - var spanOverlay = document.createElement("span"); - spanOverlay.id = UUID + "_" + msg.counter; - textOverlay.appendChild(spanOverlay); - textOverlay.style.height = "unset"; - textOverlay.style.textAlign = "left"; - textOverlay.style.display = "block"; - textOverlay.style.position = "fixed"; - textOverlay.style.bottom = "0"; - } - - spanOverlay.innerHTML = label + transcript + "
    "; - spanOverlay.style.fontSize = (parseInt(session.labelsize || 100) / 100.0) * 4.5 + "vh"; - spanOverlay.style.lineHeight = (parseInt(session.labelsize || 100) / 100) * 6 + "vh"; - spanOverlay.style.margin = (parseInt(session.labelsize || 100) / 100.0) * 0.75 + "vh"; - - if (session.rpcs[UUID].color && session.ccColored) { - spanOverlay.style.color = session.rpcs[UUID].color; - } - - if (msg.isFinal) { - var showtime = 3000; - clearTimeout(spanOverlay.timeout); - spanOverlay.timeout = setTimeout( - function (ele) { - ele.parentNode.removeChild(ele); - }, - showtime, - spanOverlay - ); - } else { - clearTimeout(spanOverlay.timeout); - spanOverlay.timeout = setTimeout( - function (ele) { - ele.parentNode.removeChild(ele); - }, - 30000, - spanOverlay - ); - } - } -} - -var chatUpdateTimeout = null; -function updateMessages() { - if (session.chatbutton === false) { - return; - } - - getById("chatNotification").classList.remove("notification", "red"); - if (session.chat) { - getById("chattoggle").classList.remove("pulsate"); - } - - const chatBody = document.getElementById("chatBody"); - chatBody.innerHTML = ""; - for (var i in messageList) { - var time = timeSince(messageList[i].time) || ""; - time = " - " + time + ""; - var msg = document.createElement("div"); - var message = replaceURLs(messageList[i].msg); - - if (messageList[i].type == "sent") { - msg.innerHTML = message + "" + time + ""; - msg.classList.add("outMessage"); - } else if (messageList[i].type == "recv" || messageList[i].type == "action") { - var label = ""; - if (messageList[i].label) { - label = messageList[i].label; - } - msg.innerHTML = label + message + "" + time + ""; - msg.classList.add("inMessage"); - } else if (messageList[i].type == "alert") { - msg.innerHTML = message + "" + time + ""; - msg.classList.add("inMessage"); - } else if (messageList[i].type == "tip") { - msg.innerHTML = message + "" + time + ""; - msg.classList.add("tipMessage"); - } else { - msg.innerHTML = message; - msg.classList.add("outMessage"); - } - - chatBody.appendChild(msg); - } - showDownloadLinks(); - for (var i in msgTransferList) { - var time = timeSince(msgTransferList[i].time) || ""; - time = " - " + time + ""; - - var msg = document.createElement("div"); - if ("idx" in msgTransferList[i]) { - msg.id = "transfer_" + msgTransferList[i].idx; - msg.classList.add("transfer"); - } - if (msgTransferList[i].type == "sent") { - msg.innerHTML = msgTransferList[i].msg + "" + time + ""; - msg.classList.add("outMessage"); - } else if (msgTransferList[i].type == "recv" || msgTransferList[i].type == "action") { - var label = ""; - if (msgTransferList[i].label) { - label = msgTransferList[i].label; - } - msg.innerHTML = label + msgTransferList[i].msg + "" + time + ""; - msg.classList.add("inMessage"); - } else if (msgTransferList[i].type == "alert") { - msg.innerHTML = msgTransferList[i].msg + "" + time + ""; - msg.classList.add("inMessage"); - } else { - msg.innerHTML = msgTransferList[i].msg; - msg.classList.add("outMessage"); - } - - if (msg.id && document.getElementById(msg.id)) { - document.getElementById(msg.id).innerHTML = msg.innerHTML; - } else { - chatBody.appendChild(msg); - } - } - if (chatUpdateTimeout) { - clearInterval(chatUpdateTimeout); - } - chatBody.scrollTop = chatBody.scrollHeight; - if (chatUpdateTimeout) { - clearTimeout(chatUpdateTimeout); - } - chatUpdateTimeout = setTimeout(updateMessages, 60000); -} - -function EnterButtonChat(event) { - // Number 13 is the "Enter" key on the keyboard - var key = event.which || event.keyCode; - if (key === 13) { - // Cancel the default action, if needed - event.preventDefault(); - // Trigger the button element with a click - sendChatMessage(); - } -} - -function showCustomizer(arg, ele) { - //getById("directorLinksButton").innerHTML=' LINKS (GUEST INVITES & SCENES)' - getById("showCustomizerButton1").style.backgroundColor = ""; - getById("showCustomizerButton2").style.backgroundColor = ""; - getById("showCustomizerButton3").style.backgroundColor = ""; - getById("showCustomizerButton4").style.backgroundColor = ""; - getById("showCustomizerButton1").style.boxShadow = ""; - getById("showCustomizerButton2").style.boxShadow = ""; - getById("showCustomizerButton3").style.boxShadow = ""; - getById("showCustomizerButton4").style.boxShadow = ""; - - if (getById("customizeLinks" + arg).style.display != "none") { - getById("customizeLinks").style.display = "none"; - getById("customizeLinks" + arg).style.display = "none"; - } else { - //directorLinks").style.display="none"; - getById("showCustomizerButton" + arg).style.backgroundColor = "#1e0000"; - getById("showCustomizerButton" + arg).style.boxShadow = "inset 0px 0px 1px #b90000"; - getById("customizeLinks1").style.display = "none"; - getById("customizeLinks3").style.display = "none"; - getById("customizeLinks").style.display = "block"; - getById("customizeLinks" + arg).style.display = "block"; - } -} - -function setPTTvalue() { - var key = ""; - if (PPTHotkey.ctrl) { - key += "Control"; - } - if (PPTHotkey.meta) { - if (key) { - key += " + "; - } - key += "Meta"; - } - if (PPTHotkey.alt) { - if (key) { - key += " + "; - } - key += "Alt"; - } - - if (PPTHotkey.key == "Control") { - // - } else if (PPTHotkey.key == "Alt") { - // - } else if (PPTHotkey.key == "Meta") { - // - } else if (PPTHotkey.key !== false) { - if (key) { - key += " + "; - } - if (PPTHotkey.key === " ") { - key += "Space"; - } else { - key += PPTHotkey.key; - } - } else if (key && navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - getById("pptHotKey").title = "Note: Global hot-keys can't simply be Control, Alt, or Meta keys."; - } - getById("pptHotKey").value = key; - - try { - if (window.electronApi && window.electronApi.updatePPT) { - window.electronApi.updatePPT(PPTHotkey); - } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - if (!ipcRenderer) { - ipcRenderer = require("electron").ipcRenderer; - } - if (ipcRenderer) { - ipcRenderer.send("PPTHotkey", PPTHotkey); - } - } - } catch (e) { - errorlog(e); - } -} - -var PPTHotkey = getStorage("PPTHotkey") || false; -if (PPTHotkey) { - setPTTvalue(); -} - -function setHotKeyAuto(hotkeyInput) { - PPTHotkey = { - ctrl: false, - alt: false, - meta: false, - key: false - }; - - var key = ""; - - if (hotkeyInput) { - const modifiers = hotkeyInput.replaceAll(" ", "+").split("+"); - modifiers.forEach(modifier => { - const trimmedModifier = modifier.trim().toLowerCase(); - if (trimmedModifier === "control") { - PPTHotkey.ctrl = true; - key += "Control"; - } else if (trimmedModifier === "ctrl") { - PPTHotkey.ctrl = true; - key += "Control"; - } else if (trimmedModifier === "alt") { - PPTHotkey.alt = true; - key += "Alt"; - } else if (trimmedModifier === "meta") { - PPTHotkey.meta = true; - key += "Meta"; - } - }); - var lastKey = modifiers.pop().trim(); - PPTHotkey.key = lastKey; - - if (lastKey || lastKey === " " || lastKey === 0) { - if (key) { - key += " + "; - } - if (lastKey === " ") { - key += "Space"; - } else { - key += lastKey; - } - } - } else { - PPTHotkey.ctrl = true; - PPTHotkey.key = "m"; - PPTHotkey.meta = true; - key = "Control + Alt + m"; - } - - setStorage("PPTHotkey", PPTHotkey, 99999); - getById("pptHotKey").value = key; - - try { - if (window.electronApi && window.electronApi.updatePPT) { - window.electronApi.updatePPT(PPTHotkey); - } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - if (!ipcRenderer) { - ipcRenderer = require("electron").ipcRenderer; - } - if (ipcRenderer) { - ipcRenderer.send("PPTHotkey", PPTHotkey); - } - } - } catch (e) { - errorlog(e); - } -} - -function setHotKey(keyinput = true) { - if (!keyinput) { - // clears if false - getById("pptHotKey").value = ""; - getById("pptHotKey0").value = ""; - PPTHotkey = false; - removeStorage("PPTHotkey"); - - try { - if (window.electronApi && window.electronApi.updatePPT) { - window.electronApi.updatePPT(PPTHotkey); - } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - if (!ipcRenderer) { - ipcRenderer = require("electron").ipcRenderer; - } - if (ipcRenderer) { - ipcRenderer.send("PPTHotkey", PPTHotkey); - } - } - } catch (e) { - errorlog(e); - } - - return; - } - - PPTHotkey = { - ctrl: false, - alt: false, - meta: false, - key: false - }; - - log(event); - var key = ""; - if (event.ctrlKey) { - key += "Control"; - PPTHotkey.ctrl = true; - } - if (event.metaKey) { - if (key) { - key += " + "; - } - key += "Meta"; - PPTHotkey.meta = true; - } - if (event.altKey) { - if (key) { - key += " + "; - } - key += "Alt"; - PPTHotkey.alt = true; - } - - if (event.key == "Control") { - // - } else if (event.key == "Alt") { - // - } else if (event.key == "Meta") { - // - } else if (event.key || event.key === " " || event.key === 0) { - if (key) { - key += " + "; - } - if (event.key === " ") { - key += "Space"; - } else { - key += event.key; - } - PPTHotkey.key = event.key; - } - setStorage("PPTHotkey", PPTHotkey, 99999); - event.target.value = key; - - try { - if (window.electronApi && window.electronApi.updatePPT) { - window.electronApi.updatePPT(PPTHotkey); - } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - if (!ipcRenderer) { - ipcRenderer = require("electron").ipcRenderer; - } - if (ipcRenderer) { - ipcRenderer.send("PPTHotkey", PPTHotkey); - } - } - } catch (e) { - errorlog(e); - } - - getById("pptHotKey").value = event.target.value; - getById("pptHotKey0").value = event.target.value; - - event.preventDefault(); - event.stopPropagation(); - return false; -} - -function setupGoogleDriveUploader(filename = false, sessionUri = false) { - if (!session.gdrive) { - session.gdrive = {}; - session.gdrive.accessToken = false; - } - - var gdrive = {}; - var uploading = false; - var tokenClient; - var isInitialized = false; - var initializationPromise; - - const SCOPES = "https://www.googleapis.com/auth/drive.file"; - - var totalChunksRecorded = 0; - var totalChunksUploaded = 0; - var currentByte = 0; - var chunks = new Blob([]); - var finalized = false; - - gdrive.promise = false; - gdrive.sessionUri = sessionUri; - - // Create an initialization promise to track when everything is ready - initializationPromise = new Promise((resolve, reject) => { - // We'll resolve this when the token client is fully initialized - if (!gdrive.sessionUri) { - loadScript("https://accounts.google.com/gsi/client", function () { - log("Google Identity Services loaded"); - initTokenClient(); - resolve(); - }); - } else { - resolve(); - } - }); - - // Setup the authentication promise - if (!filename && !sessionUri) { - var res, rej; - gdrive.promise = new Promise((resolve, reject) => { - res = resolve; - rej = reject; - }); - gdrive.promise.resolve = res; - gdrive.promise.reject = rej; - } - - gdrive.startResumableUpload = async function (fname, retry = true) { - console.log("startResumableUpload", retry); - - const fileMetadata = { name: fname }; - - if (session.GDRIVE_FOLDERNAME) { - let folderId = null; - - const query = `name = '${session.GDRIVE_FOLDERNAME}' and mimeType = 'application/vnd.google-apps.folder' and 'root' in parents and trashed = false`; - const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}`; - - const response = await fetch(url, { - method: "GET", - headers: { - Authorization: "Bearer " + session.gdrive.accessToken - } - }); - - const result = await response.json(); - - if (result.files && result.files.length > 0) { - folderId = result.files[0].id; - } - - if (!folderId) { - log("creating new folder as folder not found."); - try { - const folderMetadata = { - name: session.GDRIVE_FOLDERNAME, - mimeType: "application/vnd.google-apps.folder" - }; - - const createResponse = await fetch("https://www.googleapis.com/drive/v3/files", { - method: "POST", - headers: { - Authorization: "Bearer " + session.gdrive.accessToken, - "Content-Type": "application/json" - }, - body: JSON.stringify(folderMetadata) - }); - - const createResult = await createResponse.json(); - folderId = createResult.id; - } catch (e) { - errorlog(e); - } - } - - if (folderId) { - fileMetadata.parents = [folderId]; - } - } - - const metadata = new Blob([JSON.stringify(fileMetadata)], { type: "application/json" }); - try { - var response = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable", { - method: "POST", - headers: { - Authorization: "Bearer " + session.gdrive.accessToken, - "Content-Type": "application/json; charset=UTF-8" - }, - body: metadata - }); - if (!response.ok) { - if (!session.cleanOutput) { - warnUser("⚠️ Error: Failed to configure the Google Drive upload."); - } - throw new Error("Start resumable upload failed: " + response.statusText); - } - return response.headers.get("Location"); // This is the session URI for the resumable upload - } catch (err) { - errorlog(err); - try { - if (retry) { - session.gdrive.accessToken = false; - var res, rej; - gdrive.promise = new Promise((resolve, reject) => { - res = resolve; - rej = reject; - }); - gdrive.promise.resolve = res; - gdrive.promise.reject = rej; - filename = false; - - // Make sure we're initialized before requesting token - await gdrive.ensureInitialized(); - tokenClient.requestAccessToken(); - await gdrive.promise; - - if (session.gdrive.accessToken) { - return await gdrive.startResumableUpload(fname, false); - } else { - return false; - } - } - } catch (err2) { - errorlog(err2); - return false; - } - } - }; - - function initTokenClient() { - console.log("Initializing GIS token client"); - if ( - typeof google === "undefined" || - !google.accounts || - !google.accounts.oauth2 - ) { - const gisError = new Error("Google Identity Services failed to load."); - errorlog(gisError); - if (!session.cleanOutput) { - warnUser("Google sign-in was blocked. Allow accounts.google.com and try again.", 8000); - } - if (gdrive.promise && gdrive.promise.reject) { - gdrive.promise.reject(gisError); - } - return; - } - tokenClient = google.accounts.oauth2.initTokenClient({ - client_id: session.GDRIVE_CLIENT_ID, - scope: SCOPES, - callback: onTokenResponse, - error_callback: onTokenError - }); - - isInitialized = true; - - // If we have no promise yet but the user requested access, set one up - if (!gdrive.promise && !sessionUri && !filename) { - var res, rej; - gdrive.promise = new Promise((resolve, reject) => { - res = resolve; - rej = reject; - }); - gdrive.promise.resolve = res; - gdrive.promise.reject = rej; - } - - // If we have a filename, start upload immediately when possible - if (filename) { - setTimeout(async () => { - try { - if (session.gdrive && session.gdrive.accessToken) { - console.log("Using cached Google Drive token for upload"); - gdrive.sessionUri = await gdrive.startResumableUpload(filename); - if (gdrive.sessionUri) { - uploadLoop(); - return; - } - console.warn("Failed to reuse cached Drive token, requesting a new one..."); - } - } catch (err) { - errorlog(err); - } - console.log("Requesting access token for immediate upload"); - tokenClient.requestAccessToken(); - }, 250); // Small delay to ensure tokenClient is fully initialized - } - } - - function onTokenError(response) { - console.warn("Token error:", response); - if (gdrive.promise && gdrive.promise.reject) { - gdrive.promise.reject(response); - } - } - - async function onTokenResponse(tokenResponse) { - console.log("Token response received", tokenResponse); - - if (tokenResponse.error === "popup_closed_by_user" || tokenResponse.error === "access_denied") { - errorlog("User cancelled the sign-in process."); - if (gdrive.promise && gdrive.promise.reject) { - gdrive.promise.reject(new Error("User cancelled authentication")); - } - } else if (tokenResponse.error !== undefined) { - errorlog("Token error: " + tokenResponse.error); - if (gdrive.promise && gdrive.promise.reject) { - gdrive.promise.reject(new Error(tokenResponse.error)); - } - } else { - // Successfully got access token - console.log("Access token obtained successfully"); - session.gdrive.accessToken = tokenResponse.access_token; - - if (filename) { - try { - gdrive.sessionUri = await gdrive.startResumableUpload(filename); - console.log("Session URI:", gdrive.sessionUri); - uploadLoop(); - } catch (e) { - console.error("Error starting upload:", e); - if (gdrive.promise && gdrive.promise.reject) { - gdrive.promise.reject(e); - } - return; - } - } - - // Always resolve the promise if we got a token successfully - if (gdrive.promise && gdrive.promise.resolve) { - console.log("Resolving promise with access token"); - gdrive.promise.resolve(tokenResponse.access_token); - } - } - } - - // Check if initialized and wait if not - gdrive.ensureInitialized = async function () { - if (!isInitialized) { - console.log("Waiting for initialization to complete..."); - await initializationPromise; - console.log("Initialization complete"); - } - }; - - // Function to manually request access token - gdrive.requestAccessToken = async function () { - await gdrive.ensureInitialized(); - - if (tokenClient) { - console.log("Manually requesting access token"); - tokenClient.requestAccessToken(); - } else { - console.error("Token client not initialized"); - if (gdrive.promise && gdrive.promise.reject) { - gdrive.promise.reject(new Error("Token client not initialized")); - } - } - }; - - gdrive.revokeToken = function () { - if (session.gdrive.accessToken) { - google.accounts.oauth2.revoke(session.gdrive.accessToken, () => { - console.log('Access token revoked'); - session.gdrive.accessToken = false; - }); - } - }; - - /// the following doesn't need to be signed in; just access to the gdrive.sessionUri URL - - gdrive.addChunk = function (chunk) { - if (chunk && chunks) { - totalChunksRecorded += chunk.size; - chunks = new Blob([chunks, chunk], { type: chunk.type }); - if (!session.cleanOutput) { - getById("progressContainer").classList.remove("hidden"); - } - updateProgressBar(); - } else if (chunk === false) { - finalized = true; - } - uploadLoop(); - }; - - async function uploadLoop() { - if (uploading || !gdrive.sessionUri) { - return; - } - uploading = true; - while (chunks && (finalized || chunks.size > 256 * 1024)) { - if (finalized) { - var chunk = chunks.slice(0, chunks.size); - let res = await finalizeUpload(chunk); - log(res); - return; - } else { - var chunkSize = Math.floor(chunks.size / (256 * 1024)) * (256 * 1024); - var chunk = chunks.slice(0, chunkSize); - chunks = chunks.slice(chunkSize); - } - currentByte = await uploadChunk(chunk); - } - uploading = false; - } - - async function uploadChunk(chunk) { - const endByte = currentByte + chunk.size - 1; - totalChunksUploaded += chunk.size; - const headers = new Headers({ - "Content-Range": `bytes ${currentByte}-${endByte}/*` - }); - const response = await fetch(gdrive.sessionUri, { - method: "PUT", - headers: headers, - body: chunk - }); - if (!response.ok && response.status !== 308) { - throw new Error(`Failed to upload chunk: ${response.statusText}`); - } - updateProgressBar(); - return endByte + 1; - } - - async function finalizeUpload(chunk) { - const endByte = currentByte + chunk.size - 1; - const headers = new Headers({ - "Content-Range": `bytes ${currentByte}-${endByte}/${endByte + 1}` - }); - const response = await fetch(gdrive.sessionUri, { - method: "PUT", - headers: headers, - body: chunk - }); - if (chunk) { - totalChunksUploaded += chunk.size; - } - updateProgressBar(2); - - return response.json(); - } - - function updateProgressBar(state = 0) { - // Implementation unchanged - if (state == 2) { - setTimeout(function () { - if (getById("progressBar").style.width == "100%") { - getById("progressContainer").classList.add("hidden"); - } - }, 1000); - getById("progressBar").style.width = "100%"; - var msg = {}; - msg.gdrive = { up: parseInt(totalChunksUploaded / 1024), rec: parseInt(totalChunksUploaded / 1024), state: state }; - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - } else if (totalChunksRecorded > 0) { - var progressPercentage = (totalChunksUploaded / (totalChunksRecorded || 1)) * 100; - var bytesLeft = parseInt((totalChunksRecorded - totalChunksUploaded) / 1024); - getById("progressBar").style.width = progressPercentage + "%"; - getById("progressBar").innerHTML = "Upload progress to Google Drive: " + progressPercentage.toFixed(2) + "%, with " + convertKilobytes(bytesLeft) + " left"; - - var msg = {}; - msg.gdrive = { up: parseInt(totalChunksUploaded / 1024), rec: parseInt(totalChunksRecorded / 1024), state: state }; - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - } - } - - return gdrive; -} - -function convertKilobytes(kilobytes) { - const KB_IN_MB = 1024; - const KB_IN_GB = 1024 * 1024; - - if (kilobytes >= KB_IN_GB) { - return Math.ceil(kilobytes / KB_IN_GB).toFixed(0) + " GB"; - } else if (kilobytes >= KB_IN_MB) { - return Math.ceil(kilobytes / KB_IN_MB).toFixed(0) + " MB"; - } else { - return kilobytes + "KB"; - } -} - -const DROPBOX_APP_KEY_FALLBACK = "uwxixfldkii1xpt"; -const DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize"; -const DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token"; -const DROPBOX_SCOPES = "files.content.write files.metadata.write"; -const DROPBOX_SDK_URL = "https://cdnjs.cloudflare.com/ajax/libs/dropbox.js/10.34.0/Dropbox-sdk.min.js"; -const DROPBOX_OAUTH_STORAGE_KEY = "dropboxOAuthTokens"; -const DROPBOX_OAUTH_SESSION_KEY = "dropboxOAuthSession"; -const DROPBOX_AUTH_MESSAGE_SOURCE = "vdoninja-dropbox-auth"; -const DROPBOX_ALLOWED_REDIRECT_ORIGINS = [ - "https://vdo.ninja", - "https://dev.versus.cam", - "https://versus.cam", - "https://backup.vdo.ninja", - "https://obs.ninja", - "http://localhost:8080" -]; -const DROPBOX_REFRESH_SKEW_MS = 120000; -var dropboxScriptPromise = null; -var dropboxInitPromise = null; -var dropboxAuthFlowPromise = null; -var dropboxAuthFlowResolver = null; -var dropboxAuthFlowRejecter = null; -var dropboxAuthWindow = null; -var dropboxAuthWindowMonitor = null; - -function getDropboxAppKey() { - if (typeof session !== "undefined" && session.DROPBOX_APP_KEY) { - return session.DROPBOX_APP_KEY; - } - return DROPBOX_APP_KEY_FALLBACK; -} - -function getDropboxRedirectUri() { - if (typeof window === "undefined" || !window.location || !window.location.origin) { - return DROPBOX_ALLOWED_REDIRECT_ORIGINS[0] + "/dropbox-auth.html"; - } - var origin = window.location.origin.replace(/\/+$/, ""); - if (DROPBOX_ALLOWED_REDIRECT_ORIGINS.indexOf(origin) === -1) { - return DROPBOX_ALLOWED_REDIRECT_ORIGINS[0] + "/dropbox-auth.html"; - } - return origin + "/dropbox-auth.html"; -} - -function persistDropboxAuthSession(sessionData) { - try { - localStorage.setItem(DROPBOX_OAUTH_SESSION_KEY, JSON.stringify(sessionData)); - } catch (e) { } -} - -function clearDropboxAuthSession() { - try { - localStorage.removeItem(DROPBOX_OAUTH_SESSION_KEY); - } catch (e) { } -} - -function getStoredDropboxOAuthTokens() { - if (typeof session !== "undefined" && session.dropboxOAuth) { - return session.dropboxOAuth; - } - try { - var raw = localStorage.getItem(DROPBOX_OAUTH_STORAGE_KEY); - if (!raw) { - return null; - } - var parsed = JSON.parse(raw); - if (parsed && typeof parsed === "object") { - if (typeof session !== "undefined") { - session.dropboxOAuth = parsed; - if (parsed.accessToken) { - session.dropboxAccessToken = parsed.accessToken; - } - } - return parsed; - } - } catch (e) { } - return null; -} - -function persistDropboxOAuthTokens(record) { - if (!record || !record.accessToken) { - return; - } - var existing = getStoredDropboxOAuthTokens(); - var normalized = { - accessToken: record.accessToken, - refreshToken: record.refreshToken || (existing && existing.refreshToken) || null, - expiresAt: record.expiresAt || (existing && existing.expiresAt) || 0, - scope: record.scope || (existing && existing.scope) || DROPBOX_SCOPES, - tokenType: record.tokenType || (existing && existing.tokenType) || "bearer" - }; - try { - localStorage.setItem(DROPBOX_OAUTH_STORAGE_KEY, JSON.stringify(normalized)); - } catch (e) { } - if (typeof session !== "undefined") { - session.dropboxOAuth = normalized; - session.dropboxAccessToken = normalized.accessToken; - } -} - -function clearDropboxOAuthTokens() { - if (typeof session !== "undefined") { - session.dropboxOAuth = null; - } - try { - localStorage.removeItem(DROPBOX_OAUTH_STORAGE_KEY); - } catch (e) { } -} - -function normalizeDropboxTokenResponse(response, fallbackRefreshToken = null) { - if (!response || typeof response !== "object" || !response.access_token) { - return null; - } - var expiresIn = 0; - if (response.expires_in) { - var parsed = parseInt(response.expires_in, 10); - if (!isNaN(parsed) && parsed > 0) { - expiresIn = parsed * 1000; - } - } - var expiresAt = expiresIn ? Date.now() + Math.max(0, expiresIn - DROPBOX_REFRESH_SKEW_MS) : 0; - return { - accessToken: response.access_token, - refreshToken: response.refresh_token || fallbackRefreshToken || null, - expiresAt: expiresAt, - scope: response.scope || DROPBOX_SCOPES, - tokenType: response.token_type || "bearer" - }; -} - -function dropboxTokenExpired(tokens) { - if (!tokens || !tokens.accessToken) { - return true; - } - if (!tokens.expiresAt) { - return false; - } - return Date.now() >= tokens.expiresAt; -} - -async function refreshDropboxAccessToken(existingTokens) { - if (!existingTokens || !existingTokens.refreshToken) { - throw new Error("Dropbox refresh token missing."); - } - var body = new URLSearchParams(); - body.set("grant_type", "refresh_token"); - body.set("refresh_token", existingTokens.refreshToken); - body.set("client_id", getDropboxAppKey()); - var response = await fetch(DROPBOX_TOKEN_URL, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body: body.toString() - }); - if (!response.ok) { - throw new Error("Failed to refresh Dropbox token."); - } - var data = await response.json(); - var normalized = normalizeDropboxTokenResponse(data, existingTokens.refreshToken); - if (!normalized) { - throw new Error("Dropbox refresh response invalid."); - } - persistDropboxOAuthTokens(normalized); - return normalized; -} - -async function ensureDropboxOAuthAccessToken({ interactive = false } = {}) { - var tokens = getStoredDropboxOAuthTokens(); - if (tokens && !dropboxTokenExpired(tokens)) { - return tokens; - } - if (tokens && tokens.refreshToken) { - try { - return await refreshDropboxAccessToken(tokens); - } catch (e) { - errorlog(e); - clearDropboxOAuthTokens(); - clearDropboxAuthSession(); - tokens = null; - } - } - if (!interactive) { - return null; - } - return beginDropboxOAuthFlow(); -} - -function generateRandomString(length = 64) { - var charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; - var result = ""; - if (typeof window !== "undefined" && window.crypto && window.crypto.getRandomValues) { - var values = new Uint32Array(length); - window.crypto.getRandomValues(values); - for (var i = 0; i < length; i++) { - result += charset[values[i] % charset.length]; - } - } else { - for (var j = 0; j < length; j++) { - result += charset[Math.floor(Math.random() * charset.length)]; - } - } - return result; -} - -function base64UrlEncode(arrayBuffer) { - var bytes = new Uint8Array(arrayBuffer); - var binary = ""; - for (var i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -} - -async function createDropboxPkcePair() { - var verifier = generateRandomString(64); - if (typeof window !== "undefined" && window.crypto && window.crypto.subtle && typeof TextEncoder !== "undefined") { - try { - var data = new TextEncoder().encode(verifier); - var digest = await window.crypto.subtle.digest("SHA-256", data); - return { verifier: verifier, challenge: base64UrlEncode(digest), method: "S256" }; - } catch (e) { } - } - return { verifier: verifier, challenge: verifier, method: "plain" }; -} - -function generateDropboxState() { - return generateRandomString(32); -} - -async function beginDropboxOAuthFlow() { - if (dropboxAuthFlowPromise) { - return dropboxAuthFlowPromise; - } - dropboxAuthFlowPromise = new Promise(async (resolve, reject) => { - dropboxAuthFlowResolver = resolve; - dropboxAuthFlowRejecter = reject; - try { - var pkce = await createDropboxPkcePair(); - var state = generateDropboxState(); - var redirectUri = getDropboxRedirectUri(); - var clientId = getDropboxAppKey(); - clearDropboxAuthSession(); - persistDropboxAuthSession({ - verifier: pkce.verifier, - state: state, - redirectUri: redirectUri, - clientId: clientId, - scope: DROPBOX_SCOPES, - origin: typeof window !== "undefined" && window.location ? window.location.origin : "", - ts: Date.now() - }); - var params = new URLSearchParams({ - response_type: "code", - client_id: clientId, - redirect_uri: redirectUri, - code_challenge: pkce.challenge, - code_challenge_method: pkce.method, - token_access_type: "offline", - state: state - }); - if (DROPBOX_SCOPES) { - params.set("scope", DROPBOX_SCOPES); - } - var authUrl = DROPBOX_AUTH_URL + "?" + params.toString(); - dropboxAuthWindow = window.open(authUrl, "vdoninja-dropbox-auth", "width=600,height=720"); - if (!dropboxAuthWindow) { - throw new Error("Dropbox authorization popup was blocked. Allow popups and try again."); - } - dropboxAuthWindowMonitor = setInterval(() => { - if (!dropboxAuthWindow || dropboxAuthWindow.closed) { - cleanupDropboxAuthFlow(new Error("Dropbox authorization window was closed."), true); - } - }, 750); - } catch (err) { - cleanupDropboxAuthFlow(err, true); - } - }); - return dropboxAuthFlowPromise; -} - -function cleanupDropboxAuthFlow(result, isError) { - if (dropboxAuthWindowMonitor) { - clearInterval(dropboxAuthWindowMonitor); - dropboxAuthWindowMonitor = null; - } - if (dropboxAuthWindow && !dropboxAuthWindow.closed) { - try { - dropboxAuthWindow.close(); - } catch (e) { } - } - dropboxAuthWindow = null; - var resolver = dropboxAuthFlowResolver; - var rejecter = dropboxAuthFlowRejecter; - dropboxAuthFlowResolver = null; - dropboxAuthFlowRejecter = null; - dropboxAuthFlowPromise = null; - if (isError) { - if (typeof rejecter === "function") { - rejecter(result instanceof Error ? result : new Error(result || "Dropbox authorization failed")); - } - } else if (typeof resolver === "function") { - resolver(result); - } -} - -function isAllowedDropboxAuthOrigin(origin) { - if (!origin) { - return false; - } - if (origin === window.location.origin) { - return true; - } - return DROPBOX_ALLOWED_REDIRECT_ORIGINS.indexOf(origin) !== -1; -} - -function dropboxAuthMessageHandler(event) { - if (!event || !event.data || !isAllowedDropboxAuthOrigin(event.origin)) { - return; - } - var data = event.data; - if (!data || data.source !== DROPBOX_AUTH_MESSAGE_SOURCE) { - return; - } - if (data.type === "request-session" && event.source && typeof event.source.postMessage === "function") { - var sessionPayload = null; - try { - var rawSession = localStorage.getItem(DROPBOX_OAUTH_SESSION_KEY); - if (rawSession) { - sessionPayload = JSON.parse(rawSession); - } - } catch (e) { - sessionPayload = null; - } - if (sessionPayload && data.state && sessionPayload.state && sessionPayload.state !== data.state) { - sessionPayload = null; - } - try { - event.source.postMessage({ source: DROPBOX_AUTH_MESSAGE_SOURCE, type: "session", session: sessionPayload }, event.origin); - } catch (e) { } - return; - } - if (data.type === "tokens" && data.tokens) { - persistDropboxOAuthTokens(data.tokens); - clearDropboxAuthSession(); - cleanupDropboxAuthFlow(data.tokens, false); - } else if (data.type === "error") { - if (data.clearTokens) { - clearDropboxOAuthTokens(); - } - clearDropboxAuthSession(); - cleanupDropboxAuthFlow(new Error(data.message || "Dropbox authorization failed"), true); - } -} - + + session.seeding = true; + + if (session.videoMutedFlag) { + session.videoMuted = true; + toggleVideoMute(true); + } + + session.seedStream(); + }; +}; // publishFrameSource + +function tryAgain(event) { + // audio or video agnostic track reconnect ------------not actually in use,. maybe out of date + log("TRY AGAIN TRIGGERED"); + warnlog(event); +} + +function enterPressedClick(event, ele) { + if (event.keyCode === 13) { + event.preventDefault(); + ele.click(); + } +} + +function enterPressed(event, callback) { + // Number 13 is the "Enter" key on the keyboard + if (event.keyCode === 13) { + event.preventDefault(); + callback(); + } +} + +function dragElement(elmnt) { + if (session.disableMouseEvents) { + return; + } + log("dragElement started"); + + function onvideoclick() { + log("onvideoclick"); + log(pos3 + " " + pos4); + //log(pos3o + " " + pos4o); + tapToFocus(parseInt((pos3 * 100) / elmnt.clientWidth), parseInt((pos4 / elmnt.clientHeight) * 100)); + return false; + } + + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + // calculate the new cursor position: + log("dragging"); + log(e); + if (Date.now() - millis < 100) { + return; + } + + dragged = true; + millis = Date.now(); + + if (e.type == "touchstart" || e.type == "touchmove" || e.type == "touchend" || e.type == "touchcancel") { + var touch = e.touches[0] || e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; + pos1 = touch.clientX; + pos2 = touch.clientY; + } else if (e.type == "mousedown" || e.type == "mouseup" || e.type == "mousemove" || e.type == "mouseover" || e.type == "mouseout" || e.type == "mouseenter" || e.type == "mouseleave") { + pos1 = e.clientX; + pos2 = e.clientY; + } + + if (!zoomable) { + return; + } + + var zoom = parseFloat(((pos4 - pos2) * 2) / elmnt.offsetHeight); + + if (zoom > 1) { + zoom = 1.0; + } else if (zoom < -1) { + zoom = -1.0; + } + input.value = zoom * (input.max - input.min) + input.min; + updateCameraConstraints("zoom", input.value, false, false); + } + function closeDragElement(e) { + log("closeDragElement"); + log(e); + + // focusable + if (!dragged) { + log("dragged: " + dragged); + onvideoclick(); + } + dragged = false; + + elmnt.removeEventListener("touchend", closeDragElement); + elmnt.removeEventListener("mouseup", closeDragElement); + + /* stop moving when mouse button is released:*/ + //document.ontouchend = null; + //document.onmouseup = null; + document.onmousemove = null; + document.ontouchmove = null; + } + function dragMouseDown(e) { + log("dragMouseDown"); + log(e); + + dragged = false; + millis = Date.now(); + + e = e || window.event; + e.preventDefault(); + + pos0 = input.value; + if (e.type == "touchstart" || e.type == "touchmove" || e.type == "touchend" || e.type == "touchcancel") { + var touch = e.touches[0] || e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; + pos3 = touch.clientX; + pos4 = touch.clientY; + //pos3o = touch.offsetX; + //pos4o = touch.offsetX; + } else if (e.type == "mousedown" || e.type == "mouseup" || e.type == "mousemove" || e.type == "mouseover" || e.type == "mouseout" || e.type == "mouseenter" || e.type == "mouseleave") { + pos3 = e.clientX; + pos4 = e.clientY; + //pos3o = e.offsetX; + //pos4o = e.offsetX; + } + elmnt.addEventListener("touchend", closeDragElement); + elmnt.addEventListener("mouseup", closeDragElement); + document.ontouchmove = elementDrag; + document.onmousemove = elementDrag; + } + + try { + var stream = elmnt.srcObject; + try { + var track0 = stream.getVideoTracks(); + } catch (e) { + return; + } + + if (!track0.length) { + return; + } + var focusable = false; + var zoomable = false; + var dragged = false; + var input = getById("zoomSlider"); + track0 = track0[0]; + if (track0.getCapabilities) { + var capabilities = track0.getCapabilities(); + var settings = track0.getSettings(); + + if ("focusDistance" in capabilities) { + log("focusable"); + focusable = true; + } + + if ("zoom" in capabilities) { + if (capabilities.zoom.min !== capabilities.zoom.max) { + log("zoomable;"); + zoomable = true; + input.min = capabilities.zoom.min; + input.max = capabilities.zoom.max; + input.step = capabilities.zoom.step; + input.value = settings.zoom; + } + } + } + + var millis = Date.now(); + var pos0 = 1; + var pos3 = 0; + var pos4 = 0; + var pos1 = 0; + var pos2 = 0; + //var pos3o = 0; + //var pos4o = 0; + } catch (e) { + errorlog(e); + return; + } + + if (!focusable && !zoomable) { + return; + } // can't be zoomed or focused. + + log("drag on"); + elmnt.onmousedown = dragMouseDown; + elmnt.ontouchstart = dragMouseDown; +} + +function previewIframe(iframeSrc) { + // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: loadIframe();"> , but don't call it before the page loads. + if (!session.iFramesAllowed) { + warnUser("Can't create iFRAME - security is tainted due to possible CSS injection"); + errorlog("Can't create iFRAME - security is tainted due to possible CSS injection"); + return; + } + var iframe = document.createElement("iframe"); + iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.border = "10px dashed rgb(64 65 62)"; + iframe.classList.add("insecure"); + iframe.setAttribute("allowtransparency", "true"); + iframe.setAttribute("crossorigin", "anonymous"); + iframe.setAttribute("credentialless", "true"); + + iframeSrc = parseURL4Iframe(iframeSrc); + + /* if (typeof iframeSrc == "object"){ // special handler. + iframeSrc = iframeSrc.parsedSrc; + } */ + + iframe.src = iframeSrc; + getById("previewIframe").innerHTML = ""; + getById("previewIframe").style.width = "640px"; + getById("previewIframe").style.height = "360px"; + getById("previewIframe").style.margin = "auto"; + getById("previewIframe").appendChild(iframe); +} + +function loadIframe(iframesrc, target) { + // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: loadIframe();"> , but don't call it before the page loads. + /* if (document.getElementById("mainmenu")) { + var m = getById("mainmenu"); + m.remove(); + } */ + + if (!session.iFramesAllowed) { + return false; + } + + if (!target) { + return false; + } + + if (typeof target == "string") { + let UUID = target; + var iframe = document.createElement("iframe"); + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.id = "iframe_" + UUID; + iframe.dataset.UUID = UUID; + iframe.loadedYoutubeListen = false; + + if (session.director) { + // + } else if (session.scene !== false) { + if (session.view) { + // specific video to be played + iframe.style.display = "block"; + } else if (session.scene === "0") { + iframe.style.display = "block"; + } else { + // group scene I guess; needs to be added manually + iframe.style.display = "none"; + } + } else if (session.roomid !== false) { + // + } else { + iframe.style.display = "block"; + } + } else { + var iframe = target; + } + + iframe.classList.add("insecure"); + iframe.setAttribute("allowtransparency", "true"); + iframe.setAttribute("crossorigin", "anonymous"); + iframe.setAttribute("credentialless", "true"); + iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location + + if (iframesrc == "") { + iframesrc = "./"; + iframe.classList.remove("insecure"); + } + + // trusted domains + var ipsafe = false; + + if (iframesrc.startsWith("https://www.youtube.com/") || iframesrc.startsWith("https://youtube.com/")) { + iframe.classList.remove("insecure"); + setTimeout( + function (iframe_id) { + YoutubeListen(iframe_id); + }, + 1000, + iframe.id + ); // create stats feedback for the director; syncing. + if (session.noaudio) { + if (iframesrc.includes("?")) { + iframesrc += "&mute=1"; + } else { + iframesrc += "?mute=1"; + } + } + ipsafe = true; + } else if (iframesrc.includes("vdo.ninja/")) { + iframe.classList.remove("insecure"); + ipsafe = false; + if (isIFrame) { + console.warn("You're not allow to put this domain inside an iframe of an iframe."); + return false; + } + } else if (iframesrc.includes("obs.ninja/")) { + iframe.classList.remove("insecure"); + ipsafe = false; + if (isIFrame) { + console.warn("You're not allow to put this domain inside an iframe of an iframe."); + return false; + } + } else if (iframesrc.includes("versus.cam/")) { + iframe.classList.remove("insecure"); + ipsafe = false; + if (isIFrame) { + console.warn("You're not allow to put this domain inside an iframe of an iframe."); + return false; + } + } else if (iframesrc.includes("invite.cam/")) { + ipsafe = false; + if (isIFrame) { + console.warn("You're not allow to put this domain inside an iframe of an iframe."); + return false; + } + } else if (iframesrc.startsWith("https://player.twitch.tv/")) { + iframe.classList.remove("insecure"); + ipsafe = true; + } else if (iframesrc.startsWith("https://x.com/")) { + iframe.classList.remove("insecure"); + ipsafe = false; + } else if (iframesrc.startsWith("https://twitch.tv/")) { + iframe.classList.remove("insecure"); + ipsafe = true; + } else if (iframesrc.startsWith("https://caption.ninja/")) { + iframe.classList.remove("insecure"); + ipsafe = true; + } else if (iframesrc.startsWith("https://www.twitch.tv/")) { + iframe.classList.remove("insecure"); + ipsafe = true; + } else if (iframesrc.startsWith("https://vimeo.com/")) { + iframe.classList.remove("insecure"); + ipsafe = true; + } else if (iframesrc.startsWith("https://player.vimeo.com/")) { + iframe.classList.remove("insecure"); + ipsafe = true; + } else if (iframesrc.startsWith("https://meshcast.io/")) { + iframe.classList.remove("insecure"); + try { + if (document.domain.endsWith(".vdo.ninja")) { + document.domain = "vdo.ninja"; + } + } catch (e) { + errorlog(e); + } + ipsafe = true; + } else if (iframesrc.startsWith("https://app.stageten.tv/")) { + iframe.classList.remove("insecure"); + ipsafe = true; + } else if (iframesrc.startsWith("https://socialstream.ninja/")) { + iframe.classList.remove("insecure"); + ipsafe = false; + } else if (session.cleanOutput && window.obsstudio) { + iframe.classList.remove("insecure"); + } + + if (!ipsafe) { + iframe.title = "⚠️ This section is an iframe that may be of untrusted origin. Use caution."; + } + + if (!ipsafe && (urlParams.has("privacy") || urlParams.has("private"))) { + if (session.cleanOutput || window.obsstudio) { + iframesrc = "./confirm.html?clean&url=" + encodeURI(iframesrc); + } else { + iframesrc = "./confirm.html?url=" + encodeURI(iframesrc); + } + } else if (!ipsafe && (typeof target == "object")) { // likely widget + if (session.widgetwidth <= 28) { // 28 for marcus + iframe.classList.remove("insecure"); + } else if (isIFrame && (session.widgetwidth <= 50)) { + iframe.classList.remove("insecure"); + } else { + if (session.cleanOutput || window.obsstudio) { + iframesrc = "./confirm.html?clean&url=" + encodeURI(iframesrc); + } else { + iframesrc = "./confirm.html?url=" + encodeURI(iframesrc); + } + } + } + + if (isIFrame && ["invite.cam", "invitecamera.com", "vdo.ninja", "versus.cam", "dev.versus.cam", "backup.vdo.ninja", "proxy.vdo.ninia", "proxy.obs.ninja", "insecure.vdo.ninja", "insecure.obs.ninja", "rtc.ninja"].includes(getParentHostname())) { + iframe.classList.add("insecure"); + } + + iframe.src = iframesrc; + + pokeIframeAPI("iframe-loaded", iframesrc); + return iframe; +} + +function dropDownButtonAction(ele) { + var ele = getById("dropButton"); + if (ele) { + ele.parentNode.removeChild(ele); + //getById('container-5').classList.remove('hidden'); + //getById('container-8').classList.remove('hidden'); + //getById('container-6').classList.remove('hidden'); + document.querySelectorAll("div.column.card").forEach(child => { + child.classList.remove("hidden"); + }); + } +} + +function updateConstraintSliders() { + log("updateConstraintSliders"); + if (session.roomid !== false && session.roomid !== "" && session.director !== true && session.forceMediaSettings == false) { + if (session.controlRoomBitrate !== false) { + listCameraSettings(); + } + if (session.effect !== false) { + //if ((iOS) || (iPad)){ + //} else { + getById("effectsDiv3").style.display = "block"; + getById("effectSelector3").value = session.effect || "0"; + //} + } + } else { + listAudioSettings(); + listCameraSettings(); + + //if ((iOS) || (iPad)){ + // } else { + if (session.effect !== false) { + getById("effectsDiv3").style.display = "block"; + try { + getById("effectSelector3").value = session.effect || "0"; + } catch (E) { } + } + //} + } + //checkIfPIP(); // this doesn't actually work on iOS still, so whatever. +} + +function checkIfPIP() { + try { + if (session.videoElement && ((session.videoElement.webkitSupportsPresentationMode && typeof session.videoElement.webkitSetPresentationMode === "function") || document.pictureInPictureEnabled || !videoElement.disablePictureInPicture)) { + // Toggle PiP when the user clicks the button. + + getById("pIpStartButton").addEventListener("click", function (event) { + // if ( (document.pictureInPictureEnabled || !videoElement.disablePictureInPicture)){ + //session.videoElement.requestPictureInPicture(); + // } else { + session.videoElement.webkitSetPresentationMode(session.videoElement.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture"); + // } + }); + getById("pIpStartButton").style.display = "inline-block"; + } + } catch (e) { + errorlog(e); + } +} + +function togglePictureInPicture(videoElement) { + if (document.pictureInPictureElement) { + if (document.pictureInPictureElement.id == videoElement.id) { + document.exitPictureInPicture(); + pokeIframeAPI("picture-in-picture", false); + return false; + } else { + document.exitPictureInPicture(); + pokeIframeAPI("picture-in-picture", false); + videoElement.requestPictureInPicture(); + pokeIframeAPI("picture-in-picture", true); + } + } else if (document.pictureInPictureEnabled) { + videoElement.requestPictureInPicture(); + pokeIframeAPI("picture-in-picture", true); + } + return true; +} + +function mixMinusAudio(uid = false) { + if (session.stereo === false) { + var merger = session.audioCtx.createChannelMerger(1); + } else { + var merger = session.audioCtx.createChannelMerger(2); + } + + if (session.videoElement && session.videoElement.srcObject) { + var tracks = session.videoElement.srcObject.getAudioTracks(); + for (var i = 0; i < tracks.length; i++) { + try { + var tempStream = createMediaStream(); + tempStream.addTrack(tracks[i]); + trackStream = session.audioCtx.createMediaStreamSource(tempStream); + + if (session.stereo !== false) { + var splitter = session.audioCtx.createChannelSplitter(2); + trackStream.connect(splitter); + splitter.connect(merger, 0, 0); + try { + splitter.connect(merger, 1, 1); + } catch (e) { + errorlog(e); + try { + splitter.connect(merger, 0, 1); // hack. + } catch (e) { + errorlog(e); + } + } + } else { + trackStream.connect(merger, 0, 0); + } + } catch (e) { + errorlog(e); + } + } + } + + for (var UUID in session.rpcs) { + if (uid && UUID === uid) { + continue; + } + if (!session.rpcs[UUID].videoElement) { + continue; + } else if (!session.rpcs[UUID].videoElement.srcObject) { + continue; + } + + var tracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); + for (var i = 0; i < tracks.length; i++) { + try { + var tempStream = createMediaStream(); + tempStream.addTrack(tracks[i]); + trackStream = session.audioCtx.createMediaStreamSource(tempStream); + + if (session.stereo !== false) { + var splitter = session.audioCtx.createChannelSplitter(2); + trackStream.connect(splitter); + splitter.connect(merger, 0, 0); + try { + splitter.connect(merger, 1, 1); + } catch (e) { + errorlog(e); + try { + splitter.connect(merger, 0, 1); // hack. + } catch (e) { + errorlog(e); + } + } + } else { + trackStream.connect(merger, 0, 0); + } + } catch (e) { + errorlog(e); + } + } + } + + var destination = session.audioCtx.createMediaStreamDestination(); + merger.connect(destination); + return destination.stream; +} + +// Cleanup audio nodes for a guest's mix-minus to prevent memory leaks +function cleanupMixMinusAudioNodes(uuid) { + if (!session.mixMinusState || !session.mixMinusState[uuid]) { + return; + } + + var nodes = session.mixMinusState[uuid].audioNodes; + if (!nodes) { + return; + } + + try { + // Disconnect all source nodes + if (nodes.sources) { + for (var i = 0; i < nodes.sources.length; i++) { + try { + nodes.sources[i].disconnect(); + } catch (e) { } + } + nodes.sources = []; + } + + // Disconnect all splitter nodes + if (nodes.splitters) { + for (var i = 0; i < nodes.splitters.length; i++) { + try { + nodes.splitters[i].disconnect(); + } catch (e) { } + } + nodes.splitters = []; + } + + // Disconnect merger + if (nodes.merger) { + try { + nodes.merger.disconnect(); + } catch (e) { } + nodes.merger = null; + } + + // Destination doesn't need explicit disconnect + nodes.destination = null; + + } catch (e) { + warnlog("Error cleaning up mix-minus audio nodes: " + e); + } +} + +// Director mix-minus: Creates a custom audio mix for a specific guest +// Includes all other guests' audio + director's audio, excluding the target guest's own audio +function createDirectorMixMinusForGuest(targetUUID) { + // Check if this guest has mix enabled (either via &mixminus or via UI) + // Allow if directorMixMinus is set OR if guest-specific state is enabled + if (!session.directorMixMinus && (!session.mixMinusState || !session.mixMinusState[targetUUID] || !session.mixMinusState[targetUUID].enabled)) { + return null; + } + + if (!session.mixMinusState) { + session.mixMinusState = {}; + } + + if (!session.audioCtx) { + warnlog("Audio context not initialized for mix-minus"); + return null; + } + + // Initialize state for this guest if not exists + if (!session.mixMinusState[targetUUID]) { + initMixMinusStateForGuest(targetUUID); + } + + var guestState = session.mixMinusState[targetUUID]; + + // Cleanup previous audio nodes before creating new ones (or if disabled) + cleanupMixMinusAudioNodes(targetUUID); + + if (!guestState || !guestState.enabled) { + return null; + } + + // Ensure audioNodes object exists + if (!guestState.audioNodes) { + guestState.audioNodes = { + merger: null, + destination: null, + sources: [], + splitters: [] + }; + } + + // Create channel merger based on stereo setting + var merger; + try { + if (session.stereo === false) { + merger = session.audioCtx.createChannelMerger(1); + } else { + merger = session.audioCtx.createChannelMerger(2); + } + guestState.audioNodes.merger = merger; + } catch (e) { + errorlog("Failed to create audio merger for mix-minus: " + e); + return null; + } + + // Helper function to connect audio track to merger + function connectTrackToMerger(track) { + try { + var tempStream = createMediaStream(); + tempStream.addTrack(track); + var trackStream = session.audioCtx.createMediaStreamSource(tempStream); + guestState.audioNodes.sources.push(trackStream); + + if (session.stereo !== false) { + var splitter = session.audioCtx.createChannelSplitter(2); + guestState.audioNodes.splitters.push(splitter); + trackStream.connect(splitter); + splitter.connect(merger, 0, 0); + try { + splitter.connect(merger, 1, 1); + } catch (e) { + errorlog(e); + try { + splitter.connect(merger, 0, 1); + } catch (e) { + errorlog(e); + } + } + } else { + trackStream.connect(merger, 0, 0); + } + } catch (e) { + errorlog(e); + } + } + + // Add director's processed audio mix (if enabled) + if (guestState.useDirectorMix !== false && session.videoElement && session.videoElement.srcObject) { + var mixTracks = session.videoElement.srcObject.getAudioTracks(); + for (var i = 0; i < mixTracks.length; i++) { + connectTrackToMerger(mixTracks[i]); + } + } + + // Add raw input devices (if any are enabled) + if (guestState.rawDevices && session.streamSrc) { + var inputTracks = session.streamSrc.getAudioTracks(); + for (var i = 0; i < inputTracks.length; i++) { + var track = inputTracks[i]; + var deviceId = track.getSettings().deviceId || track.id; + if (guestState.rawDevices[deviceId] === true) { + connectTrackToMerger(track); + } + } + } + + // Add all other guests' audio (from session.rpcs) + for (var UUID in session.rpcs) { + // Skip the target guest (they don't need to hear themselves) + if (UUID === targetUUID) { + continue; + } + + // Check if this source is excluded for this guest + if (guestState.excludeSources && guestState.excludeSources.includes(UUID)) { + continue; + } + + // If using include mode, check if source is explicitly included + if (guestState.includeSources && guestState.includeSources.length > 0) { + if (!guestState.includeSources.includes(UUID)) { + continue; + } + } + + // Skip if rpcs entry doesn't exist or has no audio + if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].videoElement.srcObject) { + continue; + } + + var guestTracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); + for (var i = 0; i < guestTracks.length; i++) { + connectTrackToMerger(guestTracks[i]); + } + } + + // Create destination stream + var destination = session.audioCtx.createMediaStreamDestination(); + merger.connect(destination); + guestState.audioNodes.destination = destination; + + return destination.stream; +} + +// Initialize mix-minus state for a guest +function initMixMinusStateForGuest(uuid) { + if (!session.mixMinusState) { + session.mixMinusState = {}; + } + + if (!session.mixMinusDefaults) { + session.mixMinusDefaults = { + allGuestsEnabled: true, + includeDirectorAudio: true, + includeAllGuests: session.directorMixMinus ? true : false // Only include guests by default if &mixminus is set + }; + } + + // Don't overwrite existing state (audio nodes may already be stored) + if (session.mixMinusState[uuid]) { + return; + } + + session.mixMinusState[uuid] = { + enabled: session.mixMinusDefaults.allGuestsEnabled, + excludeSources: [], + includeSources: [], + // Director audio options + useDirectorMix: true, // Use processed WebAudio output (with effects) + rawDevices: {}, // { deviceId: true/false } for raw input devices + directorAudioDevices: {}, // Legacy - kept for backwards compatibility + // Audio node references for cleanup + audioNodes: { + merger: null, + destination: null, + sources: [], // MediaStreamSource nodes + splitters: [] // ChannelSplitter nodes + } + }; + + // Initialize raw input devices (from session.streamSrc) - disabled by default + if (session.streamSrc) { + var inputTracks = session.streamSrc.getAudioTracks(); + for (var i = 0; i < inputTracks.length; i++) { + var deviceId = inputTracks[i].getSettings().deviceId || inputTracks[i].id; + session.mixMinusState[uuid].rawDevices[deviceId] = false; // Disabled by default + } + } + + // Legacy: also track processed WebAudio output devices + if (session.videoElement && session.videoElement.srcObject) { + var directorTracks = session.videoElement.srcObject.getAudioTracks(); + for (var i = 0; i < directorTracks.length; i++) { + var deviceId = directorTracks[i].getSettings().deviceId || directorTracks[i].id; + session.mixMinusState[uuid].directorAudioDevices[deviceId] = true; + } + } + + // Without &mixminus, exclude other guests by default (director audio only) + // With &mixminus, include all guests by default (mix-minus behavior) + if (!session.mixMinusDefaults.includeAllGuests) { + // Add all current guests (except target) to excludeSources + for (var guestUUID in session.rpcs) { + if (guestUUID !== uuid) { + session.mixMinusState[uuid].excludeSources.push(guestUUID); + } + } + } +} + +// Toggle mix-minus enabled/disabled for a specific guest +function toggleMixMinusForGuest(uuid) { + if (!session.mixMinusState) { + session.mixMinusState = {}; + } + if (!session.mixMinusState[uuid]) { + initMixMinusStateForGuest(uuid); + } + session.mixMinusState[uuid].enabled = !session.mixMinusState[uuid].enabled; + updateMixMinusForGuest(uuid); + return session.mixMinusState[uuid].enabled; +} + +// Toggle a specific audio source in the mix for a guest +function toggleSourceInMixForGuest(sourceUUID, targetUUID) { + if (!session.mixMinusState) { + session.mixMinusState = {}; + } + if (!session.mixMinusState[targetUUID]) { + initMixMinusStateForGuest(targetUUID); + } + + var state = session.mixMinusState[targetUUID]; + var idx = state.excludeSources.indexOf(sourceUUID); + if (idx > -1) { + state.excludeSources.splice(idx, 1); // Remove from exclude list (enable) + } else { + state.excludeSources.push(sourceUUID); // Add to exclude list (disable) + } + + updateMixMinusForGuest(targetUUID); + return idx > -1; // Returns true if source is now enabled +} + +// Toggle a director audio device in the mix for a guest +function toggleDirectorDeviceInMix(deviceId, targetUUID) { + if (!session.mixMinusState) { + session.mixMinusState = {}; + } + if (!session.mixMinusState[targetUUID]) { + initMixMinusStateForGuest(targetUUID); + } + + var state = session.mixMinusState[targetUUID]; + state.directorAudioDevices[deviceId] = !state.directorAudioDevices[deviceId]; + + updateMixMinusForGuest(targetUUID); + return state.directorAudioDevices[deviceId]; +} + +// Set mix-minus state for all guests +function setMixMinusForAll(enabled) { + if (!session.mixMinusDefaults) { + session.mixMinusDefaults = { + allGuestsEnabled: enabled, + includeDirectorAudio: true, + includeAllGuests: true + }; + } else { + session.mixMinusDefaults.allGuestsEnabled = enabled; + } + + if (!session.mixMinusState) { + session.mixMinusState = {}; + return; + } + + for (var uuid in session.mixMinusState) { + session.mixMinusState[uuid].enabled = enabled; + updateMixMinusForGuest(uuid); + } +} + +// Update/rebuild the mix-minus stream for a guest +// This should replace the audio track being sent to the guest +function updateMixMinusForGuest(uuid) { + if (!session.pcs[uuid]) { + return; + } + + var mixStream = createDirectorMixMinusForGuest(uuid); + if (!mixStream) { + return; + } + + var mixTracks = mixStream.getAudioTracks(); + if (!mixTracks.length) { + return; + } + + // Replace ALL audio tracks being sent to this guest + try { + var senders = session.pcs[uuid].getSenders(); + var replacedCount = 0; + for (var i = 0; i < senders.length; i++) { + if (senders[i].track && senders[i].track.kind === "audio") { + senders[i].replaceTrack(mixTracks[0]); + replacedCount++; + } + } + if (replacedCount > 0) { + log("Updated mix-minus audio for guest: " + uuid + " (replaced " + replacedCount + " audio track(s))"); + } + } catch (e) { + errorlog("Error updating mix-minus for guest " + uuid + ": " + e); + } +} + +// Called when a new guest joins - initialize their mix-minus state and send mix +function onGuestJoinedMixMinus(uuid) { + if (!session.directorMixMinus) { + return; + } + + initMixMinusStateForGuest(uuid); + + // Also update existing guests' mixes to include the new guest + for (var existingUUID in session.mixMinusState) { + if (existingUUID !== uuid && session.mixMinusState[existingUUID].enabled) { + updateMixMinusForGuest(existingUUID); + } + } +} + +// Called when a guest leaves - cleanup and update other guests' mixes +function onGuestLeftMixMinus(uuid) { + // Allow cleanup if directorMixMinus is set OR if there's any mix state + if (!session.directorMixMinus && !session.mixMinusState) { + return; + } + + // Cleanup audio nodes before removing state + cleanupMixMinusAudioNodes(uuid); + + // Remove from state + delete session.mixMinusState[uuid]; + + // Remove from exclude/include lists of other guests + for (var otherUUID in session.mixMinusState) { + var state = session.mixMinusState[otherUUID]; + var idx = state.excludeSources.indexOf(uuid); + if (idx > -1) { + state.excludeSources.splice(idx, 1); + } + idx = state.includeSources.indexOf(uuid); + if (idx > -1) { + state.includeSources.splice(idx, 1); + } + // Update their mix since a source left + updateMixMinusForGuest(otherUUID); + } +} + +// UI handler for mix-minus toggle button in director panel +function directToggleMixMinus(ele, event) { + if (!session.directorMixMinus) { + warnUser("Mix-minus not enabled. Add &mixminus to your director URL."); + return; + } + + var UUID = ele.dataset.UUID; + if (!UUID) { + // Try to find UUID from parent element + try { + UUID = ele.closest("[data-UUID]").dataset.UUID; + } catch (e) { + errorlog("Could not find guest UUID for mix-minus toggle"); + return; + } + } + + var enabled = toggleMixMinusForGuest(UUID); + + // Update button appearance + if (enabled) { + ele.classList.add("pressed"); + ele.title = "Mix-minus enabled - this guest hears all other audio"; + } else { + ele.classList.remove("pressed"); + ele.title = "Mix-minus disabled for this guest"; + } + + log("Mix-minus for " + UUID + " is now: " + (enabled ? "enabled" : "disabled")); +} + +// Global variable to track open dropdown +var activeMixDropdown = null; + +// Toggle mix dropdown visibility and populate sources +function toggleMixDropdown(UUID, buttonEle, event) { + if (event) { + event.stopPropagation(); + } + + // Find or get UUID from button + if (!UUID && buttonEle) { + UUID = buttonEle.dataset.UUID; + if (!UUID) { + try { + UUID = buttonEle.closest("[data-UUID]").dataset.UUID; + } catch (e) { + errorlog("Could not find guest UUID for mix dropdown"); + return; + } + } + } + + // Find the dropdown container (sibling to PGM/Mic row) + var container = buttonEle.closest(".row").nextElementSibling; + if (!container || !container.classList.contains("mix-dropdown-container")) { + errorlog("Could not find mix dropdown container"); + return; + } + + var dropdown = container.querySelector(".mix-dropdown"); + if (!dropdown) { + errorlog("Could not find mix dropdown element"); + return; + } + + // Close any other open dropdown + if (activeMixDropdown && activeMixDropdown !== dropdown) { + activeMixDropdown.style.display = "none"; + activeMixDropdown.closest(".mix-dropdown-container").style.display = "none"; + } + + // Toggle visibility + if (dropdown.style.display === "none" || dropdown.style.display === "") { + // Initialize state if needed + if (!session.mixMinusState) { + session.mixMinusState = {}; + } + if (!session.mixMinusState[UUID]) { + initMixMinusStateForGuest(UUID); + } + + // Enable mix-minus for this guest if not already + if (!session.mixMinusState[UUID].enabled) { + session.mixMinusState[UUID].enabled = true; + updateMixMinusForGuest(UUID); + } + + // Populate and show dropdown + populateMixDropdown(UUID, dropdown); + container.style.display = "block"; + dropdown.style.display = "block"; + activeMixDropdown = dropdown; + buttonEle.classList.add("pressed"); + + // Add click outside listener to close dropdown + setTimeout(function() { + document.addEventListener("click", closeMixDropdownOnClickOutside); + }, 10); + } else { + // Hide dropdown + dropdown.style.display = "none"; + container.style.display = "none"; + activeMixDropdown = null; + buttonEle.classList.remove("pressed"); + document.removeEventListener("click", closeMixDropdownOnClickOutside); + } +} + +// Close dropdown when clicking outside +function closeMixDropdownOnClickOutside(event) { + if (activeMixDropdown && !activeMixDropdown.contains(event.target)) { + var container = activeMixDropdown.closest(".mix-dropdown-container"); + activeMixDropdown.style.display = "none"; + if (container) { + container.style.display = "none"; + } + // Find and un-press the Mix button + var row = container ? container.previousElementSibling : null; + if (row) { + var mixBtn = row.querySelector('[data-action-type="custom-mix"]'); + if (mixBtn) { + mixBtn.classList.remove("pressed"); + } + } + activeMixDropdown = null; + document.removeEventListener("click", closeMixDropdownOnClickOutside); + } +} + +// Populate dropdown with available audio sources +function populateMixDropdown(targetUUID, dropdown) { + if (!dropdown) { + return; + } + + var html = '
    Audio Sources
    '; + var state = session.mixMinusState[targetUUID]; + + // Section 1: Director Mix (processed WebAudio output with effects) + html += '
    '; + html += '
    Director Mix
    '; + + if (session.videoElement && session.videoElement.srcObject) { + var mixTracks = session.videoElement.srcObject.getAudioTracks(); + if (mixTracks.length > 0) { + var checked = state.useDirectorMix !== false; + html += '
    '; + html += ''; + html += ''; + html += '
    '; + } else { + html += '
    No director mix available
    '; + } + } else { + html += '
    No director mix available
    '; + } + html += '
    '; + + // Section 2: Director Input Devices (raw, unprocessed) + html += '
    '; + html += '
    Director Input Devices
    '; + + if (session.streamSrc) { + var inputTracks = session.streamSrc.getAudioTracks(); + if (inputTracks.length === 0) { + html += '
    No input devices
    '; + } else { + for (var i = 0; i < inputTracks.length; i++) { + var track = inputTracks[i]; + var deviceId = track.getSettings().deviceId || track.id; + var label = track.label || ("Mic " + (i + 1)); + var checked = state.rawDevices && state.rawDevices[deviceId] === true; + + html += '
    '; + html += ''; + html += ''; + html += '
    '; + } + } + } else { + html += '
    No input devices
    '; + } + html += '
    '; + + // Other guests section + html += '
    '; + html += '
    Guests
    '; + + var guestCount = 0; + for (var UUID in session.rpcs) { + // Skip the target guest (they don't hear themselves) + if (UUID === targetUUID) { + continue; + } + + if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].videoElement.srcObject) { + continue; + } + + var guestTracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); + if (guestTracks.length === 0) { + continue; + } + + guestCount++; + var guestLabel = session.rpcs[UUID].label || "Guest"; + var streamID = session.rpcs[UUID].streamID || UUID.substring(0, 6); + var displayLabel = guestLabel + " - " + streamID; + + // Check if source is excluded + var checked = state.excludeSources.indexOf(UUID) === -1; + + html += '
    '; + html += ''; + html += ''; + html += '
    '; + } + + if (guestCount === 0) { + html += '
    No other guests
    '; + } + + html += '
    '; + + dropdown.innerHTML = html; +} + +// Helper to sanitize labels for HTML display +function sanitizeLabel(str) { + if (!str) return ""; + return str.replace(//g, ">").replace(/"/g, """); +} + +// Toggle a source in the mix and update the mix +function toggleMixSource(targetUUID, sourceId, sourceType, checkbox) { + if (!session.mixMinusState || !session.mixMinusState[targetUUID]) { + return; + } + + var state = session.mixMinusState[targetUUID]; + + if (sourceType === 'mix') { + // Toggle Director Mix (processed WebAudio output) + state.useDirectorMix = !state.useDirectorMix; + updateMixMinusForGuest(targetUUID); + } else if (sourceType === 'raw') { + // Toggle raw input device + if (!state.rawDevices) { + state.rawDevices = {}; + } + state.rawDevices[sourceId] = !state.rawDevices[sourceId]; + updateMixMinusForGuest(targetUUID); + } else if (sourceType === true) { + // Legacy: Toggle director audio device (backwards compatibility) + toggleDirectorDeviceInMix(sourceId, targetUUID); + } else { + // Toggle guest audio source (sourceType === false or undefined) + toggleSourceInMixForGuest(sourceId, targetUUID); + } +} + +function listAudioSettingsPrep() { + try { + var tracks = session.streamSrc.getAudioTracks(); + if (!tracks.length) { + warnlog("session.streamSrc contains no audio tracks"); + //return; + } + } catch (e) { + warnlog(e); + return; + } + + var data = []; + + for (var i = 0; i < tracks.length; i += 1) { + track0 = tracks[i]; + var trackSet = {}; + + if (track0.getCapabilities) { + trackSet.audioConstraints = track0.getCapabilities(); + } else if (Firefox) { + // let's pretend like Firefox doesn't actually suck + trackSet.audioConstraints = { + autoGainControl: [true, false], + // "channelCount": { + // "max": 2, + // "min": 1 + // }, + // "deviceId": "default", + echoCancellation: [true, false], + // "groupId": "a3cbdec54a9b6ed473fd950415626f7e76f9d1b90f8c768faab572175a355a17", + // "latency": { + // "max": 0.01, + // "min": 0.01 + // }, + noiseSuppression: [true, false] + // "sampleRate": { + // "max": 48000, + // "min": 48000 + // }, + // "sampleSize": { + // "max": 16, + // "min": 16 + /// } + }; + } + + if (track0.getSettings) { + trackSet.currentAudioConstraints = track0.getSettings(); + if (!session.stereo) { + try { + delete trackSet.currentAudioConstraints.channelCount; + delete trackSet.audioConstraints.channelCount; + } catch (e) { } + } else if (session.audioInputChannels && session.audioInputChannels == 1) { + // this is pretty hacky, but it gets around not being able to actually set 1-channel. Not sure why. + trackSet.currentAudioConstraints.channelCount = 1; + } + } + + trackSet.trackLabel = "unknown or none"; + if (track0.label) { + trackSet.trackLabel = track0.label; + } + if (track0.id) { + trackSet.deviceId = track0.id; + } + if (i == 0) { + trackSet.equalizer = session.equalizer; // only supporting the first track at the moment. + + for (var waid in session.webAudios) { + // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){.. + try { + trackSet.lowEQ = session.webAudios[waid].lowEQ.gain.value; + trackSet.midEQ = session.webAudios[waid].midEQ.gain.value; + trackSet.highEQ = session.webAudios[waid].highEQ.gain.value; + } catch (e) { } + break; + } + } else { + trackSet.equalizer = false; + } + + if (i == 0) { + trackSet.lowcut = session.lowcut; // only supporting the first track at the moment. + if (session.lowcut) { + for (var waid in session.webAudios) { + // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){.. + try { + trackSet.lowcut = session.webAudios[waid].lowcut1.frequency.value; + } catch (e) { } + break; + } + } + } else { + trackSet.lowcut = false; + } + + trackSet.subGain = false; + for (var waid in session.webAudios) { + // TODO: EXCLUDE CURRENT TRACK IF ALREADY EXISTS ... if (track.id === wa.id){.. + try { + if (session.webAudios[waid].subGainNodes && track0.id in session.webAudios[waid].subGainNodes) { + trackSet.subGain = session.webAudios[waid].subGainNodes[track0.id].gain.value; + } + break; + } catch (e) { } + } + + if (i == 0 && !session.disableWebAudio) { + // if web audio is disabled, don't show them + trackSet.gating = session.noisegate; + trackSet.compressor = session.compressor; + trackSet.micDelay = session.micDelay; + trackSet.micPanning = session.micPanning !== false ? session.micPanning : false; + } + + data.push(trackSet); + } + + pokeIframeAPI("listing-audio-settings", data); + return data; +} + +function listVideoSettingsPrep() { + try { + var track0 = session.streamSrc.getVideoTracks(); + if (track0.length) { + track0 = track0[0]; + if (track0.getCapabilities) { + session.cameraConstraints = track0.getCapabilities(); + } + log(session.cameraConstraints); + } + } catch (e) { + warnlog(e); + return; + } + + try { + if (track0.getSettings) { + session.currentCameraConstraints = track0.getSettings(); + + if (screen && screen.orientation && screen.orientation.type) { + if (screen.orientation.type.includes("portrait")) { + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + } else if (window.matchMedia("(orientation: portrait)").matches) { + // legacy + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + } + } catch (e) { + warnlog(e); + return; + } + var msg = {}; + msg.trackLabel = "unknown or none"; + if (track0.label) { + msg.trackLabel = track0.label; + } + msg.currentCameraConstraints = session.currentCameraConstraints; + msg.cameraConstraints = session.cameraConstraints; + + pokeIframeAPI("listing-video-settings", msg); + return msg; +} + +var Final_transcript = ""; +var Interim_transcript = ""; +var Recognition = null; + +if ("webkitSpeechRecognition" in window) { + var SpeechRecognition = webkitSpeechRecognition; +} else if ("SpeechRecognition" in window) { + var SpeechRecognition = window.SpeechRecognition; +} else { + var SpeechRecognition = false; +} + +var TranscriptionCounter = 0; +var retriesRecognition = 0; +var activeRecognition = false; +var timeoutRecognition = null; +function setupClosedCaptions() { + if (activeRecognition) { + return; + } + activeRecognition = true; + + log("CLOSED CAPTIONING SETUP"); + + if (SpeechRecognition) { + Recognition = new SpeechRecognition(); + + Recognition.lang = session.transcript; + + Recognition.continuous = true; + Recognition.interimResults = true; + Recognition.maxAlternatives = 0; + + Recognition.onstart = function () { + log("started transcription: " + Date.now()); + clearTimeout(timeoutRecognition); + timeoutRecognition = setTimeout(function () { + retriesRecognition = 0; + }, 10000); + }; + Recognition.onerror = function (event) { + if (retriesRecognition <= 3) { + console.error(event); + } + errorlog(event); + }; + Recognition.onend = function (e) { + warnlog(e); + log("Stopped transcription " + Date.now()); + clearTimeout(timeoutRecognition); + timeoutRecognition = setTimeout(function () { + Recognition.start(); + }, parseInt(500 * retriesRecognition * retriesRecognition)); // restart it if it fails. + retriesRecognition += 1; + if (retriesRecognition == 3) { + console.error("Captioning service is having a problem connecting"); + } + }; + + Recognition.onresult = function (event) { + Interim_transcript = ""; + if (typeof event.results == "undefined") { + log(event); + return; + } + for (var i = event.resultIndex; i < event.results.length; ++i) { + if (event.results[i].isFinal) { + Final_transcript += event.results[i][0].transcript; + } else { + Interim_transcript += event.results[i][0].transcript; + } + } + + if (Final_transcript.length > 0) { + log("FINAL:" + Final_transcript); + try { + var data = {}; + data.isFinal = true; + data.transcript = Final_transcript; + data.counter = TranscriptionCounter; + session.sendMessage(data); + TranscriptionCounter += 1; + Final_transcript = ""; + Interim_transcript = ""; + pokeIframeAPI("transcription-text", Final_transcript); + } catch (e) { + errorlog(e); + } + } else { + try { + var data = {}; + data.isFinal = false; + data.transcript = Interim_transcript; + data.counter = TranscriptionCounter; + session.sendMessage(data); + } catch (e) { + errorlog(e); + Interim_transcript = ""; + } + } + }; + + Recognition.start(); + } else if (!session.cleanOutput) { + warnUser(getTranslation("speech-not-suppoted"), false, false); + } +} +async function requestGoogleDriveRecord(ele, state = null, bitrate = null, event = null) { + var UUID = ele.dataset.UUID || null; + // Handle CTRL+click for selection + if (event && (event.ctrlKey || event.metaKey)) { + ele.classList.toggle("armed"); + ele.ariaPressed = ele.classList.contains("armed") ? "true" : "false"; + + // Add callback only once for all armed buttons + if (document.querySelectorAll('[data-action-type="recorder-google-drive-remote"].armed').length === 1 && + ele.classList.contains("armed")) { + Callbacks.push([multiGdriveRecord]); + } + return; + } + // Single button normal operation + if (!state && ele.classList.contains("pressed")) { + var msg = {}; + msg.requestVideoRecord = false; + msg.googleDriveRecord = false; + msg.UUID = UUID; + session.sendRequest(msg, msg.UUID); + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } else if (state == null || state) { + if (!(session.gdrive && session.gdrive.accessToken)) { + session.gdrive = setupGoogleDriveUploader(); + if (session.gdrive.promise) { + log("AWAITING PROMISE"); + try { + // Make sure we're initialized before requesting a token + await session.gdrive.ensureInitialized(); + session.gdrive.requestAccessToken(); + await session.gdrive.promise; + console.log("Promise resolved with token"); + } catch (e) { + console.error("Error getting token:", e); + ele.classList.remove("armed"); + return; + } + } + } + + var filename = UUID; + if (session.rpcs[UUID]) { + filename = session.rpcs[UUID].label || session.rpcs[UUID].streamID || UUID; + } + filename = filename.replace(/[\W]+/g, "_"); + filename = filename.substring(0, 55); + filename += "_" + Date.now().toString(); + if (SafariVersion) { + filename += ".mp4"; + } else { + filename += ".webm"; + } + + log("PROMISE DONE"); + var uploadLink = await session.gdrive.startResumableUpload(filename); + + var msg = {}; + msg.requestVideoRecord = true; + msg.googleDriveRecord = uploadLink; + msg.UUID = UUID; + + if (bitrate === null) { + window.focus(); + let response = await promptRecordingOptions(getTranslation("what-bitrate-gdrive")); + if (response) { + msg.value = response.bitrate; + msg.recordConfig = response; + session.sendRequest(msg, msg.UUID); + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.classList.remove("armed"); + } else { + ele.classList.remove("armed"); + return; + } + } else { + msg.value = bitrate; + session.sendRequest(msg, msg.UUID); + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + ele.classList.remove("armed"); + } + + pokeIframeAPI("request-video-record", msg.requestVideoRecord, UUID); + } +} +async function multiGdriveRecord() { + const armedButtons = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"].armed'); + if (!armedButtons.length) return; + + armedButtons.forEach(button => { + button.classList.remove("armed"); + button.ariaPressed = "false"; + }); + + // Get recording settings once for all buttons + window.focus(); + let response = await promptRecordingOptions(getTranslation("what-bitrate-gdrive")); + if (!response) { + return; + } + + // Set up Google Drive authentication once + if (!(session.gdrive && session.gdrive.accessToken)) { + session.gdrive = setupGoogleDriveUploader(); + if (session.gdrive.promise) { + try { + if (typeof session.gdrive.ensureInitialized === "function") { + await session.gdrive.ensureInitialized(); + } + if (typeof session.gdrive.requestAccessToken === "function") { + session.gdrive.requestAccessToken(); + } + await session.gdrive.promise; + } catch (e) { + // Auth failed, clean up armed buttons + armedButtons.forEach(button => { + button.classList.remove("armed"); + button.ariaPressed = "false"; + }); + return; + } + } + } + + // Process each armed button with the same settings + for (const button of armedButtons) { + const UUID = button.dataset.UUID || null; + + // Generate unique filename for each recording + const filename = ((session.rpcs[UUID] && (session.rpcs[UUID].label || session.rpcs[UUID].streamID)) || UUID) + .replace(/[\W]+/g, "_") + .substring(0, 55) + + "_" + Date.now().toString() + + (SafariVersion ? ".mp4" : ".webm"); + + // Get upload link for each recording + const uploadLink = await session.gdrive.startResumableUpload(filename); + + // Create message with shared settings + const msg = { + requestVideoRecord: true, + googleDriveRecord: uploadLink, + UUID: UUID, + value: response.bitrate, + recordConfig: response + }; + + // Send request and update button state + session.sendRequest(msg, msg.UUID); + button.classList.add("pressed"); + button.classList.remove("armed"); + button.ariaPressed = "true"; + + pokeIframeAPI("request-video-record", true, UUID); + } +} + +async function requestVideoRecord(ele, state = null, bitrate = null) { + var UUID = ele.dataset.UUID || null; + + if (!state && ele.classList.contains("pressed")) { + var msg = {}; + msg.requestVideoRecord = false; + msg.UUID = UUID; + session.sendRequest(msg, msg.UUID); + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } else if (state == null || state) { + var msg = {}; + msg.requestVideoRecord = true; + msg.UUID = UUID; + if (bitrate === null) { + window.focus(); + let response = await promptRecordingOptions(getTranslation("what-bitrate")); + if (response) { + msg.value = response.bitrate; + msg.recordConfig = response; + session.sendRequest(msg, msg.UUID); + ele.classList.add("pressed"); + ele.ariaPressed = "true"; // "btn-HL-green" + } else { return; } + } else { + msg.value = bitrate; + session.sendRequest(msg, msg.UUID); + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } + } + pokeIframeAPI("request-video-record", msg.requestVideoRecord, UUID); +} + +function changeOrderDirector(value) { + if (session.order == false) { + session.order = 0; + } + session.order += parseInt(value) || 0; + + var elements = document.querySelectorAll('[data-action-type="order-value-director"]'); + //log(elements); + if (elements[0]) { + elements[0].innerText = parseInt(session.order) || 0; + } + + var data = {}; + data = {}; + data.order = session.order; + session.sendPeers(data); + pokeIframeAPI("director-order", data.order); +} + +function changeOrder(value, UUID) { + var msg = {}; + msg.changeOrder = value; + msg.UUID = UUID; + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("change-order", value, UUID); +} + +function requestVideoHack(keyname, value, UUID, ctrl = false) { + var msg = {}; + msg.requestVideoHack = true; + msg.keyname = keyname; + msg.value = value; + msg.UUID = UUID; + msg.ctrl = ctrl; + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-video-setting", { value: value, keyname: keyname, ctrl: ctrl }, UUID); +} + +function requestAudioHack(keyname, value, UUID, deviceId = "default") { + var msg = {}; + msg.requestAudioHack = true; + msg.keyname = keyname; + msg.value = value; + msg.UUID = UUID; + msg.deviceId = deviceId; + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-audio-setting", { value: value, keyname: keyname, deviceId: deviceId }, UUID); +} + +function requestChangeEQ(keyname, value, UUID, track = 0) { + var msg = {}; + msg.requestChangeEQ = true; + msg.keyname = keyname; + msg.value = value; + msg.UUID = UUID; + msg.track = track; // pointless atm + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-change-eq", { value: value, keyname: keyname, track: track }, UUID); +} + +function requestChangeGating(keyname, value, UUID, track = 0) { + var msg = {}; + msg.requestChangeGating = true; + msg.keyname = keyname; + msg.value = value; + msg.UUID = UUID; + msg.track = track; // pointless atm + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-change-gating", { value: value, keyname: keyname, track: track }, UUID); +} +function requestChangeCompressor(keyname, value, UUID, track = 0) { + var msg = {}; + msg.requestChangeCompressor = true; + msg.keyname = keyname; + msg.value = value; + msg.UUID = UUID; + msg.track = track; // pointless atm + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-change-compressor", { value: value, keyname: keyname, track: track }, UUID); +} +function requestChangeMicDelay(value, UUID, track = 0) { + var msg = {}; + msg.requestChangeMicDelay = true; + msg.value = value; + msg.UUID = UUID; + msg.track = track; // pointless atm + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-change-mic-delay", { value: value, track: track }, UUID); +} + +function requestChangeSubGain(value, UUID, deviceId) { + var msg = {}; + msg.requestChangeSubGain = true; + msg.value = value; + msg.UUID = UUID; + msg.deviceId = deviceId; // pointless atm + log(msg); + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-sub-gain", { value: value, deviceId: deviceId }, UUID); +} + +function requestChangeLowcut(value, UUID, track = 0) { + var msg = {}; + msg.requestChangeLowcut = true; + msg.value = value; + msg.UUID = UUID; + msg.track = track; // pointless atm + session.sendRequest(msg, msg.UUID); + pokeIframeAPI("request-low-cut", value, UUID); +} + +function toggleSystemPip(vid, autoRetry = false) { + if (!vid) { + return Promise.resolve(false); + } + try { + if (vid.webkitSupportsPresentationMode && typeof vid.webkitSetPresentationMode === "function") { + vid.webkitSetPresentationMode(vid.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture"); + clearAutoPiPPrompt(); + return Promise.resolve(true); + } else if (!document.pictureInPictureEnabled) { + return Promise.resolve(false); + } + + var pipPromise = null; + if (document.pictureInPictureElement) { + if (document.pictureInPictureElement === vid) { + pipPromise = document.exitPictureInPicture(); + } else { + pipPromise = document.exitPictureInPicture().catch(errorlog).then(() => vid.requestPictureInPicture()); + } + } else { + pipPromise = vid.requestPictureInPicture(); + } + + if (!pipPromise || typeof pipPromise.then !== "function") { + clearAutoPiPPrompt(); + return Promise.resolve(true); + } + + return pipPromise + .then(() => { + clearAutoPiPPrompt(); + return true; + }) + .catch(err => { + if (autoRetry && err && (err.name === "NotAllowedError" || err.name === "InvalidStateError")) { + showAutoPiPPrompt(vid); + } + errorlog(err); + return false; + }); + } catch (e) { + if (autoRetry && e && (e.name === "NotAllowedError" || e.name === "InvalidStateError")) { + showAutoPiPPrompt(vid); + } + errorlog(e); + return Promise.resolve(false); + } +} + +function showAutoPiPPrompt(vid) { + if (!vid) { + return; + } + session.autoPiPPromptVideo = vid; + if (session.autoPiPPrompt) { + return; + } + var prompt = document.createElement("div"); + prompt.id = "pipPrompt"; + prompt.style.position = "fixed"; + prompt.style.bottom = "16px"; + prompt.style.right = "16px"; + prompt.style.zIndex = "12000"; + prompt.style.background = "rgba(20,20,24,0.95)"; + prompt.style.border = "1px solid rgba(255,255,255,0.2)"; + prompt.style.borderRadius = "8px"; + prompt.style.padding = "12px"; + prompt.style.maxWidth = "280px"; + prompt.style.display = "flex"; + prompt.style.flexDirection = "column"; + prompt.style.gap = "8px"; + prompt.style.boxShadow = "0 4px 12px rgba(0,0,0,0.35)"; + + var message = document.createElement("div"); + message.innerText = "Click to enable picture-in-picture"; + message.style.fontSize = "14px"; + message.style.lineHeight = "18px"; + + var controls = document.createElement("div"); + controls.style.display = "flex"; + controls.style.justifyContent = "space-between"; + controls.style.gap = "8px"; + + var confirmBtn = document.createElement("button"); + confirmBtn.type = "button"; + confirmBtn.innerText = "Open PiP"; + confirmBtn.style.flex = "1"; + confirmBtn.style.padding = "6px 10px"; + confirmBtn.style.borderRadius = "6px"; + confirmBtn.style.border = "1px solid rgba(255,255,255,0.2)"; + confirmBtn.style.background = "var(--accent-color, #3a7afe)"; + confirmBtn.style.color = "#fff"; + confirmBtn.style.cursor = "pointer"; + + var dismissBtn = document.createElement("button"); + dismissBtn.type = "button"; + dismissBtn.innerText = "Not now"; + dismissBtn.style.flex = "1"; + dismissBtn.style.padding = "6px 10px"; + dismissBtn.style.borderRadius = "6px"; + dismissBtn.style.border = "1px solid rgba(255,255,255,0.2)"; + dismissBtn.style.background = "rgba(255,255,255,0.08)"; + dismissBtn.style.color = "#fff"; + dismissBtn.style.cursor = "pointer"; + + confirmBtn.addEventListener("click", function (event) { + event.preventDefault(); + event.stopPropagation(); + var target = session.autoPiPPromptVideo; + clearAutoPiPPrompt(); + if (target) { + toggleSystemPip(target); + } + }); + + dismissBtn.addEventListener("click", function (event) { + event.preventDefault(); + event.stopPropagation(); + clearAutoPiPPrompt(); + }); + + controls.appendChild(confirmBtn); + controls.appendChild(dismissBtn); + prompt.appendChild(message); + prompt.appendChild(controls); + document.body.appendChild(prompt); + session.autoPiPPrompt = prompt; +} + +function clearAutoPiPPrompt() { + if (session.autoPiPPrompt) { + try { + session.autoPiPPrompt.remove(); + } catch (e) { } + session.autoPiPPrompt = false; + } + session.autoPiPPromptVideo = false; +} + +function updateDirectorsAudio(dataN, UUID) { + var audioEle = document.createElement("div"); + query("#container_" + UUID + " .advancedAudioSettings").innerHTML = ""; + + if (query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').classList.contains("pressed")) { + query("#container_" + UUID + " .advancedAudioSettings").classList.remove("hidden"); + } + //query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').classList.add("pressed"); + //query('[data-action-type="advanced-audio-settings"][data--u-u-i-d="' + UUID + '"]').ariaPressed = "true"; + + //log(dataN); + if (!dataN.length) { + + var label = document.createElement("label"); + label.innerText = "No microphone selected"; + label.style.display = "block"; + label.id = "remoteAudioLabel_" + UUID; + label.dataset.nomic = true; + label.classList.add("settingsLabel"); + label.dataset.UUID = UUID; + audioEle.appendChild(label); + + query('[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]').disabled = true; + + query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioEle); + return; + } + + query('[data-action-type="refresh-mic"][data--u-u-i-d="' + UUID + '"]').disabled = false; + + for (var n = 0; n < dataN.length; n += 1) { + var data = dataN[n]; + + if (dataN.length == 1) { + if (data.trackLabel) { + var label = document.createElement("label"); + label.innerText = data.trackLabel; + label.style.display = "block"; + label.id = "remoteAudioLabel_" + UUID; + label.classList.add("settingsLabel"); + label.dataset.UUID = UUID; + audioEle.appendChild(label); + } + } + //if (n !== 0) { + //var label = document.createElement("span"); + //label.innerText = "Coming Soon"; + //audioEle.appendChild(label); + // continue; // remove to more than one audio device (assuming other fixes are applied) + //} + + if ("micDelay" in data && n == 0) { + var label = document.createElement("label"); + var i = "micDelay"; + var div = document.createElement("div"); + label.id = "label_" + i + "_" + UUID; + label.htmlFor = "constraints_" + i + "_" + UUID; + + var input = document.createElement("input"); + input.min = 0; + input.max = 500; + input.value = data.micDelay || 0; + + input.title = "Previously was: " + input.value; + + input.type = "range"; + input.dataset.keyname = i; + //input.dataset.labelname = "mic delay (ms):"; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + " (ms):"; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.value = data.micDelay || 0; + manualInput.className = "manualInput"; + manualInput.id = "constraints_manual_" + i + "_" + UUID; + manualInput.dataset.UUID = UUID; + manualInput.dataset.track = n; + + input.dataset.track = n; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + UUID; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + input.style.margin = "2px 0px 5px"; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.onchange = function (e) { + //e.target.title = e.target.value; + getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.oninput = function (e) { + getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + requestChangeMicDelay(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + } + }; + + audioEle.appendChild(div); + div.appendChild(label); + div.appendChild(manualInput); + audioEle.appendChild(input); + } + + if (data.micPanning !== false && n == 0) { + // Director-side control: Mic Panning (0..180, 90=center) + var label = document.createElement("label"); + var i = "micPanning"; + var div = document.createElement("div"); + label.id = "label_" + i + "_" + UUID; + label.htmlFor = "constraints_" + i + "_" + UUID; + label.innerText = "Mic Pan:"; + + var input = document.createElement("input"); + input.min = 0; + input.max = 180; + input.value = data.micPanning || 90; + input.title = "0=L, 90=C, 180=R"; + input.type = "range"; + input.dataset.keyname = i; + input.dataset.track = n; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + UUID; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + input.style.margin = "2px 0px 5px"; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.value = data.micPanning || 90; + manualInput.className = "manualInput"; + manualInput.id = "constraints_manual_" + i + "_" + UUID; + manualInput.dataset.UUID = UUID; + manualInput.dataset.track = n; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeMicPanning(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.onchange = function (e) { + getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeMicPanning(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.oninput = function (e) { + getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + requestChangeMicPanning(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + } + }; + + audioEle.appendChild(div); + div.appendChild(label); + div.appendChild(manualInput); + audioEle.appendChild(input); + } + + if (data.lowcut !== false && n == 0) { + var label = document.createElement("label"); + var i = "lowCut"; + label.id = "label_" + i + "_" + UUID; + label.htmlFor = "constraints_" + i + "_" + UUID; + + var input = document.createElement("input"); + input.min = 50; + input.max = 150; + input.value = data.lowcut; + + input.title = "Previously was: " + input.value; + + input.type = "range"; + input.dataset.keyname = i; + //input.dataset.labelname = "low cut:"; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.value = data.lowcut; + manualInput.className = "manualInput"; + manualInput.id = "constraints_manual_" + i + "_" + UUID; + manualInput.dataset.UUID = UUID; + manualInput.dataset.track = n; + + input.dataset.track = n; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + UUID; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + input.style.margin = "2px 0px 5px"; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.onchange = function (e) { + //e.target.title = e.target.value; + getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.oninput = function (e) { + getById("constraints_manual_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + requestChangeLowcut(parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + } + }; + + audioEle.appendChild(label); + audioEle.appendChild(manualInput); + audioEle.appendChild(input); + } + + if (data.equalizer && n == 0) { + var label = document.createElement("label"); + var i = "Low_EQ"; + //label.id = "label_" + i + "_"+UUID; + label.htmlFor = "constraints_" + i + "_" + UUID; + + var input = document.createElement("input"); + input.min = -50; + input.max = 50; + input.value = data.lowEQ; + input.title = "Previously was: " + input.value; + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = "low EQ:"; + + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.value = data.lowEQ; + manualInput.className = "manualInput"; + manualInput.id = "label_" + i + "_" + UUID; + manualInput.dataset.UUID = UUID; + manualInput.dataset.track = n; + + input.dataset.track = n; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + UUID; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + input.style.margin = "2px 0px 5px"; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + requestChangeEQ("low", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + } + }; + + audioEle.appendChild(label); + audioEle.appendChild(manualInput); + audioEle.appendChild(input); + + var label = document.createElement("label"); + var i = "midEQ"; + //label.id = "label_" + i + "_"+UUID; + label.htmlFor = "constraints_" + i + "_" + UUID; + + var input = document.createElement("input"); + input.min = -50; + input.max = 50; + input.value = data.midEQ; + input.title = "Previously was: " + input.value; + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = "mid EQ:"; + + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.value = data.midEQ; + manualInput.className = "manualInput"; + manualInput.id = "label_" + i + "_" + UUID; + manualInput.dataset.UUID = UUID; + manualInput.dataset.track = n; + + input.dataset.track = n; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + UUID; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + input.style.margin = "2px 0px 5px"; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + requestChangeEQ("mid", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + } + }; + + audioEle.appendChild(label); + audioEle.appendChild(manualInput); + audioEle.appendChild(input); + + var label = document.createElement("label"); + var i = "highEQ"; + //label.id = "label_" + i + "_"+UUID; + label.htmlFor = "constraints_" + i + "_" + UUID; + + var input = document.createElement("input"); + input.min = -50; + input.max = 50; + input.value = data.highEQ; + input.title = "Previously was: " + input.value; + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = "high EQ:"; + + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.value = data.highEQ; + manualInput.className = "manualInput"; + manualInput.id = "label_" + i + "_" + UUID; + manualInput.dataset.UUID = UUID; + manualInput.dataset.track = n; + + input.dataset.track = n; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + UUID; + input.classList.add("inputConstraint"); + input.name = "constraints_" + i; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + requestChangeEQ("high", parseInt(e.target.value), e.target.dataset.UUID, parseInt(e.target.dataset.track)); + } + }; + + audioEle.appendChild(label); + audioEle.appendChild(manualInput); + audioEle.appendChild(input); + } + + if ("gating" in data && n == 0) { + // only show once. + var label = document.createElement("label"); + var i = "noiseGate"; + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i + "_" + n + "_" + UUID; + label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block; padding:0;"; + label.dataset.keyname = i; + label.dataset.track = n; + var input = document.createElement("select"); + var c = document.createElement("option"); + + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + input.options.add(opt); + + if (data.gating) { + opt.selected = "true"; + } + + input.dataset.deviceId = data.deviceId; + input.id = "constraints_" + i + "_" + n + "_" + UUID; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i + "_" + n; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.track = n; + input.dataset.UUID = UUID; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; + requestChangeGating("gating", e.target.value, e.target.dataset.UUID, parseInt(e.target.dataset.track)); + log(e.target.dataset.keyname, e.target.value); + }; + audioEle.appendChild(div); + div.appendChild(label); + div.appendChild(input); + } + + if ("compressor" in data && n == 0) { + var label = document.createElement("label"); + var i = "compressor"; + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i + "_" + n + "_" + UUID; + label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block; padding:0;"; + label.dataset.keyname = i; + label.dataset.track = n; + + var input = document.createElement("select"); + var c = document.createElement("option"); + + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", 1); + input.options.add(opt); + + if (data.compressor == 1) { + opt.selected = "true"; + } + opt = new Option("Limiter", 2); + input.options.add(opt); + + if (data.compressor == 2) { + opt.selected = "true"; + } + + input.dataset.deviceId = data.deviceId; + input.id = "constraints_" + i + "_" + n + "_" + UUID; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i + "_" + n; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.track = n; + input.dataset.UUID = UUID; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; + requestChangeCompressor("compressor", e.target.value, e.target.dataset.UUID, parseInt(e.target.dataset.track)); + log(e.target.dataset.keyname, e.target.value); + }; + audioEle.appendChild(div); + div.appendChild(label); + div.appendChild(input); + } + + if (dataN.length > 1) { + if (data.trackLabel) { + var label = document.createElement("label"); + label.innerText = data.trackLabel; + label.style.display = "block"; + label.id = "remoteAudioLabel_" + UUID + "_" + n + "_" + UUID; + label.classList.add("settingsLabel"); + audioEle.appendChild(label); + } + } + + for (var i in data.audioConstraints) { + try { + log(i); + log(data.audioConstraints[i]); + if (typeof data.audioConstraints[i] === "object" && data.audioConstraints[i] !== null && "max" in data.audioConstraints[i] && "min" in data.audioConstraints[i]) { + if (i === "aspectRatio") { + continue; + } else if (i === "width") { + continue; + } else if (i === "height") { + continue; + } else if (i === "frameRate") { + continue; + } else if (i === "latency") { + // continue; + } else if (i === "sampleRate") { + continue; + } else if (i === "channelCount") { + // continue; + } else if (i === "volume") { + continue; + } + + if (!("deviceId" in data.audioConstraints)) { + continue; + } // not going to support older versions. + + var label = document.createElement("label"); + //label.id = "label_" + i + "_"+n+ "_"+UUID; + label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + + var input = document.createElement("input"); + input.min = data.audioConstraints[i].min; + input.max = data.audioConstraints[i].max; + + if (parseFloat(input.min) == parseFloat(input.max)) { + continue; + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + + if ("step" in data.audioConstraints[i]) { + input.step = data.audioConstraints[i].step; + manualInput.step = data.audioConstraints[i].step; + } else if ("volume" == i) { + input.step = 0.01; + manualInput.step = 0.01; + } + + manualInput.dataset.keyname = i; + manualInput.className = "manualInput"; + manualInput.id = "label_" + i + "_" + n + "_" + UUID; + manualInput.max = data.audioConstraints[i].max; + manualInput.min = data.audioConstraints[i].min; + manualInput.dataset.UUID = UUID; + manualInput.dataset.track = n; + manualInput.dataset.keyname = i; + + if (i in data.currentAudioConstraints) { + input.value = data.currentAudioConstraints[i]; + manualInput.value = parseFloat(input.value); + //label.innerText = i + ": " + data.currentAudioConstraints[i]; + label.title = "Previously was: " + data.currentAudioConstraints[i]; + input.title = "Previously was: " + data.currentAudioConstraints[i]; + } else { + label.innerText = i; + } + + if (i === "height" || i === "width") { + input.title = "Hold CTRL (or cmd) to lock width and height together when changing them"; + input.min = 16; + } + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.track = n; + input.dataset.deviceId = data.deviceId; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + n + "_" + UUID; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i + "_" + n + "_" + UUID; + + if (i == "channelCount") { + input.style.display = "none"; + manualInput.style.margin = "5px 0px 9px 10px"; + } + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId); + }; + + input.onchange = function (e) { + //e.target.title = e.target.value; + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId); + }; + + audioEle.appendChild(label); + audioEle.appendChild(manualInput); + audioEle.appendChild(input); + } else if (typeof data.audioConstraints[i] === "object" && data.audioConstraints[i] !== null) { + if (i == "resizeMode") { + continue; + } + + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i + "_" + n + "_" + UUID; + label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block; padding:0;"; + var input = document.createElement("select"); + var c = document.createElement("option"); + + if (data.audioConstraints[i].length > 1) { + for (var opts in data.audioConstraints[i]) { + log(opts); + if (data.audioConstraints[i][opts] === false) { + var opt = new Option("Off", data.audioConstraints[i][opts]); + } else if (data.audioConstraints[i][opts] === true) { + var opt = new Option("On", data.audioConstraints[i][opts]); + } else { + var opt = new Option(data.audioConstraints[i][opts], data.audioConstraints[i][opts]); + } + input.options.add(opt); + if (i in data.currentAudioConstraints) { + if (data.audioConstraints[i][opts] == data.currentAudioConstraints[i]) { + opt.selected = "true"; + } + } + } + } else if (i.toLowerCase() == "torch") { + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + input.options.add(opt); + try { + if (i in data.currentAudioConstraints) { + if (data.audioConstraints[i]["torch"] == true) { + opt.selected = "true"; + } + } + } catch (e) { } + } else { + continue; + } + + input.id = "constraints_" + i + "_" + n + "_" + UUID; + input.className = "constraintCameraInput"; + input.name = input.id; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.track = n; + input.dataset.deviceId = data.deviceId; + input.dataset.UUID = UUID; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; + requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId); + log(e.target.dataset.keyname, e.target.value); + }; + audioEle.appendChild(div); + div.appendChild(label); + div.appendChild(input); + } else if (typeof data.audioConstraints[i] === "boolean") { + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i + "_" + n + "_" + UUID; + label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block; padding:0;"; + label.dataset.keyname = i; + label.dataset.track = n; + var input = document.createElement("select"); + var c = document.createElement("option"); + + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + input.options.add(opt); + + try { + if (data.audioConstraints[i] === true) { + opt.selected = "true"; + } + } catch (e) { } + + input.dataset.deviceId = data.deviceId; + input.id = "constraints_" + i + "_" + n + "_" + UUID; + input.className = "constraintCameraInput"; + input.name = input.id; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.track = n; + input.dataset.UUID = UUID; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname).innerText =e.target.dataset.keyname+": "+e.target.value; + requestAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, e.target.dataset.deviceId); + log(e.target.dataset.keyname, e.target.value); + }; + audioEle.appendChild(div); + div.appendChild(label); + div.appendChild(input); + } + } catch (e) { + errorlog(e); + } + } + + if (data.subGain !== false) { + var label = document.createElement("label"); + var i = "Gain"; + var div = document.createElement("div"); + label.id = "label_" + i + "_" + n + "_" + UUID; + label.htmlFor = "constraints_" + i + "_" + n + "_" + UUID; + + var input = document.createElement("input"); + input.min = 0; + input.max = 200; + input.value = data.subGain * 100; + input.title = "Previously was: " + parseInt(input.value); + input.type = "range"; + input.dataset.keyname = i; + input.dataset.track = n; + input.dataset.labelname = "Gain:"; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.value = data.subGain * 100; + manualInput.className = "manualInput"; + manualInput.id = "label_" + i + "_" + n + "_" + UUID; + manualInput.dataset.UUID = UUID; + manualInput.dataset.track = n; + + input.dataset.track = data.deviceId; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + n + "_" + UUID; + input.style = "display:block; width:100%;"; + input.name = input.id; + input.style.margin = "2px 0px 5px"; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track); + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + requestChangeSubGain(parseInt(e.target.value), e.target.dataset.UUID, e.target.dataset.track); + } + }; + + audioEle.appendChild(div); + div.appendChild(label); + div.appendChild(manualInput); + audioEle.appendChild(input); + } + + query("#container_" + UUID + " .advancedAudioSettings").appendChild(audioEle); + } + + if (fixScrollReset) { + clearTimeout(fixScrollReset); + fixScrollReset = null; + getById("directorlayout").scrollTop = fixScrollResetValue; + } +} + +var remoteSliderTimeout = 0; + +function updateDirectorsVideo(data, UUID) { + var videoEle = document.createElement("div"); + if (data.trackLabel) { + var label = document.createElement("label"); + label.innerText = data.trackLabel; + label.style.display = "block"; + label.id = "remoteVideoLabel_" + UUID; + label.dataset.UUID = UUID; + label.classList.add("settingsLabel"); + videoEle.appendChild(label); + } + + for (var i in data.cameraConstraints) { + try { + log(i); + log(data.cameraConstraints[i]); + + if (i === "focusMode") { + continue; // I'll handle this with FocusDistance instead + } else if (i === "whiteBalanceMode") { + continue; // I'll handle this elsewhere + } else if (i === "exposureMode") { + continue; // I'll handle this elsewhere + } + + if (typeof data.cameraConstraints[i] === "object" && data.cameraConstraints[i] !== null && "max" in data.cameraConstraints[i] && "min" in data.cameraConstraints[i]) { + if (i === "aspectRatio") { + // continue; + } else if (i === "width") { + // continue; + } else if (i === "height") { + // continue; + } else if (i === "frameRate") { + // continue; + } else if (i === "latency") { + // continue; + } else if (i === "sampleRate") { + continue; + } else if (i === "channelCount") { + // continue; + } + + var manualMode = false; + var manualLabel = false; + if (i === "exposureTime") { + if (data.currentCameraConstraints["exposureMode"]) { + manualMode = document.createElement("input"); + manualMode.type = "checkbox"; + manualMode.id = "manual_" + i + "_" + UUID; + manualMode.dataset.UUID = UUID; + manualMode.dataset.keyname = "exposureMode"; + manualMode.onchange = function (e) { + var value = "manual"; + if (e.target.checked) { + value = "continuous"; + } + requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true); + //getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + //getById("label_" + e.target.dataset.keyname+ "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + }; + manualLabel = document.createElement("label"); + manualLabel.htmlFor = manualMode.id; + manualLabel.innerHTML = "Auto: "; + manualLabel.style.marginLeft = "20px"; + if (data.currentCameraConstraints["exposureMode"] == "continuous") { + manualMode.checked = true; + } + } + } else if (i === "focusDistance") { + if (data.currentCameraConstraints["focusMode"]) { + manualMode = document.createElement("input"); + manualMode.type = "checkbox"; + manualMode.id = "manual_" + i + "_" + UUID; + manualMode.dataset.UUID = UUID; + manualMode.dataset.keyname = "focusMode"; + manualMode.onchange = function (e) { + var value = "manual"; + if (e.target.checked) { + value = "continuous"; + } + requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true); + }; + manualLabel = document.createElement("label"); + manualLabel.htmlFor = manualMode.id; + manualLabel.innerHTML = "Auto: "; + manualLabel.style.marginLeft = "20px"; + if (data.currentCameraConstraints["focusMode"] == "continuous") { + manualMode.checked = true; + } + } + } else if (i === "colorTemperature") { + if (data.currentCameraConstraints["whiteBalanceMode"]) { + manualMode = document.createElement("input"); + manualMode.type = "checkbox"; + manualMode.id = "manual_" + i + "_" + UUID; + manualMode.dataset.UUID = UUID; + manualMode.dataset.keyname = "whiteBalanceMode"; + manualMode.onchange = function (e) { + var value = "manual"; + if (e.target.checked) { + value = "continuous"; + } + requestVideoHack(e.target.dataset.keyname, value, e.target.dataset.UUID, true); + }; + manualLabel = document.createElement("label"); + manualLabel.htmlFor = manualMode.id; + manualLabel.innerHTML = "Auto: "; + manualLabel.style.marginLeft = "20px"; + if (data.currentCameraConstraints["whiteBalanceMode"] == "continuous") { + manualMode.checked = true; + } + } + } + + var label = document.createElement("label"); + //label.id = "label_" + i; + label.htmlFor = "constraints_" + i + " _" + UUID; + if (i === "colorTemperature") { + label.innerText = "Color Temp:"; + } else if (i === "exposureCompensation") { + label.innerText = "Exposure Comp:"; + } else if (i === "exposureTime") { + label.innerText = "Exposure:"; + } else if (i === "focusDistance") { + label.innerText = "Focus:"; + } else { + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + } + + if (i === "zoom" || i === "pan" || i === "til") { + label.innerHTML = " " + label.innerText; + } + + var input = document.createElement("input"); + + if (i === "aspectRatio") { + input.max = 5; + input.min = 0.2; + input.step = 0.00001; + } else if (i === "exposureTime") { + input.min = data.cameraConstraints[i].min; + input.max = Math.min(data.cameraConstraints[i].max, 2000); + } else { + input.min = data.cameraConstraints[i].min; + input.max = data.cameraConstraints[i].max; + } + + if (parseFloat(input.min) == parseFloat(input.max)) { + continue; + } + + if (i in data.currentCameraConstraints) { + input.value = data.currentCameraConstraints[i]; + label.title = "Previously was: " + data.currentCameraConstraints[i]; + input.title = "Previously was: " + data.currentCameraConstraints[i]; + } + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.UUID = UUID; + input.id = "constraints_" + i + "_" + UUID; + input.name = input.id; + input.classList.add("inputConstraint"); + input.manualMode = manualMode; + + if (i === "height" || i === "width") { + input.title = "Hold CTRL (or cmd) to lock width and height together when changing them"; + input.min = 16; + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.value = parseFloat(input.value); + manualInput.className = "manualInput"; + manualInput.id = "label_" + i + "_" + UUID; + manualInput.name = manualInput.id; + manualInput.dataset.keyname = i; + manualInput.dataset.UUID = UUID; + manualInput.manualMode = manualMode; + + if ("step" in data.cameraConstraints[i]) { + manualInput.step = data.cameraConstraints[i].step; + input.step = data.cameraConstraints[i].step; + } else if (i === "aspectRatio") { + input.step = 0.000001; + manualInput.step = 0.005; + } + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (e.target.manualMode) { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); + } else { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); + } + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + //updateVideoConstraints(e.target.dataset.keyname, e.target.value); + if (CtrlPressed || e.target.manualMode) { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); + } else { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); + } + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + if (Date.now() - remoteSliderTimeout > 100) { + remoteSliderTimeout = Date.now(); + if (CtrlPressed) { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); + } else { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); + } + } + }; + + videoEle.appendChild(label); + videoEle.appendChild(manualInput); + + if (manualMode && manualLabel) { + videoEle.appendChild(manualLabel); + videoEle.appendChild(manualMode); + } + + if (i === "aspectRatio") { + var preSelectButton = document.createElement("button"); + preSelectButton.value = 16 / 9.0; + preSelectButton.innerText = "16:9"; + preSelectButton.dataset.keyname = i; + preSelectButton.dataset.UUID = UUID; + preSelectButton.className = "preSelectButton"; + preSelectButton.onclick = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); + }; + videoEle.appendChild(preSelectButton); + var preSelectButton = document.createElement("button"); + preSelectButton.value = 9 / 16.0; + preSelectButton.innerText = "9:16"; + preSelectButton.dataset.UUID = UUID; + preSelectButton.className = "preSelectButton"; + preSelectButton.dataset.keyname = i; + preSelectButton.onclick = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.UUID).value = parseFloat(e.target.value); + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); + }; + videoEle.appendChild(preSelectButton); + } + + videoEle.appendChild(input); + } else if (typeof data.cameraConstraints[i] === "object" && data.cameraConstraints[i] !== null) { + if (i == "resizeMode") { + continue; + } + + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i + "_" + UUID; + label.name = label.id; + label.htmlFor = "constraints_" + i + "_" + UUID; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block; padding:0;"; + label.dataset.keyname = i; + label.dataset.UUID = UUID; + var input = document.createElement("select"); + var c = document.createElement("option"); + + if (data.cameraConstraints[i].length > 1) { + for (var opts in data.cameraConstraints[i]) { + log(opts); + if (data.cameraConstraints[i][opts] === false) { + var opt = new Option("Off", data.cameraConstraints[i][opts]); + } else if (data.cameraConstraints[i][opts] === true) { + var opt = new Option("On", data.cameraConstraints[i][opts]); + } else { + var opt = new Option(data.cameraConstraints[i][opts], data.cameraConstraints[i][opts]); + } + input.options.add(opt); + if (i in data.currentCameraConstraints) { + if (data.cameraConstraints[i][opts] == data.currentCameraConstraints[i]) { + opt.selected = "true"; + } + } + } + } else if (i.toLowerCase() == "torch") { + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + input.options.add(opt); + try { + if (i in data.currentCameraConstraints) { + if (data.cameraConstraints[i]["torch"] == true) { + opt.selected = "true"; + } + } + } catch (e) { } + } else { + continue; + } + + input.id = "constraints_" + i + "_" + UUID; + input.className = "constraintCameraInput"; + input.name = input.id; + input.dataset.UUID = UUID; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname+ "_" + e.target.dataset.UUID).innerText =e.target.dataset.keyname+": "+e.target.value; + //updateVideoConstraints(e.target.dataset.keyname, e.target.value); + if (CtrlPressed) { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); + } else { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); + } + log(e.target.dataset.keyname, e.target.value); + }; + videoEle.appendChild(div); + div.appendChild(label); + div.appendChild(input); + } else if (typeof data.cameraConstraints[i] === "boolean") { + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i + "_" + UUID; + label.name = label.id; + label.htmlFor = "constraints_" + i + "_" + UUID; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block; padding:0;"; + label.dataset.keyname = i; + label.dataset.UUID = UUID; + var input = document.createElement("select"); + var c = document.createElement("option"); + + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + input.options.add(opt); + + try { + if (data.audioConstraints[i] === true) { + opt.selected = "true"; + } + } catch (e) { } + + input.id = "constraints_" + i + "_" + UUID; + input.className = "constraintCameraInput"; + input.name = input.id; + input.style = "display:inline; padding:2px;"; + input.dataset.UUID = UUID; + input.dataset.keyname = i; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname+ "_" + e.target.dataset.UUID).innerText =e.target.dataset.keyname+": "+e.target.value; + //updateVideoConstraints(e.target.dataset.keyname, e.target.value); + if (CtrlPressed) { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, true); + } else { + requestVideoHack(e.target.dataset.keyname, e.target.value, e.target.dataset.UUID, false); + } + log(e.target.dataset.keyname, e.target.value); + }; + videoEle.appendChild(div); + div.appendChild(label); + div.appendChild(input); + } + } catch (e) { + errorlog(e); + } + } + + query("#container_" + UUID + " .advancedVideoSettings").innerHTML = ""; + query("#container_" + UUID + " .advancedVideoSettings").appendChild(videoEle); + query("#container_" + UUID + " .advancedVideoSettings").classList.remove("hidden"); + + if (fixScrollReset) { + clearTimeout(fixScrollReset); + fixScrollReset = null; + getById("directorlayout").scrollTop = fixScrollResetValue; + } +} + +/////// +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function listAudioSettings() { + getById("popupSelector_constraints_audio").innerHTML = ""; + + var tracks = session.streamSrc.getAudioTracks(); + if (!tracks.length) { + warnlog("session.streamSrc contains no audio tracks"); + return; + } + + for (var ii = 0; ii < tracks.length; ii++) { + track0 = tracks[ii]; + if (track0.getCapabilities) { + session.audioConstraints = track0.getCapabilities(); + } else if (Firefox) { + // let's pretend like Firefox doesn't actually suck + session.audioConstraints = { + autoGainControl: [true, false], + // "channelCount": { + // "max": 2, + // "min": 1 + // }, + // "deviceId": "default", + echoCancellation: [true, false], + // "groupId": "a3cbdec54a9b6ed473fd950415626f7e76f9d1b90f8c768faab572175a355a17", + // "latency": { + // "max": 0.01, + // "min": 0.01 + // }, + noiseSuppression: [true, false] + // "sampleRate": { + // "max": 48000, + // "min": 48000 + // }, + // "sampleSize": { + // "max": 16, + // "min": 16 + /// } + }; + } + + try { + if (track0.getSettings) { + session.currentAudioConstraints = track0.getSettings(); + + if (!session.stereo) { + try { + delete session.currentAudioConstraints.channelCount; + delete session.audioConstraints.channelCount; + } catch (e) { } + } else if (session.audioInputChannels && session.audioInputChannels == 1) { + // this is pretty hacky, but it gets around not being able to actually set 1-channel. Not sure why. + session.currentAudioConstraints.channelCount = 1; + } + } + } catch (e) { + errorlog(e); + } + + ////// + if (ii == 0) { + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].gainNode) { + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + var div = document.createElement("div"); + var label = document.createElement("label"); + var i = "masterGain"; + //label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block;"; + + var input = document.createElement("input"); + input.min = 0; + input.max = 200; + + input.dataset.deviceid = track0.id; // pointless + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + input.value = session.webAudios[webAudio].gainNode.gain.value * 100; + //label.innerHTML += " " + parseInt(session.webAudios[webAudio].gainNode.gain.value * 100); + input.title = parseInt(input.value); + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.dataset.deviceid = track0.id; + manualInput.dataset.labelname = label.innerHTML; + manualInput.value = session.webAudios[webAudio].gainNode.gain.value * 100; + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMainGain(e.target.value); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMainGain(e.target.value); + e.target.title = e.target.value; + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMainGain(e.target.value); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(div); + div.appendChild(label); + div.appendChild(manualInput); + div.appendChild(input); + break; + } + } + } + + if (session.micDelay !== false && ii == 0) { + // ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node) + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + var label = document.createElement("label"); + var i = "micDelay"; + label.htmlFor = "constraints_" + i; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + " (ms):"; + + var input = document.createElement("input"); + input.min = 0; + input.max = 500; + + input.dataset.deviceid = track0.id; // pointless, for now + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].micDelay) { + // session.webAudios[waid].micDelay.delayTime.setValueAtTime + input.value = session.webAudios[webAudio].micDelay.delayTime.value * 1000; + label.innerHTML += " " + parseInt(session.webAudios[webAudio].micDelay.delayTime.value * 1000); + input.title = input.value; + break; + } + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.dataset.labelname = label.innerHTML; + manualInput.value = parseFloat(input.value); + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMicDelay(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMicDelay(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMicDelay(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(label); + getById("popupSelector_constraints_audio").appendChild(manualInput); + getById("popupSelector_constraints_audio").appendChild(input); + } + + // Mic Panning - local settings UI + if (session.micPanning !== false && ii == 0) { + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + var label = document.createElement("label"); + var i = "micPanning"; + label.htmlFor = "constraints_" + i; + label.innerText = "Mic Pan:"; + + var input = document.createElement("input"); + input.min = 0; + input.max = 180; + + input.dataset.deviceid = track0.id; + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + input.value = session.micPanning !== false ? session.micPanning : 90; + label.innerHTML += " " + parseInt(input.value); + input.title = input.value + " (0=L, 90=C, 180=R)"; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.dataset.deviceid = track0.id; + manualInput.dataset.labelname = label.innerHTML; + manualInput.value = parseInt(input.value); + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMicPanning(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMicPanning(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMicPanning(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(label); + getById("popupSelector_constraints_audio").appendChild(manualInput); + getById("popupSelector_constraints_audio").appendChild(input); + } + + if (session.lowcut && ii == 0) { + // ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node) + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + var label = document.createElement("label"); + var i = "Low_Cut"; + label.htmlFor = "constraints_" + i; + label.innerText = "Low Cut:"; + + var input = document.createElement("input"); + input.min = 50; + input.max = 400; + + input.dataset.deviceid = track0.id; // pointless + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].lowcut1) { + input.value = session.webAudios[webAudio].lowcut1.frequency.value; + label.innerHTML += " " + session.webAudios[webAudio].lowcut1.frequency.value; + input.title = input.value; + break; + } + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.dataset.labelname = label.innerHTML; + manualInput.value = parseFloat(input.value); + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeLowCut(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeLowCut(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeLowCut(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(label); + getById("popupSelector_constraints_audio").appendChild(manualInput); + getById("popupSelector_constraints_audio").appendChild(input); + } + + if (session.equalizer && ii == 0) { + // ii==0 implies only track0 is supported by the web audio pipeline currently (or everything after the mixer node) + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + var label = document.createElement("label"); + var i = "Low_EQ"; + //label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = "Low EQ:"; + + var input = document.createElement("input"); + input.min = -50; + input.max = 50; + + input.dataset.deviceid = track0.id; // pointless + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].lowEQ) { + input.value = session.webAudios[webAudio].lowEQ.gain.value; + label.innerHTML += " " + session.webAudios[webAudio].lowEQ.gain.value; + input.title = input.value; + } + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.dataset.labelname = label.innerHTML; + manualInput.value = parseFloat(input.value); + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeLowEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeLowEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeLowEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(label); + getById("popupSelector_constraints_audio").appendChild(manualInput); + getById("popupSelector_constraints_audio").appendChild(input); + // + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + var label = document.createElement("label"); + var i = "Mid_EQ"; + //label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = "Mid EQ:"; + + var input = document.createElement("input"); + input.min = -50; + input.max = 50; + + input.dataset.deviceid = track0.id; // pointless + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].midEQ) { + input.value = session.webAudios[webAudio].midEQ.gain.value; + label.innerHTML += " " + session.webAudios[webAudio].midEQ.gain.value; + input.title = input.value; + } + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.dataset.labelname = label.innerHTML; + manualInput.value = parseFloat(input.value); + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMidEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMidEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeMidEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(label); + getById("popupSelector_constraints_audio").appendChild(manualInput); + getById("popupSelector_constraints_audio").appendChild(input); + // + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + var label = document.createElement("label"); + var i = "High_EQ"; + //label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerHTML = "High EQ:"; + + var input = document.createElement("input"); + input.min = -50; + input.max = 50; + + input.dataset.deviceid = track0.id; // pointless + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].highEQ) { + input.value = session.webAudios[webAudio].highEQ.gain.value; + label.innerHTML += " " + session.webAudios[webAudio].highEQ.gain.value; + input.title = input.value; + } + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.dataset.labelname = label.innerHTML; + manualInput.value = parseFloat(input.value); + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeHighEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeHighEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + changeHighEQ(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(label); + getById("popupSelector_constraints_audio").appendChild(manualInput); + getById("popupSelector_constraints_audio").appendChild(input); + } + + if (session.noisegate !== false && ii == 0) { + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].gatingNode) { + var div = document.createElement("div"); + var label = document.createElement("label"); + + var i = "noiseGating"; + + label.id = "label_" + i + "_" + ii; + label.htmlFor = "constraints_" + i + "_" + ii; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block;"; + label.dataset.keyname = i; + label.title = "This will reduce the gain ~80% when there is no one talking loudly"; + var input = document.createElement("select"); + var c = document.createElement("option"); + + input.dataset.deviceid = track0.id; + + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + if (session.noisegate) { + opt.selected = "true"; + } + input.options.add(opt); + + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + input.id = "constraints_" + i + "_" + ii; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i + "_" + ii; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + if (e.target.value == "false") { + session.noisegate = null; + } else if (e.target.value == "true") { + session.noisegate = true; + } else { + session.noisegate = e.target.value; + } + if (!session.noisegate) { + changeGatingGain(100); + changeGatingGain(100, 3100); + } + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(div); + div.appendChild(label); + div.appendChild(input); + break; + } + } + } + + //////// + if (tracks.length > 1) { + var label = document.createElement("h4"); + label.innerHTML = track0.label; + label.style = "text-shadow: 0 0 10px #fff3;margin:0px 0 10px 0"; + if (ii > 0) { + label.style = "text-shadow: 0 0 10px #fff3;margin:40px 0 10px 0"; + } + getById("popupSelector_constraints_audio").appendChild(label); + } + + for (var i in session.audioConstraints) { + try { + log(i); + log(session.audioConstraints[i]); + + if (typeof session.audioConstraints[i] === "object" && session.audioConstraints[i] !== null && "max" in session.audioConstraints[i] && "min" in session.audioConstraints[i]) { + if (i === "aspectRatio") { + continue; + } else if (i === "width") { + continue; + } else if (i === "height") { + continue; + } else if (i === "frameRate") { + continue; + } else if (i === "latency") { + // continue; + } else if (i === "sampleRate") { + //continue; + } else if (i === "sampleSize") { + //continue; + } else if (i === "channelCount") { + if (!(session.stereo && session.stereo != 3)) { + // not stereo + continue; + } + } else if (!session.disableWebAudio && i === "volume") { + continue; + } + + var label = document.createElement("label"); + //label.id = "label_" + i + "_"+ii; + label.htmlFor = "constraints_" + i + "_" + ii; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + + var input = document.createElement("input"); + input.min = session.audioConstraints[i].min; + input.max = session.audioConstraints[i].max; + + input.dataset.deviceid = track0.id; + + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + + if ("step" in session.audioConstraints[i]) { + input.step = session.audioConstraints[i].step; + manualInput.step = session.audioConstraints[i].step; + } else if ("volume" == i) { + input.step = 0.01; + manualInput.step = 0.01; + } + + if (i in session.currentAudioConstraints) { + input.value = parseFloat(session.currentAudioConstraints[i]); + label.title = "Previously was: " + session.currentAudioConstraints[i]; + input.title = "Previously was: " + session.currentAudioConstraints[i]; + } + + if (i === "height" || i === "width") { + input.title = "Hold CTRL (or cmd) to lock width and height together when changing them"; + input.min = 16; + } else if (i == "sampleRate") { + label.title = "Audio typically gets resampled to 48-kHz"; + } + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.track = ii; + + input.id = "constraints_" + i + "_" + ii; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i + "_" + ii; + + manualInput.dataset.keyname = i; + manualInput.dataset.track = ii; + manualInput.dataset.deviceid = track0.id; + + manualInput.className = "manualInput"; + manualInput.id = "label_" + i + "_" + ii; + manualInput.max = session.audioConstraints[i].max; + manualInput.min = session.audioConstraints[i].min; + + manualInput.value = parseFloat(session.currentAudioConstraints[i]); + + manualInput.onchange = function (e) { + try { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.track).value = parseFloat(e.target.value); + applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + } catch (e) { + errorlog(e); + } + }; + + input.onchange = function (e) { + try { + getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.track).value = parseFloat(e.target.value); + applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + } catch (e) { + errorlog(e); + } + }; + + // not sure if I should include "oninput" as well? Probably not needed. + + var div = document.createElement("div"); + if (parseFloat(input.min) == parseFloat(input.max)) { + manualInput.disabled = true; + manualInput.title = "Only one option available, so can't be changed"; + label.title = "Only one option available, so can't be changed"; + div.appendChild(label); + div.appendChild(manualInput); + getById("popupSelector_constraints_audio").appendChild(div); + } else { + div.appendChild(label); + div.appendChild(manualInput); + div.appendChild(input); + getById("popupSelector_constraints_audio").appendChild(div); + } + } else if (typeof session.audioConstraints[i] === "object" && session.audioConstraints[i] !== null) { + if (i == "resizeMode") { + continue; + } + + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i + "_" + ii; + label.htmlFor = "constraints_" + i + "_" + ii; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block;"; + label.dataset.keyname = i; + + var input = document.createElement("select"); + var c = document.createElement("option"); + + if (session.audioConstraints[i].length == 2) { + for (var opts in session.audioConstraints[i]) { + log(opts); + if (session.audioConstraints[i][opts] === true) { + var opt = new Option("On", session.audioConstraints[i][opts]); + } else if (session.audioConstraints[i][opts] === false) { + var opt = new Option("Off", session.audioConstraints[i][opts]); + } else { + var opt = new Option(session.audioConstraints[i][opts], session.audioConstraints[i][opts]); + } + input.options.add(opt); + if (i in session.currentAudioConstraints) { + if (session.audioConstraints[i][opts] == session.currentAudioConstraints[i]) { + opt.selected = "true"; + } + } + } + } else if (session.audioConstraints[i].length > 1) { + for (var opts in session.audioConstraints[i]) { + log(opts); + var opt = new Option(session.audioConstraints[i][opts], session.audioConstraints[i][opts]); + input.options.add(opt); + if (i in session.currentAudioConstraints) { + if (session.audioConstraints[i][opts] == session.currentAudioConstraints[i]) { + opt.selected = "true"; + } + } + } + } else if (i.toLowerCase() == "torch") { + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + input.options.add(opt); + } else { + continue; + } + + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + input.id = "constraints_" + i + "_" + ii; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i + "_" + ii; + input.dataset.deviceid = track0.id; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid); + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + getById("popupSelector_constraints_audio").appendChild(div); + div.appendChild(label); + div.appendChild(input); + } else if (typeof session.audioConstraints[i] === "boolean") { + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i + "_" + ii; + label.htmlFor = "constraints_" + i + "_" + ii; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block;"; + label.dataset.keyname = i; + var input = document.createElement("select"); + var c = document.createElement("option"); + + input.dataset.deviceid = track0.id; + + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + input.options.add(opt); + + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + + input.id = "constraints_" + i + "_" + ii; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i + "_" + ii; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; + applyAudioHack(e.target.dataset.keyname, e.target.value, e.target.dataset.deviceid); + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + getById("popupSelector_constraints_audio").appendChild(div); + div.appendChild(label); + div.appendChild(input); + } + } catch (e) { + errorlog(e); + } + } + + if (tracks.length > 1) { + for (var webAudio in session.webAudios) { + if (session.webAudios[webAudio].subGainNodes && track0.id in session.webAudios[webAudio].subGainNodes) { + if (getById("popupSelector_constraints_audio").style.display == "none") { + getById("advancedOptionsAudio").style.display = "inline-flex"; + } + var div = document.createElement("div"); + var label = document.createElement("label"); + var i = "Gain"; + label.id = "label_" + i + "_" + track0.id; + label.htmlFor = "constraints_" + i + "_" + track0.id; + label.innerText = "Gain:"; + label.style = "display:inline-block; padding:0;margin-top: 15px"; + + var input = document.createElement("input"); + input.min = 0; + input.max = 200; + + input.dataset.deviceid = track0.id; // pointless + + input.type = "range"; + input.dataset.keyname = i; + input.dataset.labelname = label.innerHTML; + input.id = "constraints_" + i + "_" + track0.id; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i + "_" + track0.id; + + input.value = session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100; + //label.innerText += " " + parseInt(session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100); + input.title = parseInt(input.value); + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + manualInput.dataset.deviceid = track0.id; + manualInput.dataset.labelname = label.innerHTML; + manualInput.value = session.webAudios[webAudio].subGainNodes[track0.id].gain.value * 100; + manualInput.className = "manualInput"; + manualInput.id = "manualInput_" + i + "_" + track0.id; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value); + //getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value); + getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value); + changeSubGain(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + //getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value); + getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value); + changeSubGain(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + }; + + input.onchange = function (e) { + //getById("label_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).innerText = "Gain: " + parseInt(e.target.value); + getById("manualInput_" + e.target.dataset.keyname + "_" + e.target.dataset.deviceid).value = parseFloat(e.target.value); + changeSubGain(e.target.value, e.target.dataset.deviceid); + e.target.title = e.target.value; + pokeIframeAPI("mic-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_audio").appendChild(div); + div.appendChild(label); + div.appendChild(manualInput); + div.appendChild(input); + break; + } + } + } + } +} + +function applyAudioHack(constraint, value = null, deviceid = "default") { + if (value == parseFloat(value)) { + value = parseFloat(value); + if (constraint == "channelCount") { + session.audioInputChannels = value; + } + value = { + exact: value + }; + } else if (value == "true") { + value = true; + } else if (value == "false") { + value = false; + } + + try { + var tracks = session.streamSrc.getAudioTracks(); + if (!tracks.length) { + warnlog("session.streamSrc contains no audio tracks"); + return; + } + + var track0 = tracks[0]; + for (var ii = 0; ii < tracks.length; ii++) { + if (tracks[ii].id == deviceid) { + track0 = tracks[ii]; + break; + } + } + + if (track0.getCapabilities) { + session.audioConstraints = track0.getCapabilities(); + } else if (Firefox) { + // Firefox fallback + session.audioConstraints = { + autoGainControl: [true, false], + deviceId: deviceid, + echoCancellation: [true, false], + noiseSuppression: [true, false] + }; + } + log(session.audioConstraints); + + if (track0.getSettings) { + session.currentAudioConstraints = track0.getSettings(); + } + } catch (e) { + warnlog("Error getting audio track info"); + errorlog(e); + return; + } + + var new_constraints = Object.assign({}, session.currentAudioConstraints, { + [constraint]: value + }); + new_constraints = { + audio: new_constraints, + video: false + }; + log("new constraints"); + log(new_constraints); + activatedPreview = false; + + enumerateDevices() + .then(gotDevices2) + .then(function () { + grabAudio("#audioSource3", null, new_constraints, false, saveAudioResult); + }); +} + +// saveAudioResult is disabled but keeping structure for potential future use +function saveAudioResult() { + return false; // DISABLED: we can't load audio settings, so no point in saving them + + /* Future implementation when audio settings can be loaded: + if (!session.streamSrc) { + return; + } + var tracks = session.streamSrc.getAudioTracks(); + if (!tracks.length) { + return; + } + var track0 = tracks[0]; + session.currentAudioConstraints = track0.getSettings(); + if (session.currentAudioConstraints.deviceId) { + setStorage("audio_" + session.currentAudioConstraints.deviceId, session.currentAudioConstraints); + } + */ +} + +function listCameraSettings() { + getById("popupSelector_constraints_video").innerHTML = ""; + + if (session.controlRoomBitrate === true) { + session.controlRoomBitrate = session.totalRoomBitrate; + } + + if (session.roomid && session.view !== "" && session.controlRoomBitrate !== false) { + log("LISTING OPTION FOR BITRATE CONTROL"); + var i = "Room Video Bitrate (kbps)"; + var label = document.createElement("label"); + + label.htmlFor = "constraints_" + i; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.title = "If you're on a slow network, you can improve frame rate and audio quality by reducing the amount of video data that others send you"; + + var input = document.createElement("input"); + input.min = 0; + input.max = parseInt(session.totalRoomBitrate * 1.5); + + if (getById("popupSelector_constraints_video").style.display == "none") { + getById("advancedOptionsCamera").style.display = "inline-flex"; + } + + input.value = parseInt(session.controlRoomBitrate); + label.innerHTML = i + ": "; + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.value = session.controlRoomBitrate; + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + input.type = "range"; + input.dataset.keyname = i; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + input.title = "If you're on a slow network, you can improve frame rate and audio quality by reducing the amount of video data that others send you"; + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + if (e.target.value > session.totalRoomBitrate) { + return; + } else { + session.controlRoomBitrate = parseInt(e.target.value); + } + updateMixer(); + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + if (e.target.value > session.totalRoomBitrate) { + return; + } else { + session.controlRoomBitrate = parseInt(e.target.value); + } + updateMixer(); + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + getById("popupSelector_constraints_video").appendChild(label); + getById("popupSelector_constraints_video").appendChild(manualInput); + getById("popupSelector_constraints_video").appendChild(input); + } + try { + var track0 = session.streamSrc.getVideoTracks(); + if (track0.length) { + track0 = track0[0]; + if (track0.getCapabilities) { + session.cameraConstraints = track0.getCapabilities(); + } else { + session.cameraConstraints = {}; // probably firefox... + } + log(session.cameraConstraints); + } + } catch (e) { + errorlog(e); + return; + } + + try { + if (track0.getSettings) { + session.currentCameraConstraints = track0.getSettings(); + if (session.mobile) { + if (screen && screen.orientation && screen.orientation.type) { + if (!screen.orientation.type.includes("portrait")) { + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + } else if (!window.matchMedia("(orientation: portrait)").matches) { + if (session.currentCameraConstraints && session.currentCameraConstraints.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + } + } else { + session.currentCameraConstraints = {}; + } + } catch (e) { + errorlog(e); + } + + var orderedConstraints = {}; + if (session.cameraConstraints.torch) { + orderedConstraints.torch = session.cameraConstraints.torch; + } + if (session.cameraConstraints.aspectRatio) { + orderedConstraints.aspectRatio = session.cameraConstraints.aspectRatio; + } + if (session.cameraConstraints.width) { + orderedConstraints.width = session.cameraConstraints.width; + } + if (session.cameraConstraints.height) { + orderedConstraints.height = session.cameraConstraints.height; + } + if (session.cameraConstraints.zoom) { + orderedConstraints.zoom = session.cameraConstraints.zoom; + } + for (var key in session.cameraConstraints) { + if (session.cameraConstraints.hasOwnProperty(key) && key !== "width" && key !== "height") { + orderedConstraints[key] = session.cameraConstraints[key]; + } + } + session.cameraConstraints = orderedConstraints; + + for (var i in session.cameraConstraints) { + try { + log(i); + log(session.cameraConstraints[i]); + + if (i === "focusMode") { + continue; // I'll handle this with FocusDistance instead + } else if (i === "whiteBalanceMode") { + continue; // I'll handle this elsewhere + } else if (i === "exposureMode") { + continue; // I'll handle this elsewhere + } + + if (typeof session.cameraConstraints[i] === "object" && session.cameraConstraints[i] !== null && "max" in session.cameraConstraints[i] && "min" in session.cameraConstraints[i]) { + var manualMode = false; + var manualLabel = false; + if (i === "exposureTime") { + if (session.currentCameraConstraints["exposureMode"]) { + manualMode = document.createElement("input"); + manualMode.type = "checkbox"; + manualMode.id = "manual_" + i; + manualMode.dataset.keyname = "exposureMode"; + manualMode.onchange = function (e) { + var value = "manual"; + if (e.target.checked) { + value = "continuous"; + } + if (CtrlPressed) { + updateCameraConstraints(e.target.dataset.keyname, value, true, false); + } else { + updateCameraConstraints(e.target.dataset.keyname, value, false, false); + } + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value }); + }; + manualLabel = document.createElement("label"); + manualLabel.htmlFor = manualMode.id; + manualLabel.innerHTML = "Auto: "; + manualLabel.style.marginLeft = "20px"; + if (session.currentCameraConstraints["exposureMode"] == "continuous") { + manualMode.checked = true; + } + } + } else if (i === "focusDistance") { + if (session.currentCameraConstraints["focusMode"]) { + manualMode = document.createElement("input"); + manualMode.type = "checkbox"; + manualMode.id = "manual_" + i; + manualMode.dataset.keyname = "focusMode"; + manualMode.onchange = function (e) { + var value = "manual"; + if (e.target.checked) { + value = "continuous"; + } + if (CtrlPressed) { + updateCameraConstraints(e.target.dataset.keyname, value, true, false); + } else { + updateCameraConstraints(e.target.dataset.keyname, value, false, false); + } + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value }); + }; + manualLabel = document.createElement("label"); + manualLabel.htmlFor = manualMode.id; + manualLabel.innerHTML = "Auto: "; + manualLabel.style.marginLeft = "20px"; + if (session.currentCameraConstraints["focusMode"] == "continuous") { + manualMode.checked = true; + } + } + } else if (i === "colorTemperature") { + if (session.currentCameraConstraints["whiteBalanceMode"]) { + manualMode = document.createElement("input"); + manualMode.type = "checkbox"; + manualMode.id = "manual_" + i; + manualMode.dataset.keyname = "whiteBalanceMode"; + manualMode.onchange = function (e) { + var value = "manual"; + if (e.target.checked) { + value = "continuous"; + } + if (CtrlPressed) { + updateCameraConstraints(e.target.dataset.keyname, value, true, false); + } else { + updateCameraConstraints(e.target.dataset.keyname, value, false, false); + } + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: value }); + }; + manualLabel = document.createElement("label"); + manualLabel.htmlFor = manualMode.id; + manualLabel.innerHTML = "Auto: "; + manualLabel.style.marginLeft = "20px"; + if (session.currentCameraConstraints["whiteBalanceMode"] == "continuous") { + manualMode.checked = true; + } + } + } + + var label = document.createElement("label"); + label.htmlFor = "constraints_" + i; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + + var input = document.createElement("input"); + input.min = parseFloat(session.cameraConstraints[i].min); + + if (i === "aspectRatio") { + input.max = 5; + input.min = 0.2; + } else if (i === "exposureTime") { + input.min = parseFloat(session.cameraConstraints[i].min); + input.max = Math.min(parseFloat(session.cameraConstraints[i].max), 2000); + } else { + input.min = parseFloat(session.cameraConstraints[i].min); + input.max = parseFloat(session.cameraConstraints[i].max); + } + + if (parseFloat(input.min) == parseFloat(input.max)) { + continue; + } + + if (getById("popupSelector_constraints_video").style.display == "none") { + getById("advancedOptionsCamera").style.display = "inline-flex"; + } + + var manualInput = document.createElement("input"); + manualInput.type = "number"; + manualInput.dataset.keyname = i; + + manualInput.className = "manualInput"; + manualInput.id = "label_" + i; + + if ("step" in session.cameraConstraints[i]) { + input.step = session.cameraConstraints[i].step; + manualInput.step = session.cameraConstraints[i].step; + } else if (i === "aspectRatio") { + input.step = 0.000001; + manualInput.step = 0.005; + } + + if (i in session.currentCameraConstraints) { + input.value = parseFloat(session.currentCameraConstraints[i]); + //label.innerHTML = i + ": " + session.currentCameraConstraints[i]; + manualInput.value = parseFloat(session.currentCameraConstraints[i]); + label.title = "Previously was: " + session.currentCameraConstraints[i]; + input.title = "Previously was: " + session.currentCameraConstraints[i]; + } else { + label.innerHTML = i; + } + if (i === "height" || i === "width") { + input.title = "Hold CTRL (or cmd) to lock width and height together when changing them"; + input.min = 16; + } + + input.type = "range"; + input.dataset.keyname = i; + input.id = "constraints_" + i; + input.style = "display:block; width:100%;"; + input.name = "constraints_" + i; + + // on manualInput.change = .. update the input field! gotta riprocate + + manualInput.onchange = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false); + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + input.oninput = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + if (CtrlPressed) { + updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false); + } else { + updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false); + } + }; + + input.onchange = function (e) { + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + if (CtrlPressed) { + updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false); + } else { + updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false); + } + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + + getById("popupSelector_constraints_video").appendChild(label); + getById("popupSelector_constraints_video").appendChild(manualInput); + if (manualMode && manualLabel) { + getById("popupSelector_constraints_video").appendChild(manualLabel); + getById("popupSelector_constraints_video").appendChild(manualMode); + } + + if (i === "aspectRatio") { + var preSelectButton = document.createElement("button"); + preSelectButton.value = 16 / 9.0; + preSelectButton.innerText = "16:9"; + preSelectButton.dataset.keyname = i; + preSelectButton.className = "preSelectButton"; + preSelectButton.onclick = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false); + }; + getById("popupSelector_constraints_video").appendChild(preSelectButton); + var preSelectButton = document.createElement("button"); + preSelectButton.value = 9 / 16.0; + preSelectButton.innerText = "9:16"; + preSelectButton.className = "preSelectButton"; + preSelectButton.dataset.keyname = i; + preSelectButton.onclick = function (e) { + getById("constraints_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + getById("label_" + e.target.dataset.keyname).value = parseFloat(e.target.value); + updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false); + }; + getById("popupSelector_constraints_video").appendChild(preSelectButton); + } + + getById("popupSelector_constraints_video").appendChild(input); + } else if (typeof session.cameraConstraints[i] === "object" && session.cameraConstraints[i] !== null) { + if (i == "resizeMode") { + continue; + } + + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block;"; + label.dataset.keyname = i; + var input = document.createElement("select"); + + if (session.cameraConstraints[i].length > 1) { + var included = false; + for (var opts in session.cameraConstraints[i]) { + log(opts); + var opt = new Option(session.cameraConstraints[i][opts], session.cameraConstraints[i][opts]); + input.options.add(opt); + if (i in session.currentCameraConstraints) { + if (session.cameraConstraints[i][opts] == session.currentCameraConstraints[i]) { + opt.selected = "true"; + included = true; + } + } + } + if (!included) { + if (i in session.currentCameraConstraints) { + var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]); + input.options.add(opt); + opt.selected = "true"; + } + } + } else if (i.toLowerCase() == "torch") { + warnlog("TORCH"); + var opt = new Option("Off", false); + input.options.add(opt); + opt = new Option("On", true); + input.options.add(opt); + try { + if (session.currentCameraConstraints[i]) { + opt.selected = "selected"; + } + } catch (e) { } + } else if (session.cameraConstraints[i].length && "continuous" == session.cameraConstraints[i][0]) { + var opt = new Option("continuous", "continuous"); + input.options.add(opt); + if (i in session.currentCameraConstraints) { + if ("continuous" == session.currentCameraConstraints[i]) { + opt.selected = "true"; + var opt = new Option("manual", "manual"); + input.options.add(opt); + var opt = new Option("none", "none"); + input.options.add(opt); + } else { + var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]); + input.options.add(opt); + opt.selected = "true"; + if (session.currentCameraConstraints[i] == "none") { + var opt = new Option("manual", "manual"); + input.options.add(opt); + } else { + var opt = new Option("none", "none"); + input.options.add(opt); + } + } + } else { + opt.selected = "true"; + var opt = new Option("manual", "manual"); + input.options.add(opt); + var opt = new Option("none", "none"); + input.options.add(opt); + } + } else if (session.cameraConstraints[i].length && "manual" == session.cameraConstraints[i][0]) { + var opt = new Option("manual", "manual"); + input.options.add(opt); + if (i in session.currentCameraConstraints) { + if ("manual" == session.currentCameraConstraints[i]) { + opt.selected = "true"; + var opt = new Option("continuous", "continuous"); + input.options.add(opt); + var opt = new Option("none", "none"); + input.options.add(opt); + } else { + var opt = new Option(session.currentCameraConstraints[i], session.currentCameraConstraints[i]); + input.options.add(opt); + opt.selected = "true"; + if (session.currentCameraConstraints[i] == "none") { + var opt = new Option("continuous", "continuous"); + input.options.add(opt); + } else { + var opt = new Option("none", "none"); + input.options.add(opt); + } + } + } else { + opt.selected = "true"; + var opt = new Option("continuous", "continuous"); + input.options.add(opt); + var opt = new Option("none", "none"); + input.options.add(opt); + } + } else { + continue; + } + + if (getById("popupSelector_constraints_video").style.display == "none") { + getById("advancedOptionsCamera").style.display = "inline-flex"; + } + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; + if (CtrlPressed) { + updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false); + } else { + updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false); + } + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + getById("popupSelector_constraints_video").appendChild(div); + div.appendChild(label); + div.appendChild(input); + } else if (typeof session.cameraConstraints[i] === "boolean") { + var div = document.createElement("div"); + var label = document.createElement("label"); + label.id = "label_" + i; + label.htmlFor = "constraints_" + i; + label.innerText = capitalizeFirstLetter(i).replace(/([a-z])([A-Z])/g, "$1 $2") + ":"; + label.style = "display:inline-block;"; + label.dataset.keyname = i; + var input = document.createElement("select"); + + var opt = new Option("Off", "false"); + input.options.add(opt); + + opt = new Option("On", "true"); + input.options.add(opt); + if (session.currentCameraConstraints[i]) { + opt.selected = "true"; + } + + if (getById("popupSelector_constraints_video").style.display == "none") { + getById("advancedOptionsCamera").style.display = "inline-flex"; + } + + input.id = "constraints_" + i; + input.className = "constraintCameraInput"; + input.name = "constraints_" + i; + input.style = "display:inline; padding:2px;"; + input.dataset.keyname = i; + input.dataset.chosen = input.value; + input.onchange = function (e) { + this.dataset.chosen = this.value; + //getById("label_"+e.target.dataset.keyname).innerHTML =e.target.dataset.keyname+": "+e.target.value; + if (CtrlPressed) { + updateCameraConstraints(e.target.dataset.keyname, e.target.value, true, false, false); + } else { + updateCameraConstraints(e.target.dataset.keyname, e.target.value, false, false, false); + } + pokeIframeAPI("camera-constraint-changed", { name: e.target.dataset.keyname, value: e.target.value }); + }; + getById("popupSelector_constraints_video").appendChild(div); + div.appendChild(label); + div.appendChild(input); + } + } catch (e) { + errorlog(e); + } + } + + if (session.currentCameraConstraints.deviceId) { + if (getStorage("camera_" + session.currentCameraConstraints.deviceId)) { + var button = document.createElement("button"); + button.innerHTML = "Reset video settings to default"; + button.style.display = "block"; + button.style.padding = "10px 5px"; + button.dataset.deviceId = session.currentCameraConstraints.deviceId; + button.onclick = function () { + var deviceId = this.dataset.deviceId; + var cameraSettings = getStorage("camera_" + deviceId); + var constraints = {}; + var resetResolution = false; + var failed = false; + if (cameraSettings["default"]) { + if (cameraSettings["current"]) { + for (var i in cameraSettings["default"]) { + if (i == "groupId") { + continue; + } else if (i === "aspectRatio") { + // do not load from storage; causes issues + continue; + } else if (i === "width") { + // continue; + } else if (i === "height") { + // continue; + } else { + // if I include any of these, it will complain about mixing types and fail + if (i in cameraSettings["current"]) { + if (cameraSettings["current"][i] != cameraSettings["default"][i]) { + track0 + .applyConstraints({ + advanced: [{ [i]: cameraSettings["default"][i] }] + }) + .then(() => { }) + .catch(e => { + errorlog("Failed to reset to defaults"); + failed = true; + }); + } + } + continue; + } + if (i in cameraSettings["current"]) { + if (cameraSettings["current"][i] != cameraSettings["default"][i]) { + if (i in session.cameraConstraints) { + if ("min" in session.cameraConstraints[i]) { + if (session.cameraConstraints[i].min > cameraSettings["default"][i]) { + continue; + } + } + if ("max" in session.cameraConstraints[i]) { + if (session.cameraConstraints[i].max < cameraSettings["default"][i]) { + continue; + } + } + } + constraints[i] = cameraSettings["default"][i]; + if (i == "width" || i == "height" || i == "aspectRatio") { + resetResolution = true; + } + } + } + } + } + } + warnlog(constraints); + if (Object.keys(constraints).length) { + track0 + .applyConstraints({ + advanced: [constraints] + }) + .then(() => { + if (!failed) { + removeStorage("camera_" + deviceId); + } + listCameraSettings(); + if (resetResolution) { + session.setResolution(); // this will reset scaling for all viewers of this stream + } + }) + .catch(e => { + errorlog("Failed to reset to defaults"); + errorlog(e); + }); + } else if (!failed) { + removeStorage("camera_" + deviceId); + listCameraSettings(); + } + }; + + getById("popupSelector_constraints_video").appendChild(button); + } + } +} + +// Audio settings application +function applySavedAudioSettings(track0) { + if (!track0?.getSettings) return; + + log("applySavedAudioSettings"); + session.currentAudioConstraints = track0.getSettings(); + + const deviceId = session.currentAudioConstraints.deviceId; + if (!deviceId) return; + + const audioSettings = getStorage("audio_" + deviceId); + if (!audioSettings?.deviceId) return; + + const constraints = {}; + const allowedProps = ["autoGainControl", "echoCancellation", "noiseSuppression"]; + + for (const prop in session.currentAudioConstraints) { + if (audioSettings[prop] !== undefined && + audioSettings[prop] !== session.currentAudioConstraints[prop] && + allowedProps.includes(prop)) { + constraints[prop] = audioSettings[prop]; + warnlog("DIFF: " + prop); + } + } + + warnlog(constraints); + if (!Object.keys(constraints).length) return; + + track0.applyConstraints({ advanced: [constraints] }) + .then(() => warnlog("audio settings updated for deviceId:" + deviceId)) + .catch(e => errorlog("Failed to reset to audio defaults")); +} + +// Video settings application +function applySavedVideoSettings(track0) { + if (!track0?.getSettings) return; + + session.currentCameraConstraints = track0.getSettings(); + + // Handle mobile orientation + if (session.mobile) { + const isPortrait = (screen?.orientation?.type?.includes("portrait")) || + window.matchMedia("(orientation: portrait)").matches; + if (!isPortrait && session.currentCameraConstraints?.aspectRatio) { + session.currentCameraConstraints.aspectRatio = 1 / session.currentCameraConstraints.aspectRatio; + } + } + + const deviceId = session.currentCameraConstraints.deviceId; + if (!deviceId) return; + + const cameraSettings = getStorage("camera_" + deviceId); + if (!cameraSettings?.current) return; + + const constraints = {}; + const skipProps = ["groupId"]; + const urlOverrides = { + aspectRatio: session.forceAspectRatio, + whiteBalanceMode: session.whiteBalance, + colorTemperature: session.whiteBalance, + exposureTime: session.exposure, + exposureMode: session.exposure, + zoom: session.zoom, + saturation: session.saturation, + sharpness: session.sharpness, + contrast: session.contrast, + brightness: session.brightness + }; + + for (const prop in session.currentCameraConstraints) { + if (!cameraSettings.current[prop] || + cameraSettings.current[prop] === session.currentCameraConstraints[prop] || + skipProps.includes(prop)) continue; + + if (urlOverrides[prop]) { + log(`${prop} is manually set via URL`); + continue; + } + + constraints[prop] = cameraSettings.current[prop]; + warnlog("DIFF: " + prop); + } + + warnlog(constraints); + if (!Object.keys(constraints).length) return; + + track0.applyConstraints({ advanced: [constraints] }) + .then(() => warnlog("video settings updated for deviceId:" + deviceId)) + .catch(e => errorlog("Failed to reset to defaults")); +} + +// Camera constraints update state +var updateCameraConstraintsBusy = false; +var updateCameraConstraintsNext = false; + +// Main camera constraints update function +async function updateCameraConstraints(constraint, value = null, ctrl = false, UUID = false, save = true) { + if (constraint === "zoom" && value === 0) { + log("can't zoom to zero"); + return; + } + + log("updateCameraConstraintsBusy?"); + + if (updateCameraConstraintsBusy) { + updateCameraConstraintsNext = [constraint, value, ctrl, UUID, save]; + return; + } + + updateCameraConstraintsBusy = true; + updateCameraConstraintsNext = false; + + try { + const track0 = session.streamSrc?.getVideoTracks()?.[0]; + + if (!track0 || track0.readyState !== "live" || !track0.enabled) { + if (!save) { + errorlog("TRACK IS NOT ENABLED"); + updateCameraConstraintsBusy = false; + updateCameraConstraintsNext = false; + } + return; + } + + // Parse value + if (value == parseFloat(value)) { + value = parseFloat(value); + } else if (value === "true") { + value = true; + } else if (value === "false") { + value = false; + } + + log({ advanced: [{ [constraint]: value }] }); + + // Get current settings and prepare storage + let cameraSettings = {}; + if (track0.getSettings) { + session.currentCameraConstraints = track0.getSettings(); + + if (session.currentCameraConstraints.deviceId) { + const storageKey = "camera_" + session.currentCameraConstraints.deviceId; + const stored = getStorage(storageKey); + + if (!stored) { + cameraSettings.default = JSON.parse(JSON.stringify(session.currentCameraConstraints)); + log(cameraSettings.default); + } else { + cameraSettings = stored; + } + } + } + + // Build constraints + const constraints = await buildConstraints(constraint, value, ctrl, track0); + + // Handle mobile orientation for constraints + if (session.mobile) { + adjustConstraintsForMobileOrientation(constraints); + } + + log("20788"); + log(constraints); + + // Apply constraints + await track0.applyConstraints({ advanced: [constraints] }) + .then(() => { + log("applied constraint"); + + if (save) { + saveConstraintSettings(track0, cameraSettings, constraint, UUID); + } + + if (updateCameraConstraintsNext) { + setTimeout(() => { + updateCameraConstraintsBusy = false; + updateCameraConstraints(...updateCameraConstraintsNext); + }, 30); + } else { + updateCameraConstraintsBusy = false; + } + }) + .catch(e => { + errorlog(e.message); + errorlog("couldn't save defaults"); + window.focus(); + updateCameraConstraintsBusy = false; + updateCameraConstraintsNext = false; + }); + + } catch (e) { + errorlog(e); + updateCameraConstraintsBusy = false; + updateCameraConstraintsNext = false; + return e; + } +} + +// Helper to build constraints based on type +async function buildConstraints(constraint, value, ctrl, track0) { + const current = session.currentCameraConstraints; + let constraints = {}; + + switch (constraint) { + case "width": + constraints.width = value; + if (current?.frameRate) constraints.frameRate = current.frameRate; + if (!ctrl && current?.height) constraints.height = current.height; + break; + + case "height": + constraints.height = value; + if (current?.frameRate) constraints.frameRate = current.frameRate; + if (!ctrl && current?.width) constraints.width = current.width; + break; + + case "frameRate": + if (!ctrl) { + constraints.frameRate = value; + if (current?.height && current?.width) { + constraints.height = current.height; + constraints.width = current.width; + } + } else { + constraints.frameRate = value; + } + break; + + case "exposureMode": + if (value === "manual") { + await applyCurrentSetting(track0, "exposureTime", current); + constraints = buildManualModeConstraints(constraint, value, "exposureTime", current); + } else { + constraints[constraint] = value; + } + break; + + case "exposureTime": + constraints[constraint] = value; + constraints.exposureMode = "manual"; + break; + + case "focusMode": + if (value === "manual") { + await applyCurrentSetting(track0, "focusDistance", current); + constraints = buildManualModeConstraints(constraint, value, "focusDistance", current); + } else { + constraints[constraint] = value; + } + break; + + case "focusDistance": + constraints[constraint] = value; + constraints.focusMode = "manual"; + break; + + case "whiteBalanceMode": + if (value === "manual") { + await applyCurrentSetting(track0, "colorTemperature", current); + constraints = buildWhiteBalanceConstraints(constraint, value, current); + } else if (value === "continuous") { + constraints[constraint] = value; + if (session.mobile && ChromiumVersion) { + constraints.colorTemperature = 5000; + } + } else { + constraints[constraint] = value; + } + break; + + case "colorTemperature": + constraints[constraint] = value; + constraints.whiteBalanceMode = "manual"; + break; + + case "aspectRatio": + constraints[constraint] = value; + if (current?.frameRate) constraints.frameRate = current.frameRate; + if (session.mobile) { + const isPortrait = (screen?.orientation?.type?.includes("portrait")) || + window.matchMedia("(orientation: portrait)").matches; + if (isPortrait && constraints.aspectRatio) { + constraints.aspectRatio = 1 / constraints.aspectRatio; + } + } + break; + + default: + constraints[constraint] = value; + } + + return constraints; +} + +// Helper for manual mode constraints +function buildManualModeConstraints(constraint, value, dependentProp, current) { + const constraints = { [constraint]: value }; + + if (current?.height && current?.width) { + constraints.height = current.height; + constraints.width = current.width; + } + + if (current?.[dependentProp]) { + constraints[dependentProp] = current[dependentProp]; + } + + return constraints; +} + +// Helper for white balance constraints +function buildWhiteBalanceConstraints(constraint, value, current) { + const constraints = { [constraint]: value }; + + if (current?.height && current?.width) { + constraints.height = current.height; + constraints.width = current.width; + } + + const colorTempConstraints = session.cameraConstraints?.colorTemperature; + if (colorTempConstraints?.max && colorTempConstraints?.min) { + if (current?.colorTemperature) { + constraints.colorTemperature = current.colorTemperature; + } else if (5000 >= colorTempConstraints.min && 5000 <= colorTempConstraints.max) { + constraints.colorTemperature = 5000; + } else { + constraints.colorTemperature = colorTempConstraints.max; + } + } + + return constraints; +} + +// Helper to apply current setting +async function applyCurrentSetting(track0, prop, current) { + if (!current?.[prop]) return; + + const tempConstraints = { [prop]: current[prop] }; + await track0.applyConstraints({ advanced: [tempConstraints] }); + session.currentCameraConstraints = track0.getSettings(); +} + +// Helper to adjust constraints for mobile orientation +function adjustConstraintsForMobileOrientation(constraints) { + const isPortrait = (screen?.orientation?.type?.includes("portrait")) || + window.matchMedia("(orientation: portrait)").matches; + + if (!isPortrait) return; + + if (constraints.width && constraints.height) { + [constraints.width, constraints.height] = [constraints.height, constraints.width]; + } else if (constraints.width) { + constraints.height = constraints.width; + delete constraints.width; + if (!constraints.aspectRatio && session.currentCameraConstraints?.height) { + constraints.width = session.currentCameraConstraints.height; + } + } else if (constraints.height) { + constraints.width = constraints.height; + delete constraints.height; + if (!constraints.aspectRatio && session.currentCameraConstraints?.width) { + constraints.height = session.currentCameraConstraints.width; + } + } +} + +// Helper to save constraint settings +function saveConstraintSettings(track0, cameraSettings, constraint, UUID) { + if (!track0.getSettings || !session.currentCameraConstraints.deviceId) return; + + session.currentCameraConstraints = track0.getSettings(); + cameraSettings.current = session.currentCameraConstraints; + setStorage("camera_" + session.currentCameraConstraints.deviceId, cameraSettings); + + if (toggleSettingsState === true) { + listCameraSettings(); + } + + if (UUID) { + const data = { + UUID: UUID, + videoOptions: listVideoSettingsPrep() + }; + sendMediaDevices(data.UUID); + session.sendMessage(data, data.UUID); + } + + if (["width", "height", "aspectRatio"].includes(constraint)) { + session.setResolution(); + } +} + +function setupSharpnessTool() { + var promise; + const worker = new Worker("./thirdparty/focus_worker.js", { type: "module" }); + worker.onerror = event => { + errorlog(event); + promise.reject(event); + }; + worker.onmessage = messageEvent => { + log("Sharpness score: " + messageEvent.data.score.avg_edge_width_perc); + promise.resolve(messageEvent.data.score.avg_edge_width_perc); + }; + + measureBlur = imageData => { + worker.postMessage({ imageData }); + }; + + const canvas = document.createElement("canvas"); + // document.getElementById("header").appendChild(canvas); + + async function getSharpness(x = 50, y = 50) { + if (session.videoElement) { + log("XY"); + log(x + " : " + y); + canvas.width = session.videoElement.videoWidth / 5; + canvas.height = session.videoElement.videoHeight / 5; + + if (x < 10) { + x = 10; + } + if (y < 10) { + y = 10; + } + if (x > 90) { + x = 90; + } + if (y > 90) { + y = 90; + } + + var sx = (session.videoElement.videoWidth / 100) * (x - 10); + var sy = (session.videoElement.videoHeight / 100) * (y - 10); + var sw = session.videoElement.videoWidth * 0.2; + var sh = session.videoElement.videoHeight * 0.2; + + canvas.getContext("2d").filter = "blur(3px)"; // denoise + canvas.getContext("2d").drawImage(session.videoElement, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); // for drawing the video element on the canvas + + const canvasData = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height); + + var res, rej; + promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + promise.resolve = res; + promise.reject = rej; + + measureBlur(canvasData); + + return promise; + } + return null; + } + + return getSharpness; +} +var sharpnessToolActive = false; +var sharpnessTool = false; +async function tapToFocus(x, y, force = false) { + if (isNaN(x) || isNaN(y)) { + return; + } + + if (sharpnessToolActive) { + return; + } + + if (!session.streamSrc) { + checkBasicStreamsExist(); + return; + } + + //var bestFocus = -1; + var track0 = session.streamSrc.getVideoTracks(); + if (!track0.length) { + log("No video tracks"); + return; + } + track0 = track0[0]; + if (!track0.getCapabilities) { + log("Track lacks advanced features. Firefox?"); + return; + } + + var capabilities = track0.getCapabilities(); + if (!("focusDistance" in capabilities)) { + log("Track doesn't support focusing"); + return; + } + + var settings = track0.getSettings(); + if ("focusMode" in settings) { + if (!force && settings.focusMode !== "manual") { + log("Need to be in manual focus mode"); + return; + } + } + + if (!sharpnessTool) { + sharpnessTool = setupSharpnessTool(); + } + + var bestFocus = -1; + var bestSharpness = 999; + sharpnessToolActive = true; + + try { + log("Current focus distance: " + capabilities.focusDistance); + await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: capabilities.focusDistance.min }] }); + await sleep(250); + + var stepping = capabilities.focusDistance.step || 0.1; + + if ((capabilities.focusDistance.max - capabilities.focusDistance.min) / stepping > 100) { + stepping = parseInt((capabilities.focusDistance.max - capabilities.focusDistance.min) / 100); + } + if (!stepping) { + stepping = 0.1; + } + for (var i = capabilities.focusDistance.min; i <= capabilities.focusDistance.max; i += stepping) { + await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: i }] }); + await sleep(120); // wait long enough for a new frame and focus to adjust. + log("focus: " + i + ", " + x + "x" + y); + var response = await sharpnessTool(x, y); + if (response && response < bestSharpness) { + bestSharpness = response; + bestFocus = i; + } else if (response === null) { + return; + } + + log(response + " " + bestSharpness + " " + bestFocus + " " + i + " " + capabilities.focusDistance.max); + } + if (bestFocus !== -1) { + log("Setting focus now to: " + bestFocus); + await track0.applyConstraints({ advanced: [{ focusMode: "manual", focusDistance: bestFocus }] }); + } + } catch (e) { + errorlog(e); + } + sharpnessToolActive = false; +} + +session.remoteFocus = async function (focusDistance, absolute = false) { + try { + var track0 = session.streamSrc.getVideoTracks()[0]; + if (!track0?.getCapabilities) return; + + var capabilities = track0.getCapabilities(); + if (!capabilities.focusDistance) { + warnlog("No Focus supported on this device"); + return; + } + + const focusRange = capabilities.focusDistance; + if (!("min" in focusRange)) return; + + if (session.focusDistance === false || session.focusDistance === undefined) { + const settings = track0.getSettings(); + session.focusDistance = settings.focusDistance || focusRange.min; + } + + let newFocusDistance; + if (absolute) { + newFocusDistance = focusRange.min + focusDistance * (focusRange.max - focusRange.min); + } else { + const range = focusRange.max - focusRange.min; + const step = focusRange.step || 0.01; + const change = Math.max(Math.abs(range * focusDistance), step); + newFocusDistance = session.focusDistance + (focusDistance > 0 ? change : -change); + } + + newFocusDistance = Math.min(Math.max(newFocusDistance, focusRange.min), focusRange.max); + + const step = focusRange.step || 0.01; + const steps = Math.round((newFocusDistance - focusRange.min) / step); + newFocusDistance = focusRange.min + (steps * step); + + // Use updateCameraConstraints with save=false to avoid debouncing + await updateCameraConstraints("focusDistance", newFocusDistance, false, false, false); + session.focusDistance = newFocusDistance; + + return session.focusDistance; + } catch (e) { + errorlog(e); + return null; + } +}; + +session.setRemoteAutofocus = async function (enabled) { + try { + var mode = enabled ? "continuous" : "manual"; + await updateCameraConstraints("focusMode", mode, false, false, false); + session.focusDistance = false; // Reset stored focus distance + log("Autofocus set to: " + mode); + } catch (e) { + errorlog(e); + } +}; + +session.remoteZoom = async function (zoom, absolute = false) { + try { + var track0 = session.streamSrc.getVideoTracks()[0]; + if (!track0?.getCapabilities) return; + + var capabilities = track0.getCapabilities(); + if (!capabilities.zoom) { + warnlog("No zoom supported on this device"); + return; + } + + const zoomRange = capabilities.zoom; + if (!("min" in zoomRange) || !("max" in zoomRange) || zoomRange.max === zoomRange.min) { + warnlog("Zoom not adjustable on this device"); + return; + } + + if (session.zoom === false || session.zoom === undefined) { + const settings = track0.getSettings(); + session.zoom = settings.zoom || zoomRange.min; + } + + let newZoom; + if (absolute) { + newZoom = zoomRange.min + zoom * (zoomRange.max - zoomRange.min); + } else { + const range = zoomRange.max - zoomRange.min; + const step = zoomRange.step || 1; + const change = Math.max(Math.abs(range * zoom), step); + newZoom = session.zoom + (zoom > 0 ? change : -change); + } + + newZoom = Math.min(Math.max(newZoom, zoomRange.min), zoomRange.max); + + const step = zoomRange.step || 1; + const steps = Math.round((newZoom - zoomRange.min) / step); + newZoom = zoomRange.min + (steps * step); + + // Use updateCameraConstraints with save=false + await updateCameraConstraints("zoom", newZoom, false, false, false); + session.zoom = newZoom; + + return session.zoom; + } catch (e) { + errorlog(e); + return null; + } +}; + +session.remotePan = async function (pan, absolute = false) { + try { + var track0 = session.streamSrc.getVideoTracks()[0]; + if (!track0?.getCapabilities) return; + + var capabilities = track0.getCapabilities(); + if (!capabilities.pan) { + warnlog("No pan supported on this device"); + return; + } + + const panRange = capabilities.pan; + if (!("min" in panRange) || !("max" in panRange) || panRange.max === panRange.min) { + warnlog("Pan not adjustable on this device"); + return; + } + + if (session.pan === false || session.pan === undefined) { + const settings = track0.getSettings(); + session.pan = settings.pan || (panRange.min + panRange.max) / 2; + } + + let newPan; + if (absolute) { + const range = panRange.max - panRange.min; + newPan = panRange.min + ((pan + 1) / 2) * range; + } else { + const range = panRange.max - panRange.min; + const step = panRange.step || 1; + const change = Math.max(Math.abs(range * pan), step); + newPan = session.pan + (pan > 0 ? change : -change); + } + + newPan = Math.min(Math.max(newPan, panRange.min), panRange.max); + + const step = panRange.step || 1; + const steps = Math.round((newPan - panRange.min) / step); + newPan = panRange.min + (steps * step); + + // Use updateCameraConstraints with save=false + await updateCameraConstraints("pan", newPan, false, false, false); + session.pan = newPan; + + return session.pan; + } catch (e) { + errorlog(e); + return null; + } +}; + +session.remoteTilt = async function (tilt, absolute = false) { + try { + var track0 = session.streamSrc.getVideoTracks()[0]; + if (!track0?.getCapabilities) return; + + var capabilities = track0.getCapabilities(); + if (!capabilities.tilt) { + warnlog("No tilt supported on this device"); + return; + } + + const tiltRange = capabilities.tilt; + if (!("min" in tiltRange) || !("max" in tiltRange) || tiltRange.max === tiltRange.min) { + warnlog("Tilt not adjustable on this device"); + return; + } + + if (session.tilt === false || session.tilt === undefined) { + const settings = track0.getSettings(); + session.tilt = settings.tilt || (tiltRange.min + tiltRange.max) / 2; + } + + let newTilt; + if (absolute) { + const range = tiltRange.max - tiltRange.min; + newTilt = tiltRange.min + ((tilt + 1) / 2) * range; + } else { + const range = tiltRange.max - tiltRange.min; + const step = tiltRange.step || 1; + const change = Math.max(Math.abs(range * tilt), step); + newTilt = session.tilt + (tilt > 0 ? change : -change); + } + + newTilt = Math.min(Math.max(newTilt, tiltRange.min), tiltRange.max); + + const step = tiltRange.step || 1; + const steps = Math.round((newTilt - tiltRange.min) / step); + newTilt = tiltRange.min + (steps * step); + + // Use updateCameraConstraints with save=false + await updateCameraConstraints("tilt", newTilt, false, false, false); + session.tilt = newTilt; + + return session.tilt; + } catch (e) { + errorlog(e); + return null; + } +}; + +session.remoteExposure = async function (exposure, absolute = false) { + try { + var track0 = session.streamSrc.getVideoTracks()[0]; + if (!track0?.getCapabilities) return; + + var capabilities = track0.getCapabilities(); + var settings = track0.getSettings(); + + if (!capabilities.exposureMode || !capabilities.exposureTime) { + warnlog("Exposure control not supported on this device"); + return; + } + + // Ensure manual mode + if (settings.exposureMode !== 'manual') { + await updateCameraConstraints("exposureMode", "manual", false, false, false); + } + + const exposureRange = capabilities.exposureTime; + + if (session.exposure === false || session.exposure === undefined) { + session.exposure = settings.exposureTime || exposureRange.min; + } + + let newExposure; + if (absolute) { + newExposure = exposureRange.min + exposure * (exposureRange.max - exposureRange.min); + } else { + const range = exposureRange.max - exposureRange.min; + const step = exposureRange.step || 1; + const change = Math.max(Math.abs(range * exposure), step); + newExposure = session.exposure + (exposure > 0 ? change : -change); + } + + newExposure = Math.min(Math.max(newExposure, exposureRange.min), exposureRange.max); + + // Use updateCameraConstraints with save=false + await updateCameraConstraints("exposureTime", newExposure, false, false, false); + session.exposure = newExposure; + + log(`Applied new exposure time: ${session.exposure}`); + + return session.exposure; + } catch (e) { + errorlog(e); + return null; + } +}; + +function toggleAudioUser(ele) { + if (!ele) { + ele = ele || getById("advancedOptionsAudio"); + ele.style.display = "inline-flex"; + if (getById("popupSelector_constraints_audio").style.display == "block") { + toggleSettings(); + } else { + getById("popupSelector_constraints_audio").style.display = "block"; + ele.classList.add("highlight"); + if (!toggleSettingsState) { + toggleSettings(); + } + } + } else { + ele = ele || getById("advancedOptionsAudio"); + toggle(getById("popupSelector_constraints_audio"), false, false); + ele.classList.toggle("highlight"); + } + + getById("popupSelector_constraints_loading").style.visibility = "visible"; + getById("popupSelector_constraints_video").style.display = "none"; + getById("popupSelector_user_settings").style.display = "none"; +} +function toggleVideoUser(ele) { + if (!ele) { + ele = ele || getById("advancedOptionsCamera"); + ele.style.display = "inline-flex"; + if (getById("popupSelector_constraints_video").style.display == "block") { + toggleSettings(); + } else { + getById("popupSelector_constraints_video").style.display = "block"; + ele.classList.add("highlight"); + if (!toggleSettingsState) { + toggleSettings(); + } + } + } else { + ele = ele || getById("advancedOptionsCamera"); + toggle(getById("popupSelector_constraints_video"), false, false); + ele.classList.toggle("highlight"); + } + + getById("popupSelector_constraints_loading").style.visibility = "visible"; + getById("popupSelector_constraints_audio").style.display = "none"; + getById("popupSelector_user_settings").style.display = "none"; +} +function toggleUserUser(ele) { + ele = ele || getById("advancedOptionsGeneral"); + if (!toggleSettingsState) { + toggleSettings(); + } + ele.classList.toggle("highlight"); + toggle(getById("popupSelector_user_settings"), false, false); + getById("popupSelector_user_settings").style.visibility = "visible"; + getById("popupSelector_constraints_video").style.display = "none"; + getById("popupSelector_constraints_audio").style.display = "none"; +} + +async function requestBasicPermissions(constraint = { video: true, audio: true }, callback = setupWebcamSelection, miconly = false) { + if (session.taintedSession === null) { + log("STILL WAITING ON HASH TO VALIDATE"); + setTimeout( + function (constraint, callback, miconly) { + requestBasicPermissions(constraint, callback, miconly); + }, + 1000, + constraint, + callback, + miconly + ); + return null; + } else if (session.taintedSession === true) { + warnlog("HASH FAILED; PASSWORD NOT VALID"); + return false; + } else { + log("NOT TAINTED 1"); + } + setTimeout(function () { + getById("getPermissions").style.display = "none"; + getById("gowebcam").style.display = ""; + }, 0); + log("REQUESTING BASIC PERMISSIONS"); + + try { + + if (!navigator.mediaDevices) { + throw new Error("navigator.mediaDevices not found - check your security / browser settings."); + } + + var timerBasicCheck = null; + if (!session.cleanOutput) { + log("Setting Timer for getUserMedia"); + timerBasicCheck = setTimeout(function () { + if (!session.cleanOutput) { + if (session.mobile) { + warnUser("Notice: Camera timed out\n\nDid you accept the camera permissions?\n\nThis error may also appear if you are in a phone call or another app is already using the camera or microphone."); + } else { + warnUser("Camera Access Request Timed Out\n\nDid you accept camera permissions? Please do so first.\n\nIf you have NDI Tools installed, try uninstalling that.\n\nPlease also ensure that your camera and audio devices are correctly connected and not already in use. Bypassing USB hubs or using different USB cables can sometimes help.\n\nYou may also just need to restart the computer"); + } + } + }, 10000); + } + + let modifiedConstraint = { ...constraint }; + + try { + const videoPermission = await navigator.permissions.query({ name: "camera" }); + const audioPermission = await navigator.permissions.query({ name: "microphone" }); + + // If video is denied but audio is allowed, adjust the constraint + if (videoPermission.state === "denied" && constraint.video) { + warnlog("Video permissions are denied"); + if (constraint.audio) { + // Keep audio if it was originally requested + modifiedConstraint.video = false; + } else { + // If no audio was requested, this will likely fail + throw new Error("Video permissions denied"); + } + } + + // If audio is denied but video is allowed, adjust the constraint + if (audioPermission.state === "denied" && constraint.audio) { + warnlog("Audio permissions are denied"); + if (constraint.video) { + // Keep video if it was originally requested + modifiedConstraint.audio = false; + } else { + // If no video was requested, this will likely fail + throw new Error("Audio permissions denied"); + } + } + } catch (permissionError) { + log("Permissions API check failed:", permissionError); + } + + if (session.audioInputChannels) { + if (modifiedConstraint.audio === true) { + modifiedConstraint.audio = {}; + modifiedConstraint.audio.channelCount = session.audioInputChannels; + } else if (modifiedConstraint.audio) { + modifiedConstraint.audio.channelCount = session.audioInputChannels; + } + } + + if (session.micSampleRate) { + if (modifiedConstraint.audio === true) { + modifiedConstraint.audio = {}; + modifiedConstraint.audio.sampleRate = parseInt(session.micSampleRate); + } else if (modifiedConstraint.audio) { + modifiedConstraint.audio.sampleRate = parseInt(session.micSampleRate); + } + } + if (session.micSampleSize) { + if (modifiedConstraint.audio === true) { + modifiedConstraint.audio = {}; + modifiedConstraint.audio.sampleSize = parseInt(session.micSampleSize); + } else if (modifiedConstraint.audio) { + modifiedConstraint.audio.sampleSize = parseInt(session.micSampleSize); + } + } + + if (!modifiedConstraint.audio && !modifiedConstraint.video) { + if (miconly) { + warnUser("We couldn't find a microphone.\n\nPlease ensure you have granted the microphone permissions."); + } else { + warnUser("We couldn't find a microphone or camera.\n\nPlease ensure you have granted the microphone and camera permissions."); + } + // return null; + } + + if (session.safemode) { + if (modifiedConstraint.video) { + modifiedConstraint.video = true; + } + if (modifiedConstraint.audio) { + modifiedConstraint.audio = true; + } + } + getUserMediaRequestID += 1; + var gumID = getUserMediaRequestID; + log("CONSTRAINT"); + log(modifiedConstraint); + var timeoutStart = 0; + if (Firefox) { + timeoutStart = 500; + } + log("timeoutStart :" + timeoutStart); + setTimeout( + async function (gumID, constraint, timerBasicCheck, callback, miconly) { + log("gumID: " + gumID); + log(constraint); + var removeAudio = false; + if (!constraint.audio && !constraint.video) { + constraint.audio = true; + removeAudio = true; + } + + // Permissions API is not supported in all browsers, so we use a try-catch block + let videoPermission = "prompt"; + let audioPermission = "prompt"; + + if (Firefox && Firefox >= 132) { + console.warn("😱 see: https://bugzilla.mozilla.org/show_bug.cgi?id=1924572#c1"); + } else { + try { + const videoStatus = await navigator.permissions.query({ name: "camera" }); + videoPermission = videoStatus.state; + const audioStatus = await navigator.permissions.query({ name: "microphone" }); + audioPermission = audioStatus.state; + log("audioPermission: " + audioPermission); + } catch (e) { + warnlog("Permissions API is not fully supported in this browser."); + } + + const safariPermissionBug = SafariVersion && SafariVersion > 18 && (iOS || iPad); + + if (videoPermission === "granted" && !safariPermissionBug) { + constraint.video = false; + } + if (audioPermission === "granted" && !safariPermissionBug) { + constraint.audio = false; + } + } + + if (!constraint.audio && !constraint.video) { + warnlog("bypassing navigator.mediaDevices.getUserMedia; permissions granted already?"); + clearTimeout(timerBasicCheck); + if (getUserMediaRequestID !== gumID) { + warnlog("GET USER MEDIA CALL HAS EXPIRED 3a"); + return; + } + closeModal(); + if (callback) { + callback(miconly); + } + return; + } + + if (Firefox) { + constraint = toFirefoxConstraint(constraint); + } + + warnlog("navigator.mediaDevices.getUserMedia starting..."); + navigator.mediaDevices + .getUserMedia(constraint) + .then(function (stream) { + // Apple needs thi to happen before I can access EnumerateDevices. + if (removeAudio) { + constraint.audio = false; // this seeems pointless? + stream.getTracks().forEach(function (track) { + stream.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + } + log("got first stream"); + clearTimeout(timerBasicCheck); + if (getUserMediaRequestID !== gumID) { + warnlog("GET USER MEDIA CALL HAS EXPIRED 3"); + stream.getTracks().forEach(function (track) { + stream.removeTrack(track); + track.stop(); + log("stopping old track"); + }); + return; + } + closeModal(); + log(stream.getTracks()); + session.streamSrc = stream; + checkBasicStreamsExist(); + updateRenderOutpipe(); + if (callback) { + callback(miconly); + } + }) + .catch(function (err) { + clearTimeout(timerBasicCheck); + warnlog("some error with GetUSERMEDIA"); + console.warn(err); /* handle the error */ + if (err.name == "NotFoundError" || err.name == "DevicesNotFoundError") { + //required track is missing + } else if (err.name == "NotReadableError" || err.name == "TrackStartError") { + //webcam or mic are already in use + } else if (err.name == "OverconstrainedError" || err.name == "ConstraintNotSatisfiedError") { + //constraints can not be satisfied by avb. devices + } else if (err.name == "NotAllowedError" || err.name == "PermissionDeniedError") { + //permission denied in browser + if (isIFrame) { + console.error('Make sure that this IFRAME has the correct permissions allowed, ie:\n\niframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;geolocation;screen-wake-lock;";'); + } + if (!session.cleanOutput) { + setTimeout(function () { + if (window.obsstudio) { + warnUser("Permissions denied.\n\nTo access the camera or microphone from within OBS, please refer to:\ndocs.vdo.ninja/guides/share-webcam-from-inside-obs.", false, false); + } else if (ChromiumVersion && !session.mobile) { + warnUser("

    Camera/mic permissions denied

    \nPlease ensure you have allowed the mic/camera permissions in your browser, such as like:\n\n\n\nFor further help on how to resolve this issue, please refer to:\n\nhttps://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions.", false, false); + } else if (Firefox && session.mobile) { + warnUser( + "

    Camera/mic permission denied

    \nPlease allow mic/camera access.\n\n\ + If not prompted, go to Settings -> Site permissions -> exceptions (at bottom) -> vdo.ninja, and then manually enable the permissions.\n\n\ + If Firefox still gives you issues, try in incognito mode or a different browser.\ + For further help, please refer to:\n\nhttps://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions.", + false, + false + ); + } else { + warnUser("Permission access to the camera or microphone was denied.\n\nPlease ensure you have allowed the mic/camera permissions in your browser.\n\nFor guides on how to resolve this issue, please refer to:\n\nhttps://docs.vdo.ninja/common-errors-and-known-issues/enable-camera-microphone-permissions.", false, false); + } + }, 1); + } + return; + } else if (err.name == "TypeError" || err.name == "TypeError") { + //empty constraints object + } else { + //permission denied in browser + if (!session.cleanOutput) { + setTimeout( + function (err) { + warnUser(err); + }, + 1, + err + ); + } + } + warnlog("trying to list webcam again"); + if (callback) { + callback(miconly); + } + }); + }, + timeoutStart, + gumID, + modifiedConstraint, + timerBasicCheck, + callback, + miconly + ); + } catch (e) { + console.warn(e); + if (!session.cleanOutput) { + if (window.isSecureContext) { + warnUser("An error has occured when trying to access the webcam or microphone. The reason is not known."); + } else if (iOS || iPad) { + warnUser("iOS version 13.4 and up is generally recommended; older than iOS 11 is not supported."); + } else { + warnUser("Error acessing camera or microphone.\n\nThe website may be loaded in an insecure context.\n\nPlease see: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia"); + } + } + } + return null; +} + +function awaitInboundCall() { + if (session.twilio) { + return; + } + loadScript("./thirdparty/twilio.min.js?v1", async function () { + if (!session.twilio) { + session.twilio = {}; + } + + class MyAudioProcessor { + constructor(audioContext) { + console.log("constructor"); + this.audioContext = session.audioCtxOutbound; + session.twilio.mixer = session.audioCtxOutbound.createGain(); // This serves as our mixer node + this.destination = session.twilio.destination = session.audioCtxOutbound.createMediaStreamDestination(); // Destination node + this.background = this.audioContext.createMediaElementSource(document.createElement("video")); + } + + async createProcessedStream(stream) { + const source = this.audioContext.createMediaStreamSource(stream); + const gain = this.audioContext.createGain(); + gain.gain.value = 0; + source.connect(gain); + gain.connect(session.twilio.mixer); + // session.twilio.micSources.push(stream); + + session.twilio.mixer.connect(this.destination); + return this.destination.stream; + } + + async destroyProcessedStream(stream) { + console.log("destroyProcessedStream called"); + console.log(stream); + } + } + + const response = await fetch("https://call.vdo.ninja:8443/token2"); + session.twilio.data = await response.json(); + + session.twilio.device = new Twilio.Device(session.twilio.data.token); + + const audioProcessor = new MyAudioProcessor(); + await session.twilio.device.audio.addProcessor(audioProcessor); + + session.twilio.device.register(); + + warnUser("Dial in access code: " + session.twilio.data.dialInNumber); + + session.twilio.device.on("registered", function () { + console.log("Twilio.Device is ready to receive incoming calls!"); + }); + + async function refresh() { + console.log("refreshing token"); + const response = await fetch("https://call.vdo.ninja:8443/refresh"); + session.twilio.data = await response.json(); + session.twilio.device.updateToken(session.twilio.data.token); + } + + session.twilio.refreshInterval = setInterval(refresh, 50 * 60 * 1000); + + session.twilio.micSources = []; + + session.twilio.updateMixer = async function (UUID = false) { + try { + console.log("update mixer"); + session.twilio.micSources.forEach(src => { + src.disconnect(); + }); + + if (session.videoElement && session.videoElement.srcObject && session.videoElement.srcObject.getAudioTracks().length) { + var micSource = session.audioCtxOutbound.createMediaStreamSource(session.videoElement.srcObject); + } else if (session.streamSrc && session.streamSrc.getAudioTracks().length) { + var micSource = session.audioCtxOutbound.createMediaStreamSource(session.streamSrc); + } else { + var micSource = false; + } + + for (var uuid in session.rpcs) { + try { + if (session.rpcs[uuid].videoElement && session.rpcs[uuid].videoElement.srcObject) { + var guestSource = session.audioCtxOutbound.createMediaStreamSource(session.rpcs[uuid].videoElement.srcObject); + if (guestSource) { + guestSource.connect(session.twilio.mixer); + session.twilio.micSources.push(guestSource); + console.log(uuid + " added to twiliio"); + } + } + } catch (e) { + errorlog(e); + } + } + + if (micSource) { + micSource.connect(session.twilio.mixer); + session.twilio.micSources.push(micSource); + console.log("micSource added to twiliio"); + } + } catch (err) { + console.error("Could not get microphone audio:", err); + } + }; + + session.twilio.device.on("incoming", call => { + session.twilio.call = call; + console.log("Incoming call from: " + session.twilio.call.parameters.From); + + session.twilio.call.on("reject", () => { + console.log("rejected"); + }); + session.twilio.call.on("disconnect", () => { + console.log("disconnected"); + }); + session.twilio.call.on("cancel", () => { + console.log("cancelled"); + }); + session.twilio.call.on("accept", () => { + console.log("accpted"); + }); + session.twilio.call.on("audio", e => { + session.twilio.element = e; + console.log("audio stream available"); + outboundAudioPipeline(session.streamSrc); + }); + + window.focus(); + confirmAlt("Incoming call from: " + session.twilio.call.parameters.From + "\n" + getTranslation("accept-inbound-caller")).then(res => { + if (res) { + if (session.audioCtxOutbound.state === "suspended") { + session.audioCtxOutbound.resume().then(() => { + console.log("AudioContext resumed"); + }); + } + session.twilio.updateMixer(); + session.twilio.call.accept(); + } else { + session.twilio.call.reject(); + } + }); + }); + }); +} + +function joinConference(roomid, mute = true) { + // not used + loadScript("./thirdparty/twilio.min.js", function () { + fetch("https://call.vdo.ninja:8443/token") + .then(response => response.json()) + .then(async data => { + const device = new Twilio.Device(data.token); + call = await device.connect({ params: { To: roomid } }); + if (mute) { + const checkForRemoteStream = setInterval(() => { + if (call.getRemoteStream && call.getRemoteStream()) { + if (call.getRemoteStream().getTracks && call.getRemoteStream().getTracks().length) { + clearInterval(checkForRemoteStream); + call.getRemoteStream().getTracks()[0].enabled = false; + console.log("call muted"); + } + } + }, 500); + } + }) + .catch(error => { + console.error("Error fetching token: ", error); + }); + }); +} + +function listenWebsocket(roomid) { + // not used + var callSocket = null; + var connecting = false; + var streams = {}; + function connectAudioInbound() { + clearTimeout(connecting); + if (callSocket) { + if (callSocket.readyState === callSocket.OPEN) { + return; + } + try { + callSocket.close(); + } catch (e) { } + } + callSocket = new WebSocket("wss://call.vdo.ninja:8443/" + roomid); + callSocket.onclose = function () { + clearTimeout(connecting); + connecting = setTimeout(function () { + connectAudioInbound(); + }, 100); + }; + callSocket.onopen = function () { + send({ join: true }); + }; + callSocket.addEventListener("message", function (event) { + var msg = JSON.parse(event.data); + if (msg.event && msg.event === "media" && msg.media && msg.media.payload && msg.streamSid) { + playAudioChunk(msg.media.payload, msg.streamSid); + if (!streams[msg.streamSid]) { + console.log("stream starting"); + streams[msg.streamSid] = true; + //audioStreams.innerHTML = ""; + //for (stream in streams){ + // audioStreams.innerHTML += stream+"
    "; + //} + console.log(streams); + } + } else if (msg.event && msg.event === "stop" && msg.streamSid) { + console.log("stream stopping"); + delete streams[msg.streamSid]; + //audioStreams.innerHTML = ""; + //for (stream in streams){ + // audioStreams.innerHTML += stream+"
    "; + //} + console.log(streams); + } // else { + // console.log(msg); + // console.log("stream misc"); + // console.log(streams); + //} + }); + } + + function base64ToUint8Array(base64) { + const binaryString = window.atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + function muLawToPCM(muLawData, gainFactor = 2.0, threshold = 30000, ratio = 2.0) { + const pcmData = new Int16Array(muLawData.length); + for (let i = 0; i < muLawData.length; i++) { + let muLawByte = muLawData[i]; + muLawByte = ~muLawByte; + let sign = muLawByte & 0x80; + muLawByte &= ~0x80; + let exponent = (muLawByte >> 4) & 0x07; + let data = muLawByte & 0x0f; + data |= 0x10; + data <<= 1; + data += 1; + data <<= exponent + 2; + data -= 0x84; + if (sign === 0) data = -data; + let amplifiedData = data * gainFactor; + if (Math.abs(amplifiedData) > threshold) { + const overThreshold = Math.abs(amplifiedData) - threshold; + const compressedData = threshold + overThreshold / ratio; + amplifiedData = amplifiedData > 0 ? compressedData : -compressedData; + } + pcmData[i] = amplifiedData; + } + return pcmData; + } + function playAudioChunk(base64Chunk, sid) { + const muLawData = base64ToUint8Array(base64Chunk); + const pcmData = muLawToPCM(muLawData); + const audioBuffer = session.audioCtx.createBuffer(1, pcmData.length, 8000); + const bufferSource = session.audioCtx.createBufferSource(); + const channelData = audioBuffer.getChannelData(0); + for (let i = 0; i < pcmData.length; i++) { + channelData[i] = pcmData[i] / 32768.0; + } + bufferSource.buffer = audioBuffer; + bufferSource.connect(session.audioCtx.destination); + bufferSource.start(); + } + function send(cmd) { + callSocket.send(JSON.stringify(cmd)); + } + connectAudioInbound(); +} + +function setupWebcamSelection(miconly = false) { + log("setupWebcamSelection();"); + + checkBasicStreamsExist(); + + try { + return enumerateDevices() + .then(function (dInfo) { + return gotDevices(dInfo, miconly); + }) + .then(function () { + if (getById("webcamquality").elements && parseInt(getById("webcamquality").elements.namedItem("resolution").value) == 3) { + // this is junk?? + if (session.maxframeRate === false) { + session.maxframeRate = 30; + session.maxframeRate_q2 = true; + } + } else if (session.maxframeRate_q2) { + session.maxframeRate = false; + session.maxframeRate_q2 = false; + } + + var audioSelect = getById("audioSource"); + var videoSelect = getById("videoSourceSelect"); + var outputSelect = getById("outputSource"); + + if (audioSelect.tagName == "UL") { + audioSelect.onchange = function () { + if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").disabled = true; + document.getElementById("gowebcam").dataset.audioready = "false"; + //document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD"; + document.getElementById("gowebcam").style.fontWeight = "normal"; + document.getElementById("gowebcam").innerHTML = "Waiting for mic to load"; + miniTranslate(document.getElementById("gowebcam"), "waiting-for-mic-to-load"); + } + activatedPreview = false; + grabAudio(); + }; + } + videoSelect.onchange = function () { + if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").disabled = true; + document.getElementById("gowebcam").dataset.ready = "false"; + //document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD"; + document.getElementById("gowebcam").style.fontWeight = "normal"; + document.getElementById("gowebcam").innerHTML = "Waiting for Camera to load"; + miniTranslate(document.getElementById("gowebcam"), "waiting-for-camera-to-load"); + } + warnlog("video source changed"); + + activatedPreview = false; + if (session.quality !== false) { + grabVideo(session.quality); + } else if (document.getElementById("webcamquality")) { + session.quality_wb = parseInt(document.getElementById("webcamquality").elements.namedItem("resolution").value); + session.quality_room = session.quality_wb; + grabVideo(session.quality_wb); + } + }; + + if (Firefox && !session.mobile) { + outputSelect.onclick = function () { + log("on click"); + if (outputSelect.options[outputSelect.selectedIndex].value === "others") { + navigator.mediaDevices.selectAudioOutput().then(device => { + if (device.kind == "audiooutput") { + session.sink = device.deviceId; + try { + var matched = false; + outputSelect.childNodes.forEach(ele => { + if (ele.value === device.deviceId) { + matched = true; + ele.selected = true; + } + }); + if (!matched) { + var option = document.createElement("option"); + option.value = device.deviceId; + option.text = device.label; + outputSelect.appendChild(option); + option.selected = true; + } + saveSettings(); // we're saving because there was an explicit action to change devices + } catch (e) { + errorlog(e); + } + if (!session.sink) { + return; + } // Not sure this would ever happen, but whatever. + resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist. + } + }); + } + }; + } + + outputSelect.onchange = function () { + if (iOS || iPad) { + return; + } + if (Firefox && !session.mobile) { + if (outputSelect.options[outputSelect.selectedIndex].value === "others") { + return; + } + } + + try { + session.sink = outputSelect.options[outputSelect.selectedIndex].value; + saveSettings(); // we're saving because there was an explicit action to change devices + } catch (e) { + errorlog(e); + } + + if (!session.sink) { + return; + } // Not sure this would ever happen, but whatever. + + resetupAudioOut(); // we'll probalby use session.sink, since outputSelect3 doesn't exist. + }; + + getById("webcamquality").onchange = function () { + if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").disabled = true; + document.getElementById("gowebcam").dataset.ready = "false"; + // document.getElementById("gowebcam").style.backgroundColor = "#DDDDDD"; + document.getElementById("gowebcam").style.fontWeight = "normal"; + document.getElementById("gowebcam").innerHTML = "Waiting for Camera to load"; + miniTranslate(document.getElementById("gowebcam"), "waiting-for-camera-to-load"); + } + + if (parseInt(getById("webcamquality").elements.namedItem("resolution").value) == 2) { + if (session.maxframeRate === false) { + session.maxframeRate = 30; + session.maxframeRate_q2 = true; + } + } else if (session.maxframeRate_q2) { + session.maxframeRate = false; + session.maxframeRate_q2 = false; + } + + activatedPreview = false; + session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); + session.quality_room = session.quality_wb; + grabVideo(session.quality_wb); + }; + + if (session.safemode) { + if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").disabled = false; + //document.getElementById("gowebcam").innerHTML = getTranslation("start"); + miniTranslate(document.getElementById("gowebcam"), "start"); + document.getElementById("gowebcam").dataset.audioready = "true"; + document.getElementById("gowebcam").dataset.ready = "true"; + document.getElementById("gowebcam").focus(); + setTimeout(function () { + updateForceRotate(); + }, 1000); + if (session.autostart) { + publishWebcam(); // no need to mirror as there is no video... + } + return; + } + } + + if (session.audioDevice !== 0) { + // change from Auto to Selected Audio Device + log("SETTING AUDIO DEVICE!!"); + activatedPreview = false; + grabAudio(); + } else if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").dataset.audioready = "true"; + } + + if (session.videoDevice === 0 || miconly) { + if (session.autostart) { + publishWebcam(); // no need to mirror as there is no video... + return; + } else if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").dataset.ready = "true"; + if (document.getElementById("gowebcam").dataset.audioready == "true") { + document.getElementById("gowebcam").disabled = false; + miniTranslate(document.getElementById("gowebcam"), "start"); + document.getElementById("gowebcam").focus(); + //document.getElementById("gowebcam").innerHTML = getTranslation("start"); + } + } + } else { + log("GRabbing video: " + session.quality); + activatedPreview = false; + if (session.quality !== false) { + grabVideo(session.quality); + } else if (document.getElementById("webcamquality")) { + session.quality_wb = parseInt(getById("webcamquality").elements.namedItem("resolution").value); + session.quality_room = session.quality_wb; + grabVideo(session.quality_wb); + } + } + + if (!(iOS || iPad || session.mobile)) { + try { + if (outputSelect.selectedIndex >= 0) { + session.sink = outputSelect.options[outputSelect.selectedIndex].value; + saveSettings(); + if (session.videoElement && !session.videoElement.paused) { + resetupAudioOut(); + } + } + } catch (e) { + errorlog(e); + } + } + }) + .catch(e => { + errorlog(e); + }); + } catch (e) { + errorlog(e); + } +} + +Promise.wait = function (ms) { + return new Promise(function (resolve) { + setTimeout(resolve, ms); + }); +}; + +Promise.prototype.timeout = function (ms) { + return Promise.race([ + this, + Promise.wait(ms).then(function () { + if (iOS || iPad) { + var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nIf using an iPhone or iPad, try fully closing your browser and open it again; Safari sometimes jams up the camera."); + errormsg.name = "timedOut"; + errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nIf using an iPhone or iPad, try fully closing your browser and open it again; Safari sometimes jams up the camera."; + throw errormsg; + } else if (session.mobile) { + var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nMake sure no other application is using the camera already and that you are using a compatible browser. If issues persist, maybe try the official native mobile app."); + errormsg.name = "timedOut"; + errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nMake sure no other application is using the camera already and that you are using a compatible browser. If issues persist, maybe try the official native mobile app."; + throw errormsg; + } else { + var errormsg = new Error("Time Out\nDid you accept camera permissions in time? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling it.\n\nPlease also ensure your camera and audio device are correctly connected and not already in use. You may also need to refresh the page."); + errormsg.name = "timedOut"; + errormsg.message = "Time Out\nDid you accept camera permissions in time? Please do so first.\n\nOtherwise, do you have NDI Tools installed? Maybe try uninstalling it.\n\nPlease also ensure your camera and audio device are correctly connected and not already in use. You may also need to refresh the page."; + throw errormsg; + } + }) + ]); +}; + +async function shareWebsite(autostart = false, evt = false) { + if (session.iframeSrc) { + if (!session.cleanOutput) { + getById("websitesharebutton2").classList.remove("hidden"); + } + + if (evt && (evt.ctrlKey || evt.metaKey)) { + if (getById("websitesharebutton2").classList.contains("green")) { + var actionMsg = {}; + actionMsg.infocus = false; + + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowIframe === true) { + session.sendMessage(actionMsg, UUID); + } + } + + getById("websitesharebutton2").classList.remove("green"); + getById("websitesharebutton2").ariaPressed = "false"; + getById("websitesharebutton2").title = "Hold CTRL (or CMD) and click to spotlight this shared website"; + } else { + if (session.streamID) { + var actionMsg = {}; + actionMsg.infocus = session.streamID; + + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowIframe === true) { + session.sendMessage(actionMsg, UUID); + } + } + + getById("websitesharebutton2").classList.add("green"); + getById("websitesharebutton2").ariaPressed = "true"; + getById("websitesharebutton2").title = "Video is currently spotlighted"; + } + } + return; + } + getById("websitesharebutton2").classList.remove("green"); + getById("websitesharebutton2").ariaPressed = "false"; + session.iframeSrc = false; + + if (session.director) { + clearDirectorSettings(); + //setStorage("directorWebsiteShare", {"website":session.iframeSrc, "roomid":session.roomid}); + } else if (document.getElementById("container_iframe") || session.iframeEle) { + if (session.iframeEle) { + session.iframeEle.remove(); + session.iframeEle = false; + } + if (document.getElementById("container_iframe")) { + document.getElementById("container_iframe").remove(); + } + + updateMixer(); + } + getById("websitesharebutton2").classList.add("hidden"); + getById("websitesharebutton").classList.remove("hidden"); + + var data = {}; + data.iframeSrc = false; + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowIframe === true) { + session.sendMessage(data, UUID); + } + } + getById("websitesharebutton2").title = "Hold CTRL (or CMD) and click to spotlight this shared website"; + return; + } + getById("websitesharebutton2").classList.remove("green"); + getById("websitesharebutton2").ariaPressed = "false"; + + if (autostart === false) { + window.focus(); + var iframeURL = await promptAlt(getTranslation("enter-website"), false, false, session.defaultIframeSrc); + } else { + var iframeURL = autostart; + } + if (!iframeURL) { + return; + } + if (iframeURL == session.iframeSrc) { + return; + } + session.defaultIframeSrc = iframeURL; + + warnlog(iframeURL); + + session.iframeSrc = parseURL4Iframe(iframeURL); + + if (session.director && !autostart) { + setStorage("directorWebsiteShare", { website: session.iframeSrc, roomid: session.roomid }); + } else if (session.iframeEle) { + session.iframeEle.src = session.iframeSrc; + if (session.iframeSrc.startsWith("https://www.youtube.com/")) { + // special handler. + setTimeout( + function (iframe_id) { + YoutubeListen(iframe_id); + }, + 1000, + iframe.id + ); + } + } else if (session.iFramesAllowed) { + + var iframe = document.createElement("iframe"); + iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location + iframe.src = session.iframeSrc; + iframe.id = "iframe_source"; + iframe.setAttribute("allowtransparency", "true"); + iframe.setAttribute("crossorigin", "anonymous"); + iframe.setAttribute("credentialless", "true"); + iframe.loadedYoutubeListen = false; + session.iframeEle = iframe; + + var container = document.createElement("div"); + iframe.container = container; + container.id = "container_iframe"; + + if (session.iframeSrc.startsWith("https://www.youtube.com/")) { + // special handler. + setTimeout( + function (iframe_id) { + YoutubeListen(iframe_id); + }, + 1000, + iframe.id + ); + } + updateMixer(); + } + + getById("websitesharebutton2").classList.remove("hidden"); + getById("websitesharebutton").classList.add("hidden"); + + var data = {}; + data.iframeSrc = session.iframeSrc; + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowIframe === true) { + session.sendMessage(data, UUID); + } + } +} + +function screenshareTypeDecider(sstype = 1) { + if (session.screenshareType) { + sstype = session.screenshareType; + } + if (sstype == 1) { + toggleScreenShare(); + } else if (sstype == 2) { + createIframePopup(); + } else if (sstype == 3) { + createSecondStream(); + } +} + +function createScreenShareURL(transparent = true) { + // iframe.src = + if (session.screenShareElement) { + var iFrameID = session.streamID.substring(0, 12) + "_" + session.generateStreamID(5); + } else if (session.screenshareid) { + var iFrameID = session.screenshareid; + } else { + var iFrameID = session.streamID.substring(0, 12) + "_" + session.generateStreamID(5); + } + + if (session.exclude) { + session.exclude.push(iFrameID); + } else { + session.exclude = []; + session.exclude.push(iFrameID); + } + + var extras = ""; + if (session.password) { + extras += "&password=" + session.password; // encodeURIComponent( + } + + if (session.privacy) { + extras += "&privacy"; + } + + if (session.meshcast) { + extras += "&meshcast"; + } + + if (session.token) { + extras += "&token=" + session.token; + } + + if (session.remote) { + if (session.remote === true) { + extras += "&remote"; + } else { + extras += "&remote=" + session.remote; + } + } + if (session.salt !== location.hostname) { + // this is default. + extras += "&salt=" + session.salt; + } + if (session.whipOutVideoBitrate) { + extras += "&wovb=" + session.whipOutVideoBitrate; + } + if (session.whipOutScreenShareBitrate) { + extras += "&wossbitrate=" + session.whipOutScreenShareBitrate; + } + + if (!session.notifyScreenShare) { + extras += "&smallshare"; + } + + if (session.screenshareContentHint) { + extras += "&sshint=" + session.screenshareContentHint; + } else if (session.contentHint) { + extras += "&sshint=" + session.contentHint; + } + + if (session.audioContentHint) { + extras += "&audiohint=" + session.audioContentHint; + } + + if (session.whipOutScreenShareCodec) { + extras += "&whipoutcodec=" + session.whipOutScreenShareCodec; + } else if (session.whipOutCodec) { + extras += "&whipoutcodec=" + session.whipOutCodec; + } + + if (session.screensharequality !== false) { + extras += "&q=" + session.screensharequality; // &quality works here, since only thing we are doing + } else if (session.quality !== false) { + extras += "&q=" + session.quality; + } else if (session.quality_ss !== false) { + extras += "&q=" + session.quality_ss; + } else { + extras += "&q=0"; + } + + if (session.screenShareLabel !== false) { + if (session.screenShareLabel) { + extras += "&label=" + encodeURIComponent(session.screenShareLabel); + } else if (session.label) { + extras += "&label=" + encodeURIComponent(session.label); + } + } + + if (session.screensharefps !== false) { + extras += "&maxframeRate=" + parseInt(session.screensharefps * 100) / 100.0; + } + if (session.screenshareAEC) { + extras += "&aec=1"; + } + if (session.screenshareDenoise) { + extras += "&denoise=1"; + } + if (session.screenshareAutogain) { + extras += "&autogain=1"; + } + if (session.screenshareStereo !== false) { + extras += "&stereo=" + session.screenshareStereo; + } + if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) { + extras += "&ar=" + session.forceAspectRatio; + } else if (session.forceScreenShareAspectRatio) { + extras += "&ar=" + session.forceScreenShareAspectRatio; + } + if (session.muted) { + extras += "&muted"; + } + + if (session.recordLocal) { + extras += "&record=" + session.recordLocal; + } + + if (session.autorecordlocal) { + extras += "&autorecordlocal"; + } + + if (transparent) { + extras += "&transparent&cleanish"; + } + // manual, since I don't want to use the auto-mixer. + return "?manual&audiodevice=1&screenshare&cb=0&nvb&nsb&autostart&view&room=" + session.roomid + "&push=" + iFrameID + extras; +} + +function createIframePopup() { + if (session.screenShareElement) { + postMessageIframe(session.screenShareElement, { close: true }); + session.screenShareElement.parentNode.removeChild(session.screenShareElement); + session.screenShareElement = false; + updateMixer(); + getById("screenshare2button").classList.remove("green"); + getById("screenshare2button").ariaPressed = "false"; + return; + } + + if ((session.queue && !session.transferred) || (session.screenShareState && !session.queue && session.transferred)) { + // if (session.queue || session.transferred){ + //getById("screenshare2button").classList.add("hidden"); + //getById("screensharebutton").classList.remove("hidden"); + toggleScreenShare(); + return; + } // can't secondary-screen share if in a queue. + + //if (!session.iFramesAllowed){errorlog("Can't create iFRAME - security is tainted due to possible CSS injection");return;} // allow because we are doing &sstype=2; not anything else. + var iframe = document.createElement("iframe"); + iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;midi;screen-wake-lock;"; // do not allow location + iframe.src = "./" + createScreenShareURL(); + iframe.setAttribute("allowtransparency", "true"); + iframe.setAttribute("crossorigin", "anonymous"); + iframe.setAttribute("credentialless", "true"); + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.overflow = "hidden"; + iframe.id = "screensharesource"; + iframe.dataset.sid = "#screensharesource"; + iframe.style.zIndex = "0"; + + session.screenShareElement = iframe; + session.screenShareElement.dataset.doNotMove = true; + + document.getElementById("main").appendChild(iframe); + + if (session.screenShareElementHidden) { + session.screenShareElement.style.display = "none"; + } + + updateMixer(); + + getById("screenshare2button").classList.add("green"); + getById("screenshare2button").ariaPressed = "true"; + + return; // ignore the rest. +} + +function previewWebcam(miconly = false) { + if (session.taintedSession === null) { + log("STILL WAITING ON HASH TO VALIDATE"); + setTimeout( + function (miconly) { + previewWebcam(miconly); + }, + 1000, + miconly + ); + return; + } else if (session.taintedSession === true) { + warnlog("HASH FAILED; PASSWORD NOT VALID"); + return; + } else { + log("NOT TAINTED"); + } + + if (activatedPreview == true) { + log("activeated preview return 1"); + return; + } + activatedPreview = true; + + if (miconly) { + // this just shares the preview section with the mic-only and video+mic modes + if (!getById("add_camera_inner").cloned) { + getById("add_camera_inner").cloned = true; + insertAfter(getById("add_camera_inner"), getById("add_microphone")); + document.getElementById("videoSourceSelect").innerHTML = ""; + } + } else if (getById("add_camera_inner").cloned) { + getById("add_camera_inner").cloned = false; + insertAfter(getById("add_camera_inner"), getById("add_camera")); + document.getElementById("videoSourceSelect").innerHTML = ""; + } + + if (session.audioDevice === 0) { + // OFF + var constraint = { + audio: false + }; + } else if ((session.echoCancellation !== false) && (session.autoGainControl !== false) && (session.noiseSuppression !== false) && (session.voiceIsolation !== true)) { + // AUTO + var constraint = { + audio: true + }; + } else { + // Disable Echo Cancellation and stuff for the PREVIEW (DEFAULT CAM/MIC) + var constraint = { + audio: {} + }; + if (session.echoCancellation !== false) { + // if not disabled, we assume it's on + constraint.audio.echoCancellation = true; + } else { + constraint.audio.echoCancellation = false; + if (!session.cleanoutput) { + getById("headphoneTip1").classList.remove("hidden"); + miniTranslate(getById("headphoneTipContext1"), "headphones-tip"); + //getById("headphoneTipContext1").innerHTML = getTranslation("headphones-tip"); + } + } + if (session.autoGainControl !== false) { + constraint.audio.autoGainControl = true; + } else { + constraint.audio.autoGainControl = false; + } + if (session.noiseSuppression !== false) { + constraint.audio.noiseSuppression = true; + } else { + constraint.audio.noiseSuppression = false; + } + + if (session.voiceIsolation === true) { + constraint.audio.voiceIsolation = true; + } + } + + if (session.videoDevice === 0 || miconly) { + constraint.video = false; + } else { + constraint.video = true; + } + + window.onorientationchange = function () { + if (Firefox) { + updateForceRotate(true); + } + setTimeout(async function () { + if (session.forceAspectRatio) { + await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + } + if (session.effect && session.effect === "7") { + digitalZoom(); + } + updateForceRotate(); + }, 200); + }; + + if (constraint.video === false && constraint.audio === false) { + if (session.autostart) { + publishWebcam(false, miconly); // no need to mirror as there is no video... + return; + } else { + getById("getPermissions").style.display = "none"; + if (document.getElementById("gowebcam")) { + document.getElementById("gowebcam").dataset.ready = "true"; + document.getElementById("gowebcam").dataset.audioready = "true"; + document.getElementById("gowebcam").disabled = false; + //document.getElementById("gowebcam").innerHTML = getTranslation("start"); + miniTranslate(document.getElementById("gowebcam"), "start"); + document.getElementById("gowebcam").focus(); + } + } + return; + } + + enumerateDevices() + .then(function (devices) { + log("enumeratated"); + log(devices); + var vtrue = false; + var atrue = false; + devices.forEach(function (device) { + if (device.kind === "audioinput") { + atrue = true; + } else if (device.kind === "videoinput") { + vtrue = true; + } + }); + if (atrue === false) { + constraint.audio = false; + } + if (vtrue === false) { + constraint.video = false; + } + setTimeout( + function (constraint, miconly) { + requestBasicPermissions(constraint, setupWebcamSelection, miconly); + }, + 0, + constraint, + miconly + ); + }) + .catch(error => { + log("enumeratated failed. Seeking permissions."); + setTimeout( + function (constraint, miconly) { + requestBasicPermissions(constraint, setupWebcamSelection, miconly); + }, + 0, + constraint, + miconly + ); + }); +} + +function copyFunction(copyText, evt = false) { + if (evt) { + if ("buttons" in evt) { + if (evt.buttons !== 0) { + return; + } + } else if ("which" in evt) { + if (evt.which !== 0) { + return; + } + } + popupMessage(evt); + evt.preventDefault(); + evt.stopPropagation(); + } + + try { + copyText.select(); + copyText.setSelectionRange(0, 99999); + document.execCommand("copy"); + } catch (e) { + var dummy = document.createElement("input"); + document.body.appendChild(dummy); + dummy.value = copyText; + dummy.select(); + document.execCommand("copy"); + document.body.removeChild(dummy); + } + return false; +} + +function generateQRPage() { + var pass = sanitizePassword(getById("invite_password").value); + if (pass.length) { + return generateHash(pass + session.salt, 4) + .then(function (hash) { + generateQRPageCallback(hash); + }) + .catch(errorlog); + } else { + generateQRPageCallback(""); + } +} + +async function updateLinkWelcome(arg, input) { + if (input.checked) { + var response = await promptAlt("Enter the message you'd like the guests to see"); + response = encodeURIComponent(response); + var param = input.dataset.param.split("=")[0]; + input.dataset.param = param + "=" + response; + } + updateLink(arg, input); +} + +function updateLinkWebP(arg, input) { + if (input.checked) { + if (!(getById("director_block_" + arg).dataset.raw.includes("&broadcast") || getById("director_block_" + arg).dataset.raw.includes("?broadcast"))) { + getById("broadcastSlider").checked = true; + updateLink(arg, getById("broadcastSlider")); + } + } + updateLink(arg, input); +} + +var soloLinkAppended = ""; +function updateLink(arg, input, solo = false) { + log("updateLink: " + input.dataset.param); + if (input.checked) { + getById("director_block_" + arg).dataset.raw += input.dataset.param; + + if (solo) { + soloLinkAppended += input.dataset.param; + } + + var string = getById("director_block_" + arg).dataset.raw; + + if (arg == 1 && getById("obfuscate_director_" + arg).checked) { + string = obfuscateURL(string); + } + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; + } else { + var string = getById("director_block_" + arg).dataset.raw + "&"; + string = string.replace(input.dataset.param + "&", "&"); + string = string.substring(0, string.length - 1); + getById("director_block_" + arg).dataset.raw = string; + + if (solo) { + soloLinkAppended += "&"; + soloLinkAppended = soloLinkAppended.replace(input.dataset.param + "&", "&"); + soloLinkAppended = soloLinkAppended.substring(0, soloLinkAppended.length - 1); + } + + if (arg == 1 && getById("obfuscate_director_" + arg).checked) { + string = obfuscateURL(string); + } + + // document.querySelector("soloLink") + // soloLink + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; + } + if (solo) { + document.querySelectorAll("a.soloLink").forEach(ele => { + try { + var href = ele.getAttribute("value") + soloLinkAppended; + ele.href = href; + ele.innerHTML = href; + } catch (e) { + errorlog(e); + } + }); + } + + // Update all solo links with universal token if in auth mode + if (session.authMode && session.universalViewToken) { + updateAllSoloLinks(); + } + + saveDirectorSettings(); +} + +function changeURL(changeURL) { + window.focus(); + if (session.consent) { + hangup(false); + window.location.href = changeURL; + } else { + confirmAlt(getTranslation("director-redirect-1") + changeURL + getTranslation("director-redirect-2")).then(res => { + if (res) { + hangup(false); + window.location.href = changeURL; + } + }); + } +} + +function updateLinkInverse(arg, input) { + log("updateLinkInverse"); + log(input.dataset.param); + if (!input.checked) { + getById("director_block_" + arg).dataset.raw += input.dataset.param; + + var string = getById("director_block_" + arg).dataset.raw; + + if (arg == 1 && getById("obfuscate_director_" + arg).checked) { + string = obfuscateURL(string); + } + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; + } else { + var string = getById("director_block_" + arg).dataset.raw + "&"; + string = string.replace(input.dataset.param + "&", "&"); + string = string.substring(0, string.length - 1); + getById("director_block_" + arg).dataset.raw = string; + + if (arg == 1 && getById("obfuscate_director_" + arg).checked) { + string = obfuscateURL(string); + } + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; + } +} + +function updateLinkScene(arg, input) { + log("updateLinkScene"); + var string = getById("director_block_" + arg).dataset.raw || ""; + + if (input.checked) { + string = changeParam(string, "scene", "0"); + } else { + string = changeParam(string, "scene", "1"); + } + getById("director_block_" + arg).dataset.raw = string; + + if (arg == 1 && getById("obfuscate_director_" + arg).checked) { + string = obfuscateURL(string); + } + + getById("director_block_" + arg).href = string; + getById("director_block_" + arg).innerText = string; +} + +function fullscreenPageToggle(state = null) { + try { + if (!document.fullscreenElement) { + // not currently full screen + if (state !== false) { + // if state is false, we are already not full screen + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(); + } + } + } else if (document.exitFullscreen) { + if (!state) { + // if toggle mode or state=false + document.exitFullscreen(); + } + } else if (document.webkitExitFullscreen) { + if (!state) { + // if toggle mode or state=false + document.webkitExitFullscreen(); + } + } + //updateMixer(); // we will do this on the event for this instead + } catch (e) { + errorlog(e); + } +} + +session.pipWindow = false; +async function PictureInPicturePageToggle(state = null) { + try { + if (typeof documentPictureInPicture === "undefined") { + return; + } + if (session.pipWindow) { + getById("testtone").parentNode.insertBefore(session.pipWindow.document.getElementById("gridlayout"), getById("testtone")); + session.pipWindow.close(); + session.pipWindow = null; + updateMixer(); + getById("PictureInPicturePage").classList.remove("green"); + } else { + session.pipWindow = await documentPictureInPicture.requestWindow({ width: 614, height: 344 }); // 360 + 30px for the window header + + session.pipWindow.addEventListener("pagehide", event => { + if (session.pipWindow) { + getById("testtone").parentNode.insertBefore(session.pipWindow.document.getElementById("gridlayout"), getById("testtone")); + session.pipWindow.close(); + session.pipWindow = null; + updateMixer(); + getById("PictureInPicturePage").classList.remove("green"); + } + }); + + var pipWindowHead = 'Pop-out Window'; + pipWindowHead += ''; + + session.pipWindow.document.body.className = "main"; + session.pipWindow.document.head.innerHTML = pipWindowHead; + session.pipWindow.document.body.style = document.body.style; + session.pipWindow.document.title = "Pop-out Window"; + session.pipWindow.document.body.append(getById("gridlayout")); + session.pipWindow.onresize = updateMixer; + updateMixer(); // just in case onresize doesn't trigger + getById("PictureInPicturePage").classList.add("green"); + } + } catch (e) { + errorlog(e); + } +} + +function resetGen() { + getById("gencontent").style.display = "block"; + getById("gencontent2").style.display = "none"; + getById("gencontent2").className = ""; //container-inner + getById("gencontent").className = "container-inner"; // + getById("gencontent2").innerHTML = ""; + getById("videoname4").focus(); +} + +function generateQRPageCallback(hash) { + try { + var title = getById("videoname4").value; + if (title.length) { + title = title.replace(/[\W]+/g, "_").replace(/_+/g, "_"); // but not what others might get. TODO: allow for non-alphanumeric characters; santitize, then URL encode instead, + title = "&label=" + title; + } + var sid = session.generateStreamID(); + + var viewstr = ""; + var sendstr = ""; + + if (getById("invite_bitrate").checked) { + viewstr += "&bitrate=20000"; + } + if (getById("invite_vp9").checked) { + viewstr += "&codec=vp9"; + } + if (getById("invite_stereo").checked) { + viewstr += "&stereo"; + sendstr += "&stereo"; + } + if (getById("invite_automic").checked) { + sendstr += "&audiodevice=1"; + } + if (getById("invite_automic").checked) { + sendstr += "&audiodevice=1"; + } + if (getById("invite_effects").checked) { + sendstr += "&effects"; + } + + if (getById("invite_remotecontrol").checked) { + // + var remote_gen_id = session.generateStreamID(); + sendstr += "&remote=" + remote_gen_id; // security + viewstr += "&remote=" + remote_gen_id; + } + + if (getById("invite_joinroom").value.trim().length) { + sendstr += "&ssid&room=" + getById("invite_joinroom").value.trim(); + viewstr += "&solo&room=" + getById("invite_joinroom").value.trim(); + } + + if (getById("invite_password").value.trim().length) { + sendstr += "&hash=" + hash; + viewstr += "&password=" + sanitizePassword(getById("invite_password").value.trim()); + } + + if (session.token) { + sendstr += "&token=" + session.token; + viewstr += "&token=" + session.token; + } + + if (getById("invite_group_chat_type").value) { + // 0 is default + if (getById("invite_group_chat_type").value == 1) { + // no video + sendstr += "&novideo"; + } else if (getById("invite_group_chat_type").value == 2) { + // no view or audio + sendstr += "&view"; + } + } + + if (getById("invite_quality").value) { + if (getById("invite_quality").value == 0) { + sendstr += "&quality=0"; + } else if (getById("invite_quality").value == 1) { + sendstr += "&quality=1"; + } else if (getById("invite_quality").value == 2) { + sendstr += "&quality=2"; + } + } + + var wss = ""; + + if (session.wssSetViaUrl) { + if (session.customWSS && session.customWSS !== true) { + wss = "&pie=" + session.customWSS; + } else if (session.customWSS == true) { + wss = "&wss=" + session.wss; + } else { + wss = "&wss2=" + session.wss; + } + } + + var hoststr = ""; + if (getById("invite_hostlink").checked) { + hoststr = "https://" + location.host + location.pathname + "?push=" + sid + "_hostlink" + "&view=" + sid + sendstr + "&bitrate=500" + title + wss; + sendstr = "https://" + location.host + location.pathname + "?push=" + sid + "&view=" + sid + "_hostlink" + sendstr + "&bitrate=1200" + title + wss; + } else { + sendstr = "https://" + location.host + location.pathname + "?push=" + sid + sendstr + title + wss; + } + + if (getById("invite_obfuscate").checked) { + sendstr = obfuscateURL(sendstr); + } + + viewstr = "https://" + location.host + location.pathname + "?view=" + sid + viewstr + title + wss; + getById("gencontent").style.display = "none"; + getById("gencontent").className = ""; // + getById("gencontent2").style.display = "block"; + getById("gencontent2").className = "container-inner"; + + getById("gencontent2").innerHTML = + '
    \ +

    Guest Invite Link:

    \ + ' + + sendstr + + '

    \ +

    and don\'t forget the

    OBS Browser Source Link:

    ' + + viewstr + + ' \ + '; + + if (hoststr) { + getById("gencontent2").innerHTML += '

    Host Chat Link:

    ' + hoststr + ' '; + } + + getById("gencontent2").innerHTML += + '

    \ + \ +
  • This invite link and OBS ingestion link are reusable.
  • \ +
  • Only one person may use a specific invite at a time.
  • \ +
  • The stream ID can be changed manually to something else; keep it unique and alphanumeric.
  • \ +
  • Nothing is stored server-side; links do not expire, nor is there anything to delete.
  • \ +


    \ + '; + + var qrcode = new QRCode(getById("qrcode"), { + width: 300, + height: 300, + colorDark: "#000000", + colorLight: "#FFFFFF", + useSVG: false + }); + qrcode.makeCode(sendstr); + getById("qrcode").title = ""; + setTimeout(function () { + getById("qrcode").title = ""; + if (getById("qrcode").getElementsByTagName("img").length) { + getById("qrcode").getElementsByTagName("img")[0].style.cursor = "none"; + getById("qrcode").getElementsByTagName("img")[0].style.margin = "0 auto"; + } + }, 100); // i really hate the title overlay that the qrcode function makes + } catch (e) { + errorlog(e); + } +} + +function initSceneList(UUID) { + Object.keys(session.sceneList).forEach((scene, index) => { + if (getById("container_" + UUID).querySelectorAll('[data-scene="' + scene + '"]').length) { + return; + } // already exists. + var newScene = document.createElement("div"); + newScene.innerHTML = '"; + newScene.classList.add("customScene"); + + var added = false; + getById("container_" + UUID) + .querySelectorAll(".customScene>[data-scene]") + .forEach(ele => { + log(ele); + if (!added && ele.dataset.scene > scene + "") { + ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); + added = true; + } + }); + if (!added) { + getById("container_" + UUID).appendChild(newScene); + } + }); +} + +function updateSceneList(scene) { + // custom scenes only. + if (!session.director) { + return; + } + if (scene in session.sceneList) { + return; + } + + if (parseInt(scene) + "" === scene) { + if (parseInt(scene) >= 0 && parseInt(scene) <= session.maxScene) { + return; + } + } + + session.sceneList[scene] = true; + for (var UUID in session.rpcs) { + var newScene = document.createElement("div"); + newScene.innerHTML = '"; + newScene.classList.add("customScene"); + var added = false; + getById("container_" + UUID) + .querySelectorAll(".customScene>[data-scene]") + .forEach(ele => { + log(ele); + if (!added && ele.dataset.scene > scene + "") { + ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); + added = true; + } + }); + if (!added) { + getById("container_" + UUID).appendChild(newScene); + } + } + + if (session.showDirector) { + if (document.getElementById("container_director")) { + var newScene = document.createElement("div"); + newScene.innerHTML = '"; + newScene.classList.add("customScene"); + //getById("container_director").appendChild(newScene); + + var added = false; + getById("container_director") + .querySelectorAll(".customScene>[data-scene]") + .forEach(ele => { + if (!added && ele.dataset.scene > scene + "") { + ele.parentNode.parentNode.insertBefore(newScene, ele.parentNode); + added = true; + } + }); + if (!added) { + getById("container_director").appendChild(newScene); + } + } + } +} + +var vis = (function () { + var stateKey, + eventKey, + keys = { + hidden: "visibilitychange", + webkitHidden: "webkitvisibilitychange", + mozHidden: "mozvisibilitychange", + msHidden: "msvisibilitychange" + }; + for (stateKey in keys) { + if (stateKey in document) { + eventKey = keys[stateKey]; + break; + } + } + return function (c) { + if (c) { + document.addEventListener(eventKey, c); + //document.addEventListener("blur", c); + //document.addEventListener("focus", c); + } + return !document[stateKey]; + }; +})(); + +function unPauseVideo(videoEle, update = true) { + try { + if (!videoEle) { + return; + } else if (!(videoEle.dataset.UUID in session.rpcs)) { + return; + } else if (!("prePausedBandwidth" in session.rpcs[videoEle.dataset.UUID])) { + return; + } // not paused; useless to have, but might as well + session.rpcs[videoEle.dataset.UUID].manualBandwidth = false; + //session.rpcs[videoEle.dataset.UUID].manualAudioBandwidth = false; + + if (session.rpcs[videoEle.dataset.UUID].videoElement) { + session.rpcs[videoEle.dataset.UUID].videoElement.play(); + } + + delete session.rpcs[videoEle.dataset.UUID].prePausedBandwidth; + session.requestRateLimit(false, videoEle.dataset.UUID, false); // passing a bitrate of false forces the saved existing bitrate to be requested. + videoEle.classList.remove("paused"); + videoEle.classList.remove("partialFadeout"); + if (update) { + updateMixer(); + } + } catch (e) { + errorlog(e); + } +} + +function pauseVideo(videoEle, update = true) { + if (!videoEle) { + return; + } else if (!(videoEle.dataset.UUID in session.rpcs)) { + return; + } + session.rpcs[videoEle.dataset.UUID].prePausedBandwidth = session.rpcs[videoEle.dataset.UUID].manualBandwidth; // useless, but whatever + session.rpcs[videoEle.dataset.UUID].manualBandwidth = 0; + + if (session.rpcs[videoEle.dataset.UUID].videoElement) { + session.rpcs[videoEle.dataset.UUID].videoElement.pause(); + } + //session.rpcs[videoEle.dataset.UUID].manualAudioBandwidth = 0; + session.requestRateLimit(false, videoEle.dataset.UUID, true); // passing a bitrate of false forces the saved existing bitrate to be requested. + videoEle.classList.add("paused"); + videoEle.classList.add("partialFadeout"); + if (update) { + updateMixer(); + } +} + +(function rightclickmenuthing() { + // right click menu + "use strict"; + + function clickInsideElement(e, value = "menu") { + var el = e.srcElement || e.target; + if (el.dataset && value in el.dataset) { + return el; + } else { + while ((el = el.parentNode)) { + if (el.dataset && value in el.dataset) { + return el; + } + } + } + return false; + } + + function getPosition(event2) { + var posx = 0; + var posy = 0; + + if (!event2) { + var event = window.event; + } + + if (event2.pageX || event2.pageY) { + posx = event2.pageX; + posy = event2.pageY; + } else if (event2.clientX || event2.clientY) { + posx = event2.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + posy = event2.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + return { x: posx, y: posy }; + } + + var taskItemInContext; + var clickCoordsX; + var clickCoordsY; + var menu; + var menuState = 0; + var lastMenu = false; + var menuWidth; + var menuHeight; + var windowWidth; + var windowHeight; + + function contextListener() { + document.addEventListener("contextmenu", function (e) { + + if (!session.cleanish && session.cleanOutput) { + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + if (e && !e.ctrlKey && !e.metaKey) { + return; + } + } else if (e && (e.ctrlKey || e.metaKey)) { + return; + } // allow for development ease + + taskItemInContext = clickInsideElement(e, "menu"); + + if (taskItemInContext) { + e.preventDefault(); + e.stopPropagation(); + if (taskItemInContext.dataset && taskItemInContext.dataset.menu) { + toggleMenuOn(taskItemInContext.dataset.menu, taskItemInContext); + } else { + toggleMenuOn(); + } + positionMenu(e); + return false; + } else { + taskItemInContext = null; + toggleMenuOff(); + } + }); + } + + function menuClickListener(e) { + var clickeElIsLink = clickInsideElement(e, "action"); + if (clickeElIsLink) { + e.preventDefault(); + e.stopPropagation(); + menuItemListener(clickeElIsLink, false, e); + return false; + } else { + var button = e.which || e.button; + if (button === 1) { + toggleMenuOff(); + } + } + } + + function handleInputElement(e) { + // for the input range slider version + var clickeElIsLink = clickInsideElement(e, "action"); + if (clickeElIsLink) { + e.preventDefault(); + e.stopPropagation(); + menuItemListener(clickeElIsLink, e.srcElement, e); + return false; + } else { + var button = e.which || e.button; + if (button === 1) { + toggleMenuOff(); + } + } + } + + function toggleMenuOn(menutype = false, ele = false) { + if (lastMenu && lastMenu !== menutype) { + try { + menuState = 0; + getById(lastMenu).classList.remove("context-menu--active"); + + document.removeEventListener("click", menuClickListener); + menu.querySelectorAll("input").forEach(ele => { + ele.removeEventListener("input", handleInputElement); + }); + } catch (e) { } + } + menu = getById(menutype || "context-menu"); + menuItemSyncState(menu); + if (menuState !== 1) { + menuState = 1; + menu.classList.add("context-menu--active"); + document.addEventListener("click", menuClickListener); + menu.querySelectorAll("input").forEach(ele => { + ele.addEventListener("input", handleInputElement); + }); + } + + if (ele && ele.classOptions) { + menu.classList.add(ele.classOptions); + } + lastMenu = menutype || "context-menu"; + } + + function toggleMenuOff() { + if (menuState !== 0) { + menuState = 0; + menu.classList.remove("context-menu--active"); + + document.removeEventListener("click", menuClickListener); + menu.querySelectorAll("input").forEach(ele => { + ele.removeEventListener("input", handleInputElement); + }); + } + lastMenu = false; + } + + function positionMenu(e) { + try { + var clickCoords = getPosition(e); + clickCoordsX = clickCoords.x; + clickCoordsY = clickCoords.y; + } catch (e) { + errorlog(e); + return; + } + + menuWidth = menu.offsetWidth + 4; + menuHeight = menu.offsetHeight + 4; + + windowWidth = window.innerWidth; + windowHeight = window.innerHeight; + + if (windowWidth - clickCoordsX < menuWidth) { + menu.style.left = windowWidth - menuWidth + "px"; + } else { + menu.style.left = clickCoordsX + "px"; + } + + if (windowHeight - clickCoordsY < menuHeight) { + menu.style.top = windowHeight - menuHeight + "px"; + } else { + menu.style.top = clickCoordsY + "px"; + } + + // Handle submenu edge positioning + var submenus = menu.querySelectorAll('.context-menu__submenu'); + submenus.forEach(function(submenu) { + submenu.classList.remove('context-menu__submenu--left'); + var parentRect = submenu.parentElement.getBoundingClientRect(); + var submenuWidth = 200; // Width defined in CSS + if (parentRect.right + submenuWidth > windowWidth) { + submenu.classList.add('context-menu__submenu--left'); + } + }); + } + + async function menuItemListener(link, inputElement = false, e = false) { + + if (link.getAttribute("data-action") === "Open") { + window.open(taskItemInContext.href); + } else if (link.getAttribute("data-action") === "Copy") { + copyFunction(taskItemInContext.href); + } else if (link.getAttribute("data-action") === "Mirror") { + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + session.mirrored = !session.mirrored; + applyMirror(session.mirrorExclude); + log("session.mirrored"); + } else { + if ("mirror" in taskItemInContext) { + taskItemInContext.mirror = !taskItemInContext.mirror; + applyMirrorGuest(taskItemInContext.mirror, taskItemInContext); + } else { + taskItemInContext.mirror = true; + applyMirrorGuest(taskItemInContext.mirror, taskItemInContext); + } + } + } else if (link.getAttribute("data-action") === "Rotate") { + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + session.rotate = ((session.rotate || 0) + 90) % 360; + if (Firefox && session.mobile) { + updateForceRotate(true); + } else { + updateForceRotate(false); + } + log("session.rotate"); + setTimeout(function () { + updateMixer(); + }, 1); + } else { + if ("manualRotate" in taskItemInContext) { + taskItemInContext.manualRotate = ((taskItemInContext.manualRotate || 0) + 90) % 360; + taskItemInContext.rotated = taskItemInContext.manualRotate; + } else { + taskItemInContext.manualRotate = ((taskItemInContext.rotated || 0) + 90) % 360; + taskItemInContext.rotated = taskItemInContext.manualRotate; + } + + if (taskItemInContext.dataset) { + taskItemInContext.dataset.rotated = taskItemInContext.rotated || 0; + } + updateVideoTransform(taskItemInContext); + setTimeout(function () { + updateMixer(); + }, 1); + } + } else if (link.getAttribute("data-action") === "FullWindow") { + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + session.infocus = true; + } else { + session.infocus = taskItemInContext.dataset.UUID; + } + updateMixer(); + } else if (link.getAttribute("data-action") === "ShrinkWindow") { + session.infocus = false; + updateMixer(); + } else if (link.getAttribute("data-action") === "Pause") { + pauseVideo(taskItemInContext); + } else if (link.getAttribute("data-action") === "UnPause") { + unPauseVideo(taskItemInContext); + } else if (link.getAttribute("data-action") === "PiP") { + togglePictureInPicture(taskItemInContext); + } else if (link.getAttribute("data-action") === "PiP2") { + PictureInPicturePageToggle(); + } else if (link.getAttribute("data-action") === "Record") { + if (taskItemInContext.stopWriter || taskItemInContext.recording) { + } else if (taskItemInContext.startWriter) { + taskItemInContext.startWriter(); + } else { + var videoKbps = session.recordDefault; + if (session.recordLocal !== false) { + videoKbps = session.recordLocal; + } + recordLocalVideo(null, videoKbps, taskItemInContext); + } + } else if (link.getAttribute("data-action") === "StopRecording") { + if (taskItemInContext.stopWriter) { + taskItemInContext.stopWriter(); + } else if (taskItemInContext.recording) { + recordLocalVideo("stop", null, taskItemInContext); + } + } else if (link.getAttribute("data-action") === "CopyFrameAsImage") { + copyVideoFrameToClipboard(taskItemInContext, e); + } else if (link.getAttribute("data-action") === "SaveFrameToDisk") { + saveVideoFrameToDisk(taskItemInContext, e); + } else if (link.getAttribute("data-action") === "DrawOnVideo") { + if (taskItemInContext.clearDrawOnVideo) { + taskItemInContext.clearDrawOnVideo(); + taskItemInContext.clearDrawOnVideo = null; + } else { + taskItemInContext.clearDrawOnVideo = drawOnThis(taskItemInContext); + } + } else if (link.getAttribute("data-action") === "ChangeBuffer") { + toggleBufferSettings(taskItemInContext.dataset.UUID); + } else if (link.getAttribute("data-action") === "Cast") { + //copyFunction(taskItemInContext.href); + } else if (link.getAttribute("data-action") === "Controls") { + + //getById("main").classList.add("forcecontrols"); // adds an annoying shadow to the bar area + //taskItemInContext.showControlBar = true; + //checkVideoControlBar(taskItemInContext); + //taskItemInContext.controls = false; + //ele.focus(); + taskItemInContext.removeAttribute("controls"); + taskItemInContext.setAttribute("controls", ""); + taskItemInContext.controls = true; + + } else if (link.getAttribute("data-action") === "HideControls") { + + //taskItemInContext.showControlBar = false; + taskItemInContext.controls = false; + taskItemInContext.removeAttribute("controls"); + + } else if (link.getAttribute("data-action") === "Edit") { + //copyFunction(taskItemInContext.href); + var response = await promptAlt("Please note, manual edits to the URL may conflict with the toggles", false, false, taskItemInContext.href); + if (response) { + taskItemInContext.href = response; + taskItemInContext.dataset.raw = response; + taskItemInContext.innerHTML = response; + } + } else if (link.getAttribute("data-action") === "QRCode") { + warnUser("Loading QR Code"); + loadQR(function tt(url) { + getById("alertModalMessage").innerHTML = ""; + var qrcode = new QRCode(getById("alertModalMessage"), { + width: 300, + height: 300, + colorDark: "#000000", + colorLight: "#FFFFFF", + useSVG: false + }); + qrcode.makeCode(url); + getById("alertModalMessage").title = ""; + setTimeout(function () { + getById("alertModalMessage").title = ""; + if (getById("alertModalMessage").getElementsByTagName("img").length) { + getById("alertModalMessage").getElementsByTagName("img")[0].style.cursor = "none"; + } + }, 100); + }, taskItemInContext.href); + } else if (link.getAttribute("data-action") === "ShowStats") { + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + var [menu, innerMenu] = statsMenuCreator(); + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu); + printMyStats(innerMenu); + } else if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, taskItemInContext.dataset.UUID); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, taskItemInContext.dataset.UUID); + } + } else if (link.getAttribute("data-action") === "OutputAudio") { + enumerateDevices().then(function (deviceInfo) { + var ele = getById(taskItemInContext.id); + + var deviceListElement = gotDevices3(deviceInfo, ele); + if (deviceListElement) { + warnUser("Select the audio playback destination for this media:\n\n"); + getById("alertModalMessage").appendChild(deviceListElement); + } else { + warnUser("No output devices available"); + } + }); + + // + } else if (link.getAttribute("data-action") === "RemoteHangup") { + if (session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { + var confirmHangup = confirm(getTranslation("confirm-disconnect-user")); + if (confirmHangup) { + var msg = {}; + msg.hangup = true; + msg.remote = session.remote; + msg = await session.encodeRemote(msg); + session.sendRequest(msg, taskItemInContext.dataset.UUID); + pokeIframeAPI("hungup", "remote", taskItemInContext.dataset.UUID); + } + } + } else if (link.getAttribute("data-action") === "RemoteReload") { + // Remote Reload Page - basic director privilege, no &remote required + if (session.rpcs[taskItemInContext.dataset.UUID]) { + var confirmReload = confirm(getTranslation("confirm-reload-user")); + if (confirmReload) { + var msg = {}; + msg.reload = true; + session.sendRequest(msg, taskItemInContext.dataset.UUID); + pokeIframeAPI("reload", "remote", taskItemInContext.dataset.UUID); + } + } + } else if (link.getAttribute("data-action") === "PTZControls") { + // Requires MUTUAL remote: both local viewer AND remote peer must have &remote + if (session.remote && session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { + togglePTZControls(taskItemInContext.dataset.UUID); + } + } else if (link.getAttribute("data-action") === "ResetAutofocus") { + // Requires MUTUAL remote: both local viewer AND remote peer must have &remote + if (session.remote && session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { + session.requestAutofocusChange(true, taskItemInContext.dataset.UUID, session.remote); + } + } else if (link.getAttribute("data-action") === "RemoteControlsParent") { + return; // Don't close menu on submenu parent click + } else if (link.getAttribute("data-action") === "SSNewTab") { + var URL = "https://" + window.location.hostname + location.pathname + createScreenShareURL(false); + log(URL); + window.open(URL, "_blank").focus(); + } else if (link.getAttribute("data-action") === "pip-clock") { + popOutClock(taskItemInContext.children[0]); + } else if (link.getAttribute("data-action") === "Publish") { + var URL = taskItemInContext.href; + URL += "&clean&chroma=000&ssar=landscape&nosettings&prefercurrenttab&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&whippush&whippushtoken&q=1"; + var win = window.open(URL, "targetWindow", "toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720"); + win.focus(); + win.resizeTo(1280, 720); + } else if (link.getAttribute("data-action") === "RecordWindow") { + var URL = taskItemInContext.href; + URL += "&clean&chroma=000&ssar=landscape&nosettings&prefercurrenttab&selfbrowsersurface=include&displaysurface=browser&np&nopush&publish&autorecordlocal"; + var win = window.open(URL, "targetWindow", "toolbar=no,location=no,status=no,scaling=no,menubar=no,scrollbars=no,resizable=no,width=1280,height=720"); + win.focus(); + win.resizeTo(1280, 720); + } else if (link.getAttribute("data-action") === "SendTip") { + var UUID = taskItemInContext.dataset.UUID; + if (UUID && session.rpcs[UUID] && session.rpcs[UUID].acceptsTips) { + if (typeof openTipModal === 'function') { + openTipModal(UUID); + } + } else if (session.pcs && session.pcs[UUID] && session.pcs[UUID].acceptsTips) { + if (typeof openTipModal === 'function') { + openTipModal(UUID); + } + } + } + + if (inputElement === false) { + log("Task ID - " + taskItemInContext + ", Task action - " + link.getAttribute("data-action")); + toggleMenuOff(); + } + } + + function menuItemSyncState(menu) { + var items = menu.querySelectorAll("[data-action]"); + for (var i = 0; i < items.length; i++) { + if (items[i].getAttribute("data-action") === "FullWindow") { + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + if (session.infocus === true) { + items[i].parentNode.classList.add("hidden"); + } else { + items[i].parentNode.classList.remove("hidden"); + } + } else if (taskItemInContext.dataset.UUID === session.infocus) { + items[i].parentNode.classList.add("hidden"); + } else { + items[i].parentNode.classList.remove("hidden"); + } + } else if (items[i].getAttribute("data-action") === "ShrinkWindow") { + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + if (session.infocus === true) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (taskItemInContext.dataset.UUID === session.infocus) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "Pause") { + if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) { + if ("prePausedBandwidth" in session.rpcs[taskItemInContext.dataset.UUID]) { + items[i].parentNode.classList.add("hidden"); + } else { + items[i].parentNode.classList.remove("hidden"); + } + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "UnPause") { + if (taskItemInContext.dataset.UUID && taskItemInContext.dataset.UUID in session.rpcs) { + if ("prePausedBandwidth" in session.rpcs[taskItemInContext.dataset.UUID]) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "Record") { + if (taskItemInContext.stopWriter || taskItemInContext.recording) { + items[i].parentNode.classList.add("hidden"); + } else { + items[i].parentNode.classList.remove("hidden"); + } + } else if (items[i].getAttribute("data-action") === "StopRecording") { + if (taskItemInContext.stopWriter || taskItemInContext.recording) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "CopyFrameAsImage") { + if (taskItemInContext.srcObject && taskItemInContext.srcObject.getVideoTracks().length) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "SaveFrameToDisk") { + if (taskItemInContext.srcObject && taskItemInContext.srcObject.getVideoTracks().length) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "Controls") { + if (taskItemInContext.controls) { + items[i].parentNode.classList.add("hidden"); + } else { + items[i].parentNode.classList.remove("hidden"); + } + } else if (items[i].getAttribute("data-action") === "HideControls") { + if (taskItemInContext.controls) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "PiP2") { + if (typeof documentPictureInPicture !== "undefined") { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "RemoteControlsParent") { + // Show/hide the entire Remote Controls submenu + // Requires MUTUAL remote: both local viewer AND remote peer must have &remote + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + items[i].parentNode.classList.add("hidden"); + } else if (session.remote && session.rpcs[taskItemInContext.dataset.UUID] && session.rpcs[taskItemInContext.dataset.UUID].stats.info && "remote" in session.rpcs[taskItemInContext.dataset.UUID].stats.info && session.rpcs[taskItemInContext.dataset.UUID].stats.info.remote) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "ChangeBuffer") { + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + items[i].parentNode.classList.add("hidden"); + } else if (session.rpcs[taskItemInContext.dataset.UUID]) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "TipRightClick") { + if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + items[i].parentNode.classList.add("hidden"); + } else { + items[i].parentNode.classList.remove("hidden"); + } + } else if (items[i].getAttribute("data-action") === "SendTip") { + // Show tip option only if: + // 1. Video source accepts tips (publisher opt-in) + // 2. Viewer has opted in with &showtips (viewer opt-in) + // 3. Not in clean mode + // 4. Not in Electron + // 5. Performer has completed Stripe setup (validated) + var UUID = taskItemInContext.dataset.UUID; + var acceptsTips = false; + var tipId = null; + var tipServer = null; + if (UUID) { + if (session.rpcs && session.rpcs[UUID] && session.rpcs[UUID].acceptsTips) { + acceptsTips = true; + tipId = session.rpcs[UUID].tipId; + tipServer = session.rpcs[UUID].tipServer; + } else if (session.pcs && session.pcs[UUID] && session.pcs[UUID].acceptsTips) { + acceptsTips = true; + tipId = session.pcs[UUID].tipId; + tipServer = session.pcs[UUID].tipServer; + } + } + // Check if performer is validated (use cache if available) + var performerValid = false; + if (tipId) { + tipServer = tipServer || session.tipServer || "https://ninjabacker.com"; + var cacheKey = tipServer + "/" + tipId; + performerValid = tipPerformerCache[cacheKey] === true; + } + if (acceptsTips && performerValid && session.showTips && !session.cleanOutput && navigator.userAgent.toLowerCase().indexOf(" electron/") === -1) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "Publish") { + if (taskItemInContext.classList.contains("publish")) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "RecordWindow") { + if (taskItemInContext.classList.contains("publish")) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } else if (items[i].getAttribute("data-action") === "RemoteReload") { + // Remote Reload Page - show for any valid RPC connection (basic director privilege) + if (taskItemInContext.id == "videosource" || taskItemInContext.id == "previewWebcam") { + items[i].parentNode.classList.add("hidden"); + } else if (session.rpcs[taskItemInContext.dataset.UUID]) { + items[i].parentNode.classList.remove("hidden"); + } else { + items[i].parentNode.classList.add("hidden"); + } + } + } + } + contextListener(); +})(); + +function checkVideoControlBar(ele) { // this is aggressive. Lets not use it unless required. + if (ele) { + if (ele.showControlBar) { + if (ele.showControlBarInterval) { + clearTimeout(ele.showControlBarInterval); + } + ele.focus(); + ele.removeAttribute("controls"); + ele.setAttribute("controls", ""); + ele.focus(); + ele.showControlBarInterval = setTimeout(function (ele) { + checkVideoControlBar(ele); + }, 100, ele); + } + } +} +function gotDevices3(deviceInfos, vid) { + var audioEle = document.createElement("select"); + log(deviceInfos); + if (!deviceInfos.length) { + return false; + } + for (let i = 0; i !== deviceInfos.length; ++i) { + if (deviceInfos[i].kind === "audiooutput") { + var opt = document.createElement("option"); + opt.innerText = deviceInfos[i].label; + opt.value = deviceInfos[i].deviceId; + audioEle.appendChild(opt); + audioEle.videoTarget = vid; + if (vid.sinkId) { + if (vid.sinkId == deviceInfos[i].deviceId) { + opt.selected = true; + } + } else if (vid.manualSink) { + if (vid.manualSink == deviceInfos[i].deviceId) { + opt.selected = true; + } + } else if (session.sink) { + if (session.sink == deviceInfos[i].deviceId) { + opt.selected = true; + } + } + } + } + audioEle.onchange = function () { + vid.manualSink = this.options[this.selectedIndex].value; + if (this.videoTarget && this.videoTarget.dataset.UUID) { + session.audioEffects = true; + updateIncomingAudioElement(this.videoTarget.dataset.UUID); + } + resetupAudioOut(this.videoTarget); + }; + return audioEle; +} + +function popupMessage(e, message = "Copied to Clipboard") { + // right click menu + + var posx = 0; + var posy = 0; + + if (!e) var e = window.event; + + if (e.pageX || e.pageY) { + posx = e.pageX; + posy = e.pageY; + } else if (e.clientX || e.clientY) { + posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + posx += 10; + + var menu = getById("messagePopup"); + menu.innerHTML = "
    " + message + "
    "; + var menuState = 0; + var menuWidth; + var menuHeight; + var menuPosition; + var menuPositionX; + var menuPositionY; + + var windowWidth; + var windowHeight; + + if (menuState !== 1) { + menuState = 1; + menu.classList.add("context-menu--active"); + } + + menuWidth = menu.offsetWidth + 4; + menuHeight = menu.offsetHeight + 4; + + windowWidth = window.innerWidth; + windowHeight = window.innerHeight; + + if (windowWidth - posx < menuWidth) { + menu.style.left = windowWidth - menuWidth + "px"; + } else { + menu.style.left = posx + "px"; + } + + if (windowHeight - posy < menuHeight) { + menu.style.top = windowHeight - menuHeight + "px"; + } else { + menu.style.top = posy + "px"; + } + + function toggleMenuOff() { + if (menuState !== 0) { + menuState = 0; + menu.classList.remove("context-menu--active"); + } + } + menu.classList.remove("fadeout"); + + var showlength = message.length * 50 || 500; + + setTimeout(function () { + menu.classList.add("fadeout"); + }, showlength); + + setTimeout(function () { + toggleMenuOff(); + }, showlength + 1000); +} + +function timeSince(date) { + var seconds = Math.floor((new Date() - date) / 1000); + + var interval = seconds / 31536000; + + if (interval > 1) { + return Math.floor(interval) + " years"; + } + interval = seconds / 2592000; + if (interval > 1) { + return Math.floor(interval) + " months"; + } + interval = seconds / 86400; + if (interval > 1) { + return Math.floor(interval) + " days"; + } + interval = seconds / 3600; + if (interval > 1) { + return Math.floor(interval) + " hours"; + } + interval = seconds / 60; + if (interval > 1) { + return Math.floor(interval) + " minutes"; + } + return "Seconds ago"; +} + +var messageList = []; +function sendChatMessage(chatMsg = false, bc = false) { + // filtered + visual + var data = {}; + if (chatMsg === false) { + var msg = document.getElementById("chatInput").value; + } else { + var msg = chatMsg; + } + //msg = sanitizeChat(msg); + if (msg == "") { + return false; + } + + msg = convertShortcodes(msg); + + var label = ""; + if (session.label) { + if (session.director) { + label = "" + session.label + ": "; + } else { + label = "" + session.label + ": "; + } + } else if (session.director) { + label = "Director: "; + } + + if (msg.trim() === "/list") { + var listMsg = null; + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].label) { + listMsg = UUID + ": " + session.rpcs[UUID].label; + } else if (session.directorList.indexOf(UUID) >= 0) { + listMsg = UUID + ": Director"; + } else { + listMsg = UUID + ": Unknown User"; + } + var data = {}; + data.msg = listMsg; + data.label = false; + data.type = "alert"; + data.time = Date.now(); + messageList.push(data); + } + for (var UUID in session.pcs) { + if (UUID in session.rpcs) { + continue; + } + if (session.pcs[UUID].label) { + listMsg = UUID + "; " + session.pcs[UUID].label; + } else if (session.directorList.indexOf(UUID) >= 0) { + listMsg = UUID + "; Director"; + } else { + listMsg = UUID + "; Unknown User"; + } + var data = {}; + data.msg = listMsg; + data.label = false; + data.type = "alert"; + data.time = Date.now(); + messageList.push(data); + } + if (listMsg === null) { + data.msg = "No other users are connected to you"; + data.label = false; + data.type = "alert"; + data.time = Date.now(); + messageList.push(data); + } + } else if (msg.startsWith("/msg ")) { + var msg = msg.split("/msg ")[1]; + msg = msg.split(" "); + uid = msg.shift().toLowerCase(); + msg = msg.join(" "); + if (msg == "") { + return false; + } + var sent = false; + for (var UUID in session.rpcs) { + if (UUID.startsWith(uid)) { + sendChat(msg, UUID); // send message to peers + var data = {}; + data.time = Date.now(); + data.msg = sanitizeChat(msg); // this is what the other person should see + data.label = label; + data.type = "sent"; + messageList.push(data); + sent = true; + } else if (session.rpcs[UUID].label && session.rpcs[UUID].label.toLowerCase().startsWith(uid)) { + sendChat(msg, UUID); // send message to peers + var data = {}; + data.time = Date.now(); + data.msg = sanitizeChat(msg); // this is what the other person should see + data.label = label; + data.type = "sent"; + messageList.push(data); + sent = true; + } else if (session.directorList.indexOf(UUID) >= 0 && "director".startsWith(uid)) { + sendChat(msg, UUID); // send message to peers + var data = {}; + data.time = Date.now(); + data.msg = sanitizeChat(msg); // this is what the other person should see + data.label = label; + data.type = "sent"; + messageList.push(data); + sent = true; + } + } + for (var UUID in session.pcs) { + if (UUID in session.rpcs) { + continue; + } + if (UUID.startsWith(uid)) { + sendChat(msg, UUID); // send message to peers + var data = {}; + data.time = Date.now(); + data.msg = sanitizeChat(msg); // this is what the other person should see + data.label = label; + data.type = "sent"; + messageList.push(data); + sent = true; + } else if (session.pcs[UUID].label && session.pcs[UUID].label.toLowerCase().startsWith(uid)) { + sendChat(msg, UUID); // send message to peers + var data = {}; + data.time = Date.now(); + data.msg = sanitizeChat(msg); // this is what the other person should see + data.label = label; + data.type = "sent"; + messageList.push(data); + sent = true; + } else if (session.directorList.indexOf(UUID) >= 0 && "director".startsWith(uid)) { + sendChat(msg, UUID); // send message to peers + var data = {}; + data.time = Date.now(); + data.msg = sanitizeChat(msg); // this is what the other person should see + data.label = label; + data.type = "sent"; + messageList.push(data); + sent = true; + } + } + if (sent == false) { + var data = {}; + data.msg = "No user found. Message not sent."; + data.label = false; + data.type = "alert"; + data.time = Date.now(); + messageList.push(data); + updateMessages(); + return false; + } + } else if (msg.startsWith("/")) { + data.msg = "Unknown command. Try '/list' or '/msg username message'."; + data.label = false; + data.type = "alert"; + data.time = Date.now(); + messageList.push(data); + updateMessages(); + return false; + } else if (session.directorChat === true) { + if (session.directorList.length) { + for (var i = 0; i < session.directorList.length; i++) { + sendChat(msg, session.directorList[i]); // send message to peers + } + var data = {}; + data.time = Date.now(); + data.msg = sanitizeChat(msg); // this is what the other person should see + data.label = label; + data.type = "sent"; + messageList.push(data); + } + } else { + sendChat(msg); // send message to peers + data.time = Date.now(); + data.msg = sanitizeChat(msg); // this is what the other person should see + data.label = label; + data.type = "sent"; + messageList.push(data); + } + document.getElementById("chatInput").value = ""; + + messageList = messageList.slice(-100); + if (!bc && session.broadcastChannel !== false) { + log(session.broadcastChannel); + session.broadcastChannel.postMessage(data); + } + updateMessages(); + + if (isIFrame) { + parent.postMessage( + { + chat: data + }, + session.iframetarget + ); + } + + var apiBlob = {}; + apiBlob.time = data.time; + apiBlob.msg = msg; + apiBlob.label = session.label; + apiBlob.type = data.type; + pokeAPI("chat", apiBlob); + + return true; +} + +function disableQualityDirector(UUID) { + // lets revert back to the director's quality settings after viewing the scene + try { + var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.add("disable"); + elements[0].ariaPressed = "false"; + elements[0].classList.remove("pressed"); + elements[0].disabled = "true"; + elements[0].title = getTranslation("preview-meshcast-disabled"); + } + var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.add("disable"); + elements[0].ariaPressed = "false"; + elements[0].classList.remove("pressed"); + elements[0].disabled = "true"; + elements[0].title = getTranslation("preview-meshcast-disabled"); + } + var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.add("disable"); + elements[0].ariaPressed = "false"; + elements[0].classList.remove("pressed"); + elements[0].disabled = "true"; + elements[0].title = getTranslation("preview-meshcast-disabled"); + } + } catch (e) { + errorlog(e); + } +} + +function applyQualityDirector(uuid = false) { + // lets revert back to the director's quality settings after viewing the scene + if (uuid) { + var eles = document.querySelectorAll('#guestFeeds button.pressed[data-action-type="change-quality1"][data--u-u-i-d="' + uuid + '"],#guestFeeds button.pressed[data-action-type="change-quality2"][data--u-u-i-d="' + uuid + '"],#guestFeeds button.pressed[data-action-type="change-quality3"][data--u-u-i-d="' + uuid + '"]'); + eles.forEach(ele => { + ele.click(); + }); + } else { + var eles = document.querySelectorAll('#guestFeeds button.pressed[data-action-type="change-quality1"],#guestFeeds button.pressed[data-action-type="change-quality2"],#guestFeeds button.pressed[data-action-type="change-quality3"]'); + eles.forEach(ele => { + ele.click(); + }); + } +} + +function toggleQualityDirector(bitrate, UUID, ele) { + // ele is specific to the button in the director's room + var eles = ele.parentNode.childNodes; + for (var i = 0; i < eles.length; i++) { + eles[i].className = ""; + } + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + session.requestRateLimit(bitrate, UUID); +} + +var clockOverlayTimer = null; +function zpadTime(number) { + var output = "" + number; + while (output.length < 2) { + output = "0" + output; + } + return output; +} +function showClock() { + getById("overlayClockContainer").classList.remove("hidden"); +} +function hideClock() { + getById("overlayClockContainer").classList.add("hidden"); +} +function setClock(initial = false, color = "#000") { + if (initial !== false) { + initial = parseInt(initial); + getById("overlayClockContainer").dataset.initial = initial; + } else { + initial = parseInt(getById("overlayClockContainer").dataset.initial); + } + + if (initial < 0) { + initial = 0; + } + + updateClock(initial, color); +} +function stopClock() { + var clock = document.getElementById("overlayClock"); + //clock.ctx = null; + //clock.canvas = null; + + //if (document.pictureInPictureElement && clock.video) { + // if (document.pictureInPictureElement == clock.video){ + // document.exitPictureInPicture(); + // pokeIframeAPI('picture-in-picture', false); + // } + //clock.video.remove; + //} + clock.innerHTML = ""; + clearInterval(clockOverlayTimer); + //setClock(); + updateClock("0", "#444"); +} +function pauseClock() { + clearInterval(clockOverlayTimer); + var current = Date.now() - parseInt(getById("overlayClockContainer").dataset.timestamp); + + if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) { + current = parseInt(Math.round(current / 1000)); + } else { + current = parseInt(getById("overlayClockContainer").dataset.initial) - parseInt(Math.round(current / 1000)); + } + + getById("overlayClockContainer").dataset.current = current; + + updateClock(current, "#00F"); +} +function resumeClock() { + if ("current" in getById("overlayClockContainer").dataset) { + startClock(parseInt(getById("overlayClockContainer").dataset.current)); + } +} +function startClock(restart = true) { + clearInterval(clockOverlayTimer); + if (restart === true) { + getById("overlayClockContainer").dataset.timestamp = Date.now(); + } else if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) { + getById("overlayClockContainer").dataset.timestamp = Date.now() - parseInt(getById("overlayClockContainer").dataset.current * 1000); + } else { + getById("overlayClockContainer").dataset.timestamp = Date.now() - (parseInt(getById("overlayClockContainer").dataset.initial) * 1000 - parseInt(getById("overlayClockContainer").dataset.current * 1000)); + } + stepClock(); + var clock = document.getElementById("overlayClock"); + if (clock && clock.video) { + clock.innerHTML = ""; + clock.appendChild(clock.video); + clock.video.play(); + } + + clockOverlayTimer = setInterval(function () { + stepClock(); + }, 999); +} +function stepClock() { + var current = Date.now() - parseInt(getById("overlayClockContainer").dataset.timestamp); + if (parseInt(getById("overlayClockContainer").dataset.initial) == 0) { + current = parseInt(Math.round(current / 1000)); + } else { + current = parseInt(getById("overlayClockContainer").dataset.initial) - parseInt(Math.round(current / 1000)); + } + + if (session.directorList.length) { + var msg = {}; + msg.timer = current; + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + } + if (current < 0 && current % 2) { + updateClock(0, "#F00"); + } else if (current < 0) { + updateClock(0, "#000"); + } else { + updateClock(current, "#000"); + } +} + +function updateClock(timeleft, color = "#000") { + var minutes = Math.floor(timeleft / 60); + var seconds = timeleft % 60; + + var clock = document.getElementById("overlayClock"); + if (clock.ctx) { + clock.ctx.beginPath(); + clock.ctx.rect(0, 0, 230, 40); + clock.ctx.fillStyle = color; + clock.ctx.fill(); + clock.ctx.fillStyle = "#FFF"; + clock.ctx.textAlign = "center"; + clock.ctx.font = "50px monospace"; + clock.ctx.fillText(zpadTime(minutes) + ":" + zpadTime(seconds), 115, 37); + } else { + clock.innerHTML = zpadTime(minutes) + ":" + zpadTime(seconds); + clock.style.backgroundColor = color + "9"; + } +} +function popOutClock(clock) { + if (!clock.ctx) { + var canvas = document.createElement("canvas"); + canvas.width = "230"; + canvas.height = "40"; + var ctx = canvas.getContext("2d"); + clock.canvas = canvas; + clock.ctx = ctx; + + ctx.beginPath(); + ctx.rect(0, 0, 230, 40); + ctx.fillStyle = "#000"; + ctx.fill(); + ctx.fillStyle = "#FFF"; + ctx.font = "50px monospace"; + ctx.textAlign = "center"; + ctx.fillText(clock.innerHTML, 115, 37); + clock.video = document.createElement("video"); + clock.innerHTML = ""; + clock.appendChild(clock.video); + clock.video.onloadedmetadata = function () { + togglePictureInPicture(clock.video); + }; + + clock.video.srcObject = canvas.captureStream(); + clock.video.play(); + //clock.video.dataset.menu = "context-menu-clock"; + } else { + clock.innerHTML = ""; + clock.appendChild(clock.video); + clock.video.play(); + togglePictureInPicture(clock.video); + } +} + +session.popupChat = null +async function createPopoutChat() { + if (session.popupChat && !session.popupChat.closed) { + session.popupChat.focus(); + return; + } + + if (session.broadcastChannelID === false) { + session.broadcastChannelID = session.generateStreamID(8); + log(session.broadcastChannelID); + + session.broadcastChannel = new BroadcastChannel(session.broadcastChannelID); + session.broadcastChannel.onmessage = function (e) { + if ("loaded" in e.data) { + session.broadcastChannel.postMessage({ + messageList: messageList + }); + } else if ("msg" in e.data) { + sendChatMessage(e.data.msg, true); + } + }; + + session.broadcastChannel.onmessageerror = function (e) { + errorlog(e); + }; + } + + let params = { + broadcastChannelID: session.broadcastChannelID, + room: session.roomid || false, + view: session.view_set ? [...session.view_set, session.streamID].join(",") : (session.roomid ? false : session.streamID), + label: session.label || false, + password: session.password + }; + + function encrypt(text, key) { + const textEncoder = new TextEncoder(); + const encodedText = textEncoder.encode(text); + const encodedKey = textEncoder.encode(key); + + const encrypted = encodedText.map((byte, i) => + byte ^ encodedKey[i % encodedKey.length] + ); + + return btoa(String.fromCharCode.apply(null, encrypted)); + } + + async function generateSecureUrl(params) { + const ENCRYPTION_KEY = 'your32characterlongencryptionkey!!'; + + const filteredParams = Object.fromEntries( + Object.entries(params).filter(([_, v]) => v != null && v !== undefined) + ); + const paramsString = JSON.stringify(filteredParams); + const encrypted = encrypt(paramsString, ENCRYPTION_KEY); + return `./popout.html?id=${session.broadcastChannelID}&data=${encodeURIComponent(encrypted)}`; + } + + let srcString = await generateSecureUrl(params); + log(srcString); + + session.popupChat = window.open(srcString, "popup", "width=600,height=480,toolbar=no,menubar=no,resizable=yes"); + + session.popupChat.document.body.style.margin = "0"; + session.popupChat.document.body.style.backgroundColor = "#000"; + session.popupChat.document.body.style.padding = "0"; + session.popupChat.document.body.style.overflow = "hidden"; + session.popupChat.document.title = "Chat pop-out"; + + const style = session.popupChat.document.createElement('style'); + style.textContent = ` + @keyframes pulse { + 0% { background-color: #000; } + 50% { background-color: #333; } + 100% { background-color: #000; } + } + body { + animation: pulse 2s ease-in-out infinite; + } + `; + session.popupChat.document.head.appendChild(style); + + return false; +} + +function replaceURLs(message) { + if (!message) return; + var urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g; + return message.replace(urlRegex, function (url) { + url = url.replace(//g, ">").replace(/["']/g, ""); // try to sanitize things, just in case. + + var punc = ""; + while (url[url.length - 1] === ".") { + url = url.slice(0, -1); + punc += "."; + } + while (url[url.length - 1] === ";") { + url = url.slice(0, -1); + punc += ";"; + } + while (url[url.length - 1] === ",") { + url = url.slice(0, -1); + punc += ","; + } + while (url[url.length - 1] === "!") { + url = url.slice(0, -1); + punc += "!"; + } + while (url[url.length - 1] === ":") { + url = url.slice(0, -1); + punc += ":"; + } + while (url[url.length - 1] === "*") { + url = url.slice(0, -1); + punc += "*"; + } + while (url[url.length - 1] === ")") { + url = url.slice(0, -1); + punc += ")"; + } + while (url[url.length - 1] === "?") { + url = url.slice(0, -1); + punc += "?"; + } + + var hyperlink = url; + if (!hyperlink.match("^https?://")) { + hyperlink = "http://" + hyperlink; + } + if (url.length > 35) { + url = url.substring(0, 35) + "..."; + } + return '' + url + "" + punc; + }); +} + +function getChatMessage(msg, label = false, director = false, overlay = false, UUID = false) { + msg = sanitizeChat(msg); // keep it clean. + if (msg == "") { + return; + } + + data = {}; + data.time = Date.now(); + + var apiBlob = {}; + apiBlob.time = data.time; + apiBlob.msg = msg; + apiBlob.label = label; + apiBlob.type = "recv"; + + if (UUID !== false) { + apiBlob.UUID = UUID; + if (UUID in session.rpcs) { + apiBlob.streamID = session.rpcs[UUID].streamID || false; + } + } + + const streamFallbackLabel = (!label && session.director && UUID && session.rpcs[UUID] && session.rpcs[UUID].streamID) + ? getPeerDisplayName(UUID, false, false) + : false; + + if (label) { + label = sanitizeLabel(label); + } + + data.msg = msg; + if (label) { + data.label = label; + if (director) { + data.label = "" + data.label + ": "; + } else { + data.label = "" + data.label + ": "; + } + label = "" + label + ":"; // label+":"; + } else if (director) { + data.label = "Director: "; + label = "Director:"; + } else { + if (session.director) { + const fallback = streamFallbackLabel || "Someone"; + data.label = "" + fallback + ": "; + if (streamFallbackLabel) { + label = "" + fallback + ":"; + } else { + label = ""; + } + } else { + data.label = ""; + label = ""; + } + } + data.type = "recv"; + + if (overlay) { + if (!(session.cleanOutput && session.cleanish == false)) { + var textOverlay = getById("overlayMsgs"); + if (textOverlay) { + if (overlay == 2) { + // Clear previous persistent messages + while (textOverlay.querySelector('.persistent-overlay')) { + textOverlay.removeChild(textOverlay.querySelector('.persistent-overlay')); + } + var spanOverlay = document.createElement("span"); + spanOverlay.className = 'persistent-overlay'; + spanOverlay.innerHTML = "" + label + " " + msg + "
    "; + textOverlay.appendChild(spanOverlay); + textOverlay.style.display = "block"; + } else { + var spanOverlay = document.createElement("span"); + spanOverlay.innerHTML = "" + label + " " + msg + "
    "; + textOverlay.appendChild(spanOverlay); + textOverlay.style.display = "block"; + var showtime = msg.length * 200 + 3000; + if (showtime > 8000) { + showtime = 8000; + } + setTimeout( + function (ele) { + try { + ele.parentNode.removeChild(ele); + } catch (e) { } + }, + showtime, + spanOverlay + ); + } + } + } + } + + if (isIFrame) { + parent.postMessage( + { + gotChat: data, // deprecated + chat: data + }, + session.iframetarget + ); + } + + pokeAPI("chat", apiBlob); + + if (session.chatbutton === false) { + return; + } // messages can still appear as overlays ^ + + messageList.push(data); + messageList = messageList.slice(-100); + + if (session.beepToNotify) { + playtone(); + showNotification("new message", msg); + } + updateMessages(); + + if (session.chat == false) { + getById("chattoggle").className = "las la-comments toggleSize pulsate"; + getById("chatbutton").className = "float"; + + if (getById("chatNotification").value) { + getById("chatNotification").value = getById("chatNotification").value + 1; + } else { + getById("chatNotification").value = 1; + } + getById("chatNotification").classList.add("notification", "red"); + } + + if (session.broadcastChannel !== false) { + session.broadcastChannel.postMessage(data); /* send */ + } +} + +function rainbow(step, colours) { + var r, g, b; + var h = 1 - step / colours; + var i = ~~(h * 6); + var f = h * 6 - i; + var q = 1 - f; + switch (i % 6) { + case 0: + (r = 1), (g = f), (b = 0); + break; + case 1: + (r = q), (g = 1), (b = 0); + break; + case 2: + (r = 0), (g = 1), (b = f); + break; + case 3: + (r = 0), (g = q), (b = 1); + break; + case 4: + (r = f), (g = 0), (b = 1); + break; + case 5: + (r = 1), (g = 0), (b = q); + break; + } + var c = "#" + ("00" + (~~(r * 200 + 35)).toString(16)).slice(-2) + ("00" + (~~(g * 200 + 35)).toString(16)).slice(-2) + ("00" + (~~(b * 200 + 35)).toString(16)).slice(-2); + return c; +} + +function getColorFromName(str, colorseed = false, totalcolors = false) { + var out = 0, + len = str.length; + if (len > 6) { + len = 6; + } + + var seed = 26; + if (colorseed) { + seed = colorseed || 1; + } + + for (var pos = 0; pos < len; pos++) { + out += (str.charCodeAt(pos) - 64) * Math.pow(seed, len - pos - 1); + } + + var colours = 167772; + + if (totalcolors) { + colours = totalcolors; + if (colours > 167772) { + colours = 167772; + } else if (colours < 1) { + colours = 1; + } + } + + out = parseInt(out % colours); // get modulus + + if (colours === 1) { + return "#F00"; + } else if (colours === 2) { + switch (out) { + case 0: + return "#F00"; + case 1: + return "#00ABFA"; + } + } else if (colours === 3) { + switch (out) { + case 0: + return "#F00"; + case 1: + return "#00A800"; + case 2: + return "#00ABFA"; + } + } else if (colours === 4) { + switch (out) { + case 0: + return "#F00"; + case 1: + return "#FFA500"; + case 2: + return "#00A800"; + case 3: + return "#00ABFA"; + } + } else if (colours === 5) { + switch (out) { + case 0: + return "#F00"; + case 1: + return "#FFA500"; + case 2: + return "#00A800"; + case 3: + return "#00ABFA"; + case 4: + return "#FF39C5"; + } + } else { + out = rainbow(out, colours); + } + return out; +} + +function updateClosedCaptions(msg, label, UUID) { + + if (!session.rpcs[UUID].color && session.ccColored) { + session.rpcs[UUID].color = getColorFromName(UUID); + } + + msg.counter = parseInt(msg.counter); + var temp = document.createElement("div"); + temp.innerText = msg.transcript; + temp.innerText = temp.innerHTML; + var transcript = temp.textContent || temp.innerText || ""; + + if (transcript == "") { + return; + } + + transcript = transcript.charAt(0).toUpperCase() + transcript.slice(1); + //transcript = transcript.substr(-1, 5000); // keep it from being too long + + if (session.nocaptionlabels) { + label = ""; + } else if (label && !(session.view && !session.view_set)) { + label = sanitizeLabel(label); + label = "" + label + ": "; + } else { + label = ""; + } + + var textOverlay = getById("overlayMsgs"); + if (textOverlay) { + if (document.getElementById(UUID + "_" + msg.counter)) { + var spanOverlay = document.getElementById(UUID + "_" + msg.counter); + } else { + var spanOverlay = document.createElement("span"); + spanOverlay.id = UUID + "_" + msg.counter; + textOverlay.appendChild(spanOverlay); + textOverlay.style.height = "unset"; + textOverlay.style.textAlign = "left"; + textOverlay.style.display = "block"; + textOverlay.style.position = "fixed"; + textOverlay.style.bottom = "0"; + } + + spanOverlay.innerHTML = label + transcript + "
    "; + spanOverlay.style.fontSize = (parseInt(session.labelsize || 100) / 100.0) * 4.5 + "vh"; + spanOverlay.style.lineHeight = (parseInt(session.labelsize || 100) / 100) * 6 + "vh"; + spanOverlay.style.margin = (parseInt(session.labelsize || 100) / 100.0) * 0.75 + "vh"; + + if (session.rpcs[UUID].color && session.ccColored) { + spanOverlay.style.color = session.rpcs[UUID].color; + } + + if (msg.isFinal) { + var showtime = 3000; + clearTimeout(spanOverlay.timeout); + spanOverlay.timeout = setTimeout( + function (ele) { + ele.parentNode.removeChild(ele); + }, + showtime, + spanOverlay + ); + } else { + clearTimeout(spanOverlay.timeout); + spanOverlay.timeout = setTimeout( + function (ele) { + ele.parentNode.removeChild(ele); + }, + 30000, + spanOverlay + ); + } + } +} + +var chatUpdateTimeout = null; +function updateMessages() { + if (session.chatbutton === false) { + return; + } + + getById("chatNotification").classList.remove("notification", "red"); + if (session.chat) { + getById("chattoggle").classList.remove("pulsate"); + } + + const chatBody = document.getElementById("chatBody"); + chatBody.innerHTML = ""; + for (var i in messageList) { + var time = timeSince(messageList[i].time) || ""; + time = " - " + time + ""; + var msg = document.createElement("div"); + var message = replaceURLs(messageList[i].msg); + + if (messageList[i].type == "sent") { + msg.innerHTML = message + "" + time + ""; + msg.classList.add("outMessage"); + } else if (messageList[i].type == "recv" || messageList[i].type == "action") { + var label = ""; + if (messageList[i].label) { + label = messageList[i].label; + } + msg.innerHTML = label + message + "" + time + ""; + msg.classList.add("inMessage"); + } else if (messageList[i].type == "alert") { + msg.innerHTML = message + "" + time + ""; + msg.classList.add("inMessage"); + } else if (messageList[i].type == "tip") { + msg.innerHTML = message + "" + time + ""; + msg.classList.add("tipMessage"); + } else { + msg.innerHTML = message; + msg.classList.add("outMessage"); + } + + chatBody.appendChild(msg); + } + showDownloadLinks(); + for (var i in msgTransferList) { + var time = timeSince(msgTransferList[i].time) || ""; + time = " - " + time + ""; + + var msg = document.createElement("div"); + if ("idx" in msgTransferList[i]) { + msg.id = "transfer_" + msgTransferList[i].idx; + msg.classList.add("transfer"); + } + if (msgTransferList[i].type == "sent") { + msg.innerHTML = msgTransferList[i].msg + "" + time + ""; + msg.classList.add("outMessage"); + } else if (msgTransferList[i].type == "recv" || msgTransferList[i].type == "action") { + var label = ""; + if (msgTransferList[i].label) { + label = msgTransferList[i].label; + } + msg.innerHTML = label + msgTransferList[i].msg + "" + time + ""; + msg.classList.add("inMessage"); + } else if (msgTransferList[i].type == "alert") { + msg.innerHTML = msgTransferList[i].msg + "" + time + ""; + msg.classList.add("inMessage"); + } else { + msg.innerHTML = msgTransferList[i].msg; + msg.classList.add("outMessage"); + } + + if (msg.id && document.getElementById(msg.id)) { + document.getElementById(msg.id).innerHTML = msg.innerHTML; + } else { + chatBody.appendChild(msg); + } + } + if (chatUpdateTimeout) { + clearInterval(chatUpdateTimeout); + } + chatBody.scrollTop = chatBody.scrollHeight; + if (chatUpdateTimeout) { + clearTimeout(chatUpdateTimeout); + } + chatUpdateTimeout = setTimeout(updateMessages, 60000); +} + +function EnterButtonChat(event) { + // Number 13 is the "Enter" key on the keyboard + var key = event.which || event.keyCode; + if (key === 13) { + // Cancel the default action, if needed + event.preventDefault(); + // Trigger the button element with a click + sendChatMessage(); + } +} + +function showCustomizer(arg, ele) { + //getById("directorLinksButton").innerHTML=' LINKS (GUEST INVITES & SCENES)' + getById("showCustomizerButton1").style.backgroundColor = ""; + getById("showCustomizerButton2").style.backgroundColor = ""; + getById("showCustomizerButton3").style.backgroundColor = ""; + getById("showCustomizerButton4").style.backgroundColor = ""; + getById("showCustomizerButton1").style.boxShadow = ""; + getById("showCustomizerButton2").style.boxShadow = ""; + getById("showCustomizerButton3").style.boxShadow = ""; + getById("showCustomizerButton4").style.boxShadow = ""; + + if (getById("customizeLinks" + arg).style.display != "none") { + getById("customizeLinks").style.display = "none"; + getById("customizeLinks" + arg).style.display = "none"; + } else { + //directorLinks").style.display="none"; + getById("showCustomizerButton" + arg).style.backgroundColor = "#1e0000"; + getById("showCustomizerButton" + arg).style.boxShadow = "inset 0px 0px 1px #b90000"; + getById("customizeLinks1").style.display = "none"; + getById("customizeLinks3").style.display = "none"; + getById("customizeLinks").style.display = "block"; + getById("customizeLinks" + arg).style.display = "block"; + } +} + +function setPTTvalue() { + var key = ""; + if (PPTHotkey.ctrl) { + key += "Control"; + } + if (PPTHotkey.meta) { + if (key) { + key += " + "; + } + key += "Meta"; + } + if (PPTHotkey.alt) { + if (key) { + key += " + "; + } + key += "Alt"; + } + + if (PPTHotkey.key == "Control") { + // + } else if (PPTHotkey.key == "Alt") { + // + } else if (PPTHotkey.key == "Meta") { + // + } else if (PPTHotkey.key !== false) { + if (key) { + key += " + "; + } + if (PPTHotkey.key === " ") { + key += "Space"; + } else { + key += PPTHotkey.key; + } + } else if (key && navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + getById("pptHotKey").title = "Note: Global hot-keys can't simply be Control, Alt, or Meta keys."; + } + getById("pptHotKey").value = key; + + try { + if (window.electronApi && window.electronApi.updatePPT) { + window.electronApi.updatePPT(PPTHotkey); + } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + if (!ipcRenderer) { + ipcRenderer = require("electron").ipcRenderer; + } + if (ipcRenderer) { + ipcRenderer.send("PPTHotkey", PPTHotkey); + } + } + } catch (e) { + errorlog(e); + } +} + +var PPTHotkey = getStorage("PPTHotkey") || false; +if (PPTHotkey) { + setPTTvalue(); +} + +function setHotKeyAuto(hotkeyInput) { + PPTHotkey = { + ctrl: false, + alt: false, + meta: false, + key: false + }; + + var key = ""; + + if (hotkeyInput) { + const modifiers = hotkeyInput.replaceAll(" ", "+").split("+"); + modifiers.forEach(modifier => { + const trimmedModifier = modifier.trim().toLowerCase(); + if (trimmedModifier === "control") { + PPTHotkey.ctrl = true; + key += "Control"; + } else if (trimmedModifier === "ctrl") { + PPTHotkey.ctrl = true; + key += "Control"; + } else if (trimmedModifier === "alt") { + PPTHotkey.alt = true; + key += "Alt"; + } else if (trimmedModifier === "meta") { + PPTHotkey.meta = true; + key += "Meta"; + } + }); + var lastKey = modifiers.pop().trim(); + PPTHotkey.key = lastKey; + + if (lastKey || lastKey === " " || lastKey === 0) { + if (key) { + key += " + "; + } + if (lastKey === " ") { + key += "Space"; + } else { + key += lastKey; + } + } + } else { + PPTHotkey.ctrl = true; + PPTHotkey.key = "m"; + PPTHotkey.meta = true; + key = "Control + Alt + m"; + } + + setStorage("PPTHotkey", PPTHotkey, 99999); + getById("pptHotKey").value = key; + + try { + if (window.electronApi && window.electronApi.updatePPT) { + window.electronApi.updatePPT(PPTHotkey); + } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + if (!ipcRenderer) { + ipcRenderer = require("electron").ipcRenderer; + } + if (ipcRenderer) { + ipcRenderer.send("PPTHotkey", PPTHotkey); + } + } + } catch (e) { + errorlog(e); + } +} + +function setHotKey(keyinput = true) { + if (!keyinput) { + // clears if false + getById("pptHotKey").value = ""; + getById("pptHotKey0").value = ""; + PPTHotkey = false; + removeStorage("PPTHotkey"); + + try { + if (window.electronApi && window.electronApi.updatePPT) { + window.electronApi.updatePPT(PPTHotkey); + } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + if (!ipcRenderer) { + ipcRenderer = require("electron").ipcRenderer; + } + if (ipcRenderer) { + ipcRenderer.send("PPTHotkey", PPTHotkey); + } + } + } catch (e) { + errorlog(e); + } + + return; + } + + PPTHotkey = { + ctrl: false, + alt: false, + meta: false, + key: false + }; + + log(event); + var key = ""; + if (event.ctrlKey) { + key += "Control"; + PPTHotkey.ctrl = true; + } + if (event.metaKey) { + if (key) { + key += " + "; + } + key += "Meta"; + PPTHotkey.meta = true; + } + if (event.altKey) { + if (key) { + key += " + "; + } + key += "Alt"; + PPTHotkey.alt = true; + } + + if (event.key == "Control") { + // + } else if (event.key == "Alt") { + // + } else if (event.key == "Meta") { + // + } else if (event.key || event.key === " " || event.key === 0) { + if (key) { + key += " + "; + } + if (event.key === " ") { + key += "Space"; + } else { + key += event.key; + } + PPTHotkey.key = event.key; + } + setStorage("PPTHotkey", PPTHotkey, 99999); + event.target.value = key; + + try { + if (window.electronApi && window.electronApi.updatePPT) { + window.electronApi.updatePPT(PPTHotkey); + } else if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + if (!ipcRenderer) { + ipcRenderer = require("electron").ipcRenderer; + } + if (ipcRenderer) { + ipcRenderer.send("PPTHotkey", PPTHotkey); + } + } + } catch (e) { + errorlog(e); + } + + getById("pptHotKey").value = event.target.value; + getById("pptHotKey0").value = event.target.value; + + event.preventDefault(); + event.stopPropagation(); + return false; +} + +function setupGoogleDriveUploader(filename = false, sessionUri = false) { + if (!session.gdrive) { + session.gdrive = {}; + session.gdrive.accessToken = false; + } + + var gdrive = {}; + var uploading = false; + var tokenClient; + var isInitialized = false; + var initializationPromise; + + const SCOPES = "https://www.googleapis.com/auth/drive.file"; + + var totalChunksRecorded = 0; + var totalChunksUploaded = 0; + var currentByte = 0; + var chunks = new Blob([]); + var finalized = false; + + gdrive.promise = false; + gdrive.sessionUri = sessionUri; + + // Create an initialization promise to track when everything is ready + initializationPromise = new Promise((resolve, reject) => { + // We'll resolve this when the token client is fully initialized + if (!gdrive.sessionUri) { + loadScript("https://accounts.google.com/gsi/client", function () { + log("Google Identity Services loaded"); + initTokenClient(); + resolve(); + }); + } else { + resolve(); + } + }); + + // Setup the authentication promise + if (!filename && !sessionUri) { + var res, rej; + gdrive.promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + gdrive.promise.resolve = res; + gdrive.promise.reject = rej; + } + + gdrive.startResumableUpload = async function (fname, retry = true) { + console.log("startResumableUpload", retry); + + const fileMetadata = { name: fname }; + + if (session.GDRIVE_FOLDERNAME) { + let folderId = null; + + const query = `name = '${session.GDRIVE_FOLDERNAME}' and mimeType = 'application/vnd.google-apps.folder' and 'root' in parents and trashed = false`; + const url = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}`; + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: "Bearer " + session.gdrive.accessToken + } + }); + + const result = await response.json(); + + if (result.files && result.files.length > 0) { + folderId = result.files[0].id; + } + + if (!folderId) { + log("creating new folder as folder not found."); + try { + const folderMetadata = { + name: session.GDRIVE_FOLDERNAME, + mimeType: "application/vnd.google-apps.folder" + }; + + const createResponse = await fetch("https://www.googleapis.com/drive/v3/files", { + method: "POST", + headers: { + Authorization: "Bearer " + session.gdrive.accessToken, + "Content-Type": "application/json" + }, + body: JSON.stringify(folderMetadata) + }); + + const createResult = await createResponse.json(); + folderId = createResult.id; + } catch (e) { + errorlog(e); + } + } + + if (folderId) { + fileMetadata.parents = [folderId]; + } + } + + const metadata = new Blob([JSON.stringify(fileMetadata)], { type: "application/json" }); + try { + var response = await fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable", { + method: "POST", + headers: { + Authorization: "Bearer " + session.gdrive.accessToken, + "Content-Type": "application/json; charset=UTF-8" + }, + body: metadata + }); + if (!response.ok) { + if (!session.cleanOutput) { + warnUser("⚠️ Error: Failed to configure the Google Drive upload."); + } + throw new Error("Start resumable upload failed: " + response.statusText); + } + return response.headers.get("Location"); // This is the session URI for the resumable upload + } catch (err) { + errorlog(err); + try { + if (retry) { + session.gdrive.accessToken = false; + var res, rej; + gdrive.promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + gdrive.promise.resolve = res; + gdrive.promise.reject = rej; + filename = false; + + // Make sure we're initialized before requesting token + await gdrive.ensureInitialized(); + tokenClient.requestAccessToken(); + await gdrive.promise; + + if (session.gdrive.accessToken) { + return await gdrive.startResumableUpload(fname, false); + } else { + return false; + } + } + } catch (err2) { + errorlog(err2); + return false; + } + } + }; + + function initTokenClient() { + console.log("Initializing GIS token client"); + if ( + typeof google === "undefined" || + !google.accounts || + !google.accounts.oauth2 + ) { + const gisError = new Error("Google Identity Services failed to load."); + errorlog(gisError); + if (!session.cleanOutput) { + warnUser("Google sign-in was blocked. Allow accounts.google.com and try again.", 8000); + } + if (gdrive.promise && gdrive.promise.reject) { + gdrive.promise.reject(gisError); + } + return; + } + tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: session.GDRIVE_CLIENT_ID, + scope: SCOPES, + callback: onTokenResponse, + error_callback: onTokenError + }); + + isInitialized = true; + + // If we have no promise yet but the user requested access, set one up + if (!gdrive.promise && !sessionUri && !filename) { + var res, rej; + gdrive.promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + gdrive.promise.resolve = res; + gdrive.promise.reject = rej; + } + + // If we have a filename, start upload immediately when possible + if (filename) { + setTimeout(async () => { + try { + if (session.gdrive && session.gdrive.accessToken) { + console.log("Using cached Google Drive token for upload"); + gdrive.sessionUri = await gdrive.startResumableUpload(filename); + if (gdrive.sessionUri) { + uploadLoop(); + return; + } + console.warn("Failed to reuse cached Drive token, requesting a new one..."); + } + } catch (err) { + errorlog(err); + } + console.log("Requesting access token for immediate upload"); + tokenClient.requestAccessToken(); + }, 250); // Small delay to ensure tokenClient is fully initialized + } + } + + function onTokenError(response) { + console.warn("Token error:", response); + if (gdrive.promise && gdrive.promise.reject) { + gdrive.promise.reject(response); + } + } + + async function onTokenResponse(tokenResponse) { + console.log("Token response received", tokenResponse); + + if (tokenResponse.error === "popup_closed_by_user" || tokenResponse.error === "access_denied") { + errorlog("User cancelled the sign-in process."); + if (gdrive.promise && gdrive.promise.reject) { + gdrive.promise.reject(new Error("User cancelled authentication")); + } + } else if (tokenResponse.error !== undefined) { + errorlog("Token error: " + tokenResponse.error); + if (gdrive.promise && gdrive.promise.reject) { + gdrive.promise.reject(new Error(tokenResponse.error)); + } + } else { + // Successfully got access token + console.log("Access token obtained successfully"); + session.gdrive.accessToken = tokenResponse.access_token; + + if (filename) { + try { + gdrive.sessionUri = await gdrive.startResumableUpload(filename); + console.log("Session URI:", gdrive.sessionUri); + uploadLoop(); + } catch (e) { + console.error("Error starting upload:", e); + if (gdrive.promise && gdrive.promise.reject) { + gdrive.promise.reject(e); + } + return; + } + } + + // Always resolve the promise if we got a token successfully + if (gdrive.promise && gdrive.promise.resolve) { + console.log("Resolving promise with access token"); + gdrive.promise.resolve(tokenResponse.access_token); + } + } + } + + // Check if initialized and wait if not + gdrive.ensureInitialized = async function () { + if (!isInitialized) { + console.log("Waiting for initialization to complete..."); + await initializationPromise; + console.log("Initialization complete"); + } + }; + + // Function to manually request access token + gdrive.requestAccessToken = async function () { + await gdrive.ensureInitialized(); + + if (tokenClient) { + console.log("Manually requesting access token"); + tokenClient.requestAccessToken(); + } else { + console.error("Token client not initialized"); + if (gdrive.promise && gdrive.promise.reject) { + gdrive.promise.reject(new Error("Token client not initialized")); + } + } + }; + + gdrive.revokeToken = function () { + if (session.gdrive.accessToken) { + google.accounts.oauth2.revoke(session.gdrive.accessToken, () => { + console.log('Access token revoked'); + session.gdrive.accessToken = false; + }); + } + }; + + /// the following doesn't need to be signed in; just access to the gdrive.sessionUri URL + + gdrive.addChunk = function (chunk) { + if (chunk && chunks) { + totalChunksRecorded += chunk.size; + chunks = new Blob([chunks, chunk], { type: chunk.type }); + if (!session.cleanOutput) { + getById("progressContainer").classList.remove("hidden"); + } + updateProgressBar(); + } else if (chunk === false) { + finalized = true; + } + uploadLoop(); + }; + + async function uploadLoop() { + if (uploading || !gdrive.sessionUri) { + return; + } + uploading = true; + while (chunks && (finalized || chunks.size > 256 * 1024)) { + if (finalized) { + var chunk = chunks.slice(0, chunks.size); + let res = await finalizeUpload(chunk); + log(res); + return; + } else { + var chunkSize = Math.floor(chunks.size / (256 * 1024)) * (256 * 1024); + var chunk = chunks.slice(0, chunkSize); + chunks = chunks.slice(chunkSize); + } + currentByte = await uploadChunk(chunk); + } + uploading = false; + } + + async function uploadChunk(chunk) { + const endByte = currentByte + chunk.size - 1; + totalChunksUploaded += chunk.size; + const headers = new Headers({ + "Content-Range": `bytes ${currentByte}-${endByte}/*` + }); + const response = await fetch(gdrive.sessionUri, { + method: "PUT", + headers: headers, + body: chunk + }); + if (!response.ok && response.status !== 308) { + throw new Error(`Failed to upload chunk: ${response.statusText}`); + } + updateProgressBar(); + return endByte + 1; + } + + async function finalizeUpload(chunk) { + const endByte = currentByte + chunk.size - 1; + const headers = new Headers({ + "Content-Range": `bytes ${currentByte}-${endByte}/${endByte + 1}` + }); + const response = await fetch(gdrive.sessionUri, { + method: "PUT", + headers: headers, + body: chunk + }); + if (chunk) { + totalChunksUploaded += chunk.size; + } + updateProgressBar(2); + + return response.json(); + } + + function updateProgressBar(state = 0) { + // Implementation unchanged + if (state == 2) { + setTimeout(function () { + if (getById("progressBar").style.width == "100%") { + getById("progressContainer").classList.add("hidden"); + } + }, 1000); + getById("progressBar").style.width = "100%"; + var msg = {}; + msg.gdrive = { up: parseInt(totalChunksUploaded / 1024), rec: parseInt(totalChunksUploaded / 1024), state: state }; + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + } else if (totalChunksRecorded > 0) { + var progressPercentage = (totalChunksUploaded / (totalChunksRecorded || 1)) * 100; + var bytesLeft = parseInt((totalChunksRecorded - totalChunksUploaded) / 1024); + getById("progressBar").style.width = progressPercentage + "%"; + getById("progressBar").innerHTML = "Upload progress to Google Drive: " + progressPercentage.toFixed(2) + "%, with " + convertKilobytes(bytesLeft) + " left"; + + var msg = {}; + msg.gdrive = { up: parseInt(totalChunksUploaded / 1024), rec: parseInt(totalChunksRecorded / 1024), state: state }; + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + } + } + + return gdrive; +} + +function convertKilobytes(kilobytes) { + const KB_IN_MB = 1024; + const KB_IN_GB = 1024 * 1024; + + if (kilobytes >= KB_IN_GB) { + return Math.ceil(kilobytes / KB_IN_GB).toFixed(0) + " GB"; + } else if (kilobytes >= KB_IN_MB) { + return Math.ceil(kilobytes / KB_IN_MB).toFixed(0) + " MB"; + } else { + return kilobytes + "KB"; + } +} + +const DROPBOX_APP_KEY_FALLBACK = "uwxixfldkii1xpt"; +const DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize"; +const DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token"; +const DROPBOX_SCOPES = "files.content.write files.metadata.write"; +const DROPBOX_SDK_URL = "https://cdnjs.cloudflare.com/ajax/libs/dropbox.js/10.34.0/Dropbox-sdk.min.js"; +const DROPBOX_OAUTH_STORAGE_KEY = "dropboxOAuthTokens"; +const DROPBOX_OAUTH_SESSION_KEY = "dropboxOAuthSession"; +const DROPBOX_AUTH_MESSAGE_SOURCE = "vdoninja-dropbox-auth"; +const DROPBOX_ALLOWED_REDIRECT_ORIGINS = [ + "https://vdo.ninja", + "https://dev.versus.cam", + "https://versus.cam", + "https://backup.vdo.ninja", + "https://obs.ninja", + "http://localhost:8080" +]; +const DROPBOX_REFRESH_SKEW_MS = 120000; +var dropboxScriptPromise = null; +var dropboxInitPromise = null; +var dropboxAuthFlowPromise = null; +var dropboxAuthFlowResolver = null; +var dropboxAuthFlowRejecter = null; +var dropboxAuthWindow = null; +var dropboxAuthWindowMonitor = null; + +function getDropboxAppKey() { + if (typeof session !== "undefined" && session.DROPBOX_APP_KEY) { + return session.DROPBOX_APP_KEY; + } + return DROPBOX_APP_KEY_FALLBACK; +} + +function getDropboxRedirectUri() { + if (typeof window === "undefined" || !window.location || !window.location.origin) { + return DROPBOX_ALLOWED_REDIRECT_ORIGINS[0] + "/dropbox-auth.html"; + } + var origin = window.location.origin.replace(/\/+$/, ""); + if (DROPBOX_ALLOWED_REDIRECT_ORIGINS.indexOf(origin) === -1) { + return DROPBOX_ALLOWED_REDIRECT_ORIGINS[0] + "/dropbox-auth.html"; + } + return origin + "/dropbox-auth.html"; +} + +function persistDropboxAuthSession(sessionData) { + try { + localStorage.setItem(DROPBOX_OAUTH_SESSION_KEY, JSON.stringify(sessionData)); + } catch (e) { } +} + +function clearDropboxAuthSession() { + try { + localStorage.removeItem(DROPBOX_OAUTH_SESSION_KEY); + } catch (e) { } +} + +function getStoredDropboxOAuthTokens() { + if (typeof session !== "undefined" && session.dropboxOAuth) { + return session.dropboxOAuth; + } + try { + var raw = localStorage.getItem(DROPBOX_OAUTH_STORAGE_KEY); + if (!raw) { + return null; + } + var parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + if (typeof session !== "undefined") { + session.dropboxOAuth = parsed; + if (parsed.accessToken) { + session.dropboxAccessToken = parsed.accessToken; + } + } + return parsed; + } + } catch (e) { } + return null; +} + +function persistDropboxOAuthTokens(record) { + if (!record || !record.accessToken) { + return; + } + var existing = getStoredDropboxOAuthTokens(); + var normalized = { + accessToken: record.accessToken, + refreshToken: record.refreshToken || (existing && existing.refreshToken) || null, + expiresAt: record.expiresAt || (existing && existing.expiresAt) || 0, + scope: record.scope || (existing && existing.scope) || DROPBOX_SCOPES, + tokenType: record.tokenType || (existing && existing.tokenType) || "bearer" + }; + try { + localStorage.setItem(DROPBOX_OAUTH_STORAGE_KEY, JSON.stringify(normalized)); + } catch (e) { } + if (typeof session !== "undefined") { + session.dropboxOAuth = normalized; + session.dropboxAccessToken = normalized.accessToken; + } +} + +function clearDropboxOAuthTokens() { + if (typeof session !== "undefined") { + session.dropboxOAuth = null; + } + try { + localStorage.removeItem(DROPBOX_OAUTH_STORAGE_KEY); + } catch (e) { } +} + +function normalizeDropboxTokenResponse(response, fallbackRefreshToken = null) { + if (!response || typeof response !== "object" || !response.access_token) { + return null; + } + var expiresIn = 0; + if (response.expires_in) { + var parsed = parseInt(response.expires_in, 10); + if (!isNaN(parsed) && parsed > 0) { + expiresIn = parsed * 1000; + } + } + var expiresAt = expiresIn ? Date.now() + Math.max(0, expiresIn - DROPBOX_REFRESH_SKEW_MS) : 0; + return { + accessToken: response.access_token, + refreshToken: response.refresh_token || fallbackRefreshToken || null, + expiresAt: expiresAt, + scope: response.scope || DROPBOX_SCOPES, + tokenType: response.token_type || "bearer" + }; +} + +function dropboxTokenExpired(tokens) { + if (!tokens || !tokens.accessToken) { + return true; + } + if (!tokens.expiresAt) { + return false; + } + return Date.now() >= tokens.expiresAt; +} + +async function refreshDropboxAccessToken(existingTokens) { + if (!existingTokens || !existingTokens.refreshToken) { + throw new Error("Dropbox refresh token missing."); + } + var body = new URLSearchParams(); + body.set("grant_type", "refresh_token"); + body.set("refresh_token", existingTokens.refreshToken); + body.set("client_id", getDropboxAppKey()); + var response = await fetch(DROPBOX_TOKEN_URL, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: body.toString() + }); + if (!response.ok) { + throw new Error("Failed to refresh Dropbox token."); + } + var data = await response.json(); + var normalized = normalizeDropboxTokenResponse(data, existingTokens.refreshToken); + if (!normalized) { + throw new Error("Dropbox refresh response invalid."); + } + persistDropboxOAuthTokens(normalized); + return normalized; +} + +async function ensureDropboxOAuthAccessToken({ interactive = false } = {}) { + var tokens = getStoredDropboxOAuthTokens(); + if (tokens && !dropboxTokenExpired(tokens)) { + return tokens; + } + if (tokens && tokens.refreshToken) { + try { + return await refreshDropboxAccessToken(tokens); + } catch (e) { + errorlog(e); + clearDropboxOAuthTokens(); + clearDropboxAuthSession(); + tokens = null; + } + } + if (!interactive) { + return null; + } + return beginDropboxOAuthFlow(); +} + +function generateRandomString(length = 64) { + var charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + var result = ""; + if (typeof window !== "undefined" && window.crypto && window.crypto.getRandomValues) { + var values = new Uint32Array(length); + window.crypto.getRandomValues(values); + for (var i = 0; i < length; i++) { + result += charset[values[i] % charset.length]; + } + } else { + for (var j = 0; j < length; j++) { + result += charset[Math.floor(Math.random() * charset.length)]; + } + } + return result; +} + +function base64UrlEncode(arrayBuffer) { + var bytes = new Uint8Array(arrayBuffer); + var binary = ""; + for (var i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +async function createDropboxPkcePair() { + var verifier = generateRandomString(64); + if (typeof window !== "undefined" && window.crypto && window.crypto.subtle && typeof TextEncoder !== "undefined") { + try { + var data = new TextEncoder().encode(verifier); + var digest = await window.crypto.subtle.digest("SHA-256", data); + return { verifier: verifier, challenge: base64UrlEncode(digest), method: "S256" }; + } catch (e) { } + } + return { verifier: verifier, challenge: verifier, method: "plain" }; +} + +function generateDropboxState() { + return generateRandomString(32); +} + +async function beginDropboxOAuthFlow() { + if (dropboxAuthFlowPromise) { + return dropboxAuthFlowPromise; + } + dropboxAuthFlowPromise = new Promise(async (resolve, reject) => { + dropboxAuthFlowResolver = resolve; + dropboxAuthFlowRejecter = reject; + try { + var pkce = await createDropboxPkcePair(); + var state = generateDropboxState(); + var redirectUri = getDropboxRedirectUri(); + var clientId = getDropboxAppKey(); + clearDropboxAuthSession(); + persistDropboxAuthSession({ + verifier: pkce.verifier, + state: state, + redirectUri: redirectUri, + clientId: clientId, + scope: DROPBOX_SCOPES, + origin: typeof window !== "undefined" && window.location ? window.location.origin : "", + ts: Date.now() + }); + var params = new URLSearchParams({ + response_type: "code", + client_id: clientId, + redirect_uri: redirectUri, + code_challenge: pkce.challenge, + code_challenge_method: pkce.method, + token_access_type: "offline", + state: state + }); + if (DROPBOX_SCOPES) { + params.set("scope", DROPBOX_SCOPES); + } + var authUrl = DROPBOX_AUTH_URL + "?" + params.toString(); + dropboxAuthWindow = window.open(authUrl, "vdoninja-dropbox-auth", "width=600,height=720"); + if (!dropboxAuthWindow) { + throw new Error("Dropbox authorization popup was blocked. Allow popups and try again."); + } + dropboxAuthWindowMonitor = setInterval(() => { + if (!dropboxAuthWindow || dropboxAuthWindow.closed) { + cleanupDropboxAuthFlow(new Error("Dropbox authorization window was closed."), true); + } + }, 750); + } catch (err) { + cleanupDropboxAuthFlow(err, true); + } + }); + return dropboxAuthFlowPromise; +} + +function cleanupDropboxAuthFlow(result, isError) { + if (dropboxAuthWindowMonitor) { + clearInterval(dropboxAuthWindowMonitor); + dropboxAuthWindowMonitor = null; + } + if (dropboxAuthWindow && !dropboxAuthWindow.closed) { + try { + dropboxAuthWindow.close(); + } catch (e) { } + } + dropboxAuthWindow = null; + var resolver = dropboxAuthFlowResolver; + var rejecter = dropboxAuthFlowRejecter; + dropboxAuthFlowResolver = null; + dropboxAuthFlowRejecter = null; + dropboxAuthFlowPromise = null; + if (isError) { + if (typeof rejecter === "function") { + rejecter(result instanceof Error ? result : new Error(result || "Dropbox authorization failed")); + } + } else if (typeof resolver === "function") { + resolver(result); + } +} + +function isAllowedDropboxAuthOrigin(origin) { + if (!origin) { + return false; + } + if (origin === window.location.origin) { + return true; + } + return DROPBOX_ALLOWED_REDIRECT_ORIGINS.indexOf(origin) !== -1; +} + +function dropboxAuthMessageHandler(event) { + if (!event || !event.data || !isAllowedDropboxAuthOrigin(event.origin)) { + return; + } + var data = event.data; + if (!data || data.source !== DROPBOX_AUTH_MESSAGE_SOURCE) { + return; + } + if (data.type === "request-session" && event.source && typeof event.source.postMessage === "function") { + var sessionPayload = null; + try { + var rawSession = localStorage.getItem(DROPBOX_OAUTH_SESSION_KEY); + if (rawSession) { + sessionPayload = JSON.parse(rawSession); + } + } catch (e) { + sessionPayload = null; + } + if (sessionPayload && data.state && sessionPayload.state && sessionPayload.state !== data.state) { + sessionPayload = null; + } + try { + event.source.postMessage({ source: DROPBOX_AUTH_MESSAGE_SOURCE, type: "session", session: sessionPayload }, event.origin); + } catch (e) { } + return; + } + if (data.type === "tokens" && data.tokens) { + persistDropboxOAuthTokens(data.tokens); + clearDropboxAuthSession(); + cleanupDropboxAuthFlow(data.tokens, false); + } else if (data.type === "error") { + if (data.clearTokens) { + clearDropboxOAuthTokens(); + } + clearDropboxAuthSession(); + cleanupDropboxAuthFlow(new Error(data.message || "Dropbox authorization failed"), true); + } +} + function streamSaverMessageHandler(event) { if (!event || !event.data || !event.data.streamSaverError) { return; @@ -49099,5978 +49564,5978 @@ if (typeof window !== "undefined") { window.addEventListener("message", streamSaverMessageHandler, false); } - -function getStoredDropboxToken() { - try { - return localStorage.getItem("dropboxAccessToken") || null; - } catch (e) { - return null; - } -} - -function persistDropboxToken(token) { - if (!token) { - return; - } - try { - localStorage.setItem("dropboxAccessToken", token); - } catch (e) { } -} - -function clearDropboxAuthState({ clearToken = false, clearOAuth = false } = {}) { - if (typeof session !== "undefined") { - session.dbx = false; - if (clearToken) { - session.dropboxAccessToken = null; - try { - localStorage.removeItem("dropboxAccessToken"); - } catch (e) { } - } - } - if (clearOAuth) { - if (typeof session !== "undefined") { - session.dropboxAccessToken = null; - } - clearDropboxOAuthTokens(); - clearDropboxAuthSession(); - } - try { - localStorage.removeItem("dropboxSession"); - } catch (e) { } -} - -function dropboxErrorSuggestsReauth(error) { - if (!error) { - return false; - } - var summary = ""; - if (error.error && error.error.error_summary) { - summary = error.error.error_summary; - } else if (error.error_summary) { - summary = error.error_summary; - } - if (summary && (summary.indexOf("expired_access_token") !== -1 || summary.indexOf("invalid_access_token") !== -1)) { - return true; - } - var status = error.status || (error.error && error.error.status) || false; - if (status && parseInt(status) === 401) { - return true; - } - return false; -} - -function ensureDropboxSDKLoaded() { - if (window.Dropbox && window.Dropbox.Dropbox) { - return Promise.resolve(); - } - if (dropboxScriptPromise) { - return dropboxScriptPromise; - } - dropboxScriptPromise = new Promise((resolve, reject) => { - var existing = document.querySelector("script[src='" + DROPBOX_SDK_URL + "']"); - if (existing) { - existing.addEventListener("load", () => resolve(), { once: true }); - existing.addEventListener("error", () => reject(new Error("Failed to load Dropbox SDK")), { once: true }); - } else { - var script = document.createElement("script"); - script.type = "text/javascript"; - script.src = DROPBOX_SDK_URL; - script.onload = () => resolve(); - script.onerror = () => reject(new Error("Failed to load Dropbox SDK")); - document.head.appendChild(script); - } - }).catch(error => { - dropboxScriptPromise = null; - throw error; - }); - return dropboxScriptPromise; -} - -async function setupDropbox(accessToken = null, options = {}) { - if (typeof session === "undefined") { - return null; - } - var opts = typeof options === "object" && options !== null ? options : {}; - var interactive = opts.interactive === true; - var forceReauth = opts.forceReauth === true; - var token = null; - var manualToken = null; - if (typeof accessToken === "string" && accessToken.trim().length) { - manualToken = accessToken.trim(); - token = manualToken; - } - if (forceReauth && manualToken) { - forceReauth = false; - } - if (forceReauth) { - clearDropboxOAuthTokens(); - if (typeof session !== "undefined") { - session.dropboxOAuth = null; - } - } - var preferOAuth = !manualToken && (forceReauth || (interactive && Boolean(session.dropboxOAuth || getStoredDropboxOAuthTokens()))); - var oauthTokens = null; - if (!forceReauth) { - oauthTokens = session.dropboxOAuth || getStoredDropboxOAuthTokens(); - } - if (oauthTokens && dropboxTokenExpired(oauthTokens)) { - try { - oauthTokens = await refreshDropboxAccessToken(oauthTokens); - } catch (e) { - errorlog(e); - clearDropboxOAuthTokens(); - oauthTokens = null; - } - } - if (oauthTokens && oauthTokens.accessToken) { - session.dropboxOAuth = oauthTokens; - } - if (!token && oauthTokens && oauthTokens.accessToken) { - token = oauthTokens.accessToken; - } - if (!token) { - var legacySessionToken = session.dropboxAccessToken || null; - if (legacySessionToken && (!preferOAuth || (oauthTokens && oauthTokens.accessToken === legacySessionToken))) { - token = legacySessionToken; - } - } - if (!token && !preferOAuth) { - var paramToken = typeof urlParams !== "undefined" && urlParams.get("dropbox"); - token = paramToken || getStoredDropboxToken(); - } - if (forceReauth) { - token = null; - } - if (!token) { - try { - var oauthResponse = await ensureDropboxOAuthAccessToken({ interactive: interactive || preferOAuth }); - if (oauthResponse && oauthResponse.accessToken) { - token = oauthResponse.accessToken; - } - } catch (e) { - throw e; - } - } - if (!token) { - return null; - } - if (!forceReauth && session.dbx && session.dropboxAccessToken === token) { - return session.dbx; - } - session.dropboxAccessToken = token; - if (manualToken && opts.persist !== false) { - persistDropboxToken(token); - } - if (dropboxInitPromise) { - return dropboxInitPromise; - } - dropboxInitPromise = (async currentToken => { - await ensureDropboxSDKLoaded(); - if (!window.Dropbox || !window.Dropbox.Dropbox) { - throw new Error("Dropbox SDK unavailable"); - } - var client = new Dropbox.Dropbox({ accessToken: currentToken }); - session.dbx = client; - session.dropboxAccessToken = currentToken; - resumeDropbox(); - return client; - })(token) - .catch(error => { - var needsReauth = dropboxErrorSuggestsReauth(error); - clearDropboxAuthState({ clearToken: needsReauth, clearOAuth: needsReauth }); - throw error; - }) - .finally(() => { - dropboxInitPromise = null; - }); - return dropboxInitPromise; -} - -if (typeof window !== "undefined") { - window.setupDropbox = setupDropbox; -} - -function resumeDropbox() { - var sessionData = localStorage.getItem("dropboxSession"); - if (sessionData) { - sessionData = JSON.parse(sessionData); - sessionData.forEach(main => { - session.dbx - .filesUploadSessionFinish({ cursor: { session_id: main.result.session_id, offset: main.vdo.offset }, commit: { path: "/" + main.vdo.filename } }) - .then(function (response) { - console.log(response); - console.log("File uploaded to Dropbox:", response.result.path_display); - DBXqueue = []; - //localStorage.removeItem('dropboxSession'); - }) - .catch(function (error) { - localStorage.removeItem("dropboxSession"); - console.error("Error uploading file:", error); - if (!session.cleanOutput) { - confirmAlt("There was an error finalizing a previous file upload. \n" + (error.error_summary || "") + "\n\nWould you like to keep trying?").then(res => { - if (!res) { - localStorage.removeItem("dropboxSession"); - } - }); - } - }); - }); - } -} - -async function streamVideoToDropbox(filename) { - if (!session.dbx && typeof setupDropbox === "function") { - try { - await setupDropbox(); - } catch (e) { - errorlog(e); - } - } - if (!session.dbx) { - if (session.directorUUID) { - var failMsg = {}; - failMsg.dropbox = -2; - for (var di = 0; di < session.directorList.length; di++) { - failMsg.UUID = session.directorList[di]; - session.sendPeers(failMsg, failMsg.UUID); - } - } - return; - } - - var main; - try { - main = await session.dbx.filesUploadSessionStart({ close: false }); - } catch (e) { - errorlog(e); - if (!session.cleanOutput) { - warnUser("Dropbox failed to initialize.\n\nAre your credentials valid? Tokens may expire after a few hours.", 8000); - } - if (dropboxErrorSuggestsReauth(e)) { - clearDropboxAuthState({ clearToken: true, clearOAuth: true }); - } - return; - } - - var sessionId = main.result.session_id; - var offset = 0; - var chunkCounter = 0; - var DBXqueue = []; - var resolverQueue = []; - var uploadActive = false; - - log(main); - main.vdo = { filename: filename, offset: offset }; - - var persisted = localStorage.getItem("dropboxSession"); - if (persisted) { - try { - persisted = JSON.parse(persisted); - } catch (e) { - errorlog(e); - persisted = []; - } - persisted.push(main); - } else { - persisted = [main]; - } - localStorage.setItem("dropboxSession", JSON.stringify(persisted)); - - if (session.directorUUID) { - var initMsg = {}; - initMsg.dropbox = -1; - for (var ii = 0; ii < session.directorList.length; ii++) { - initMsg.UUID = session.directorList[ii]; - session.sendPeers(initMsg, initMsg.UUID); - } - } - - var totalChunksRecorded = 0; - var totalChunksUploaded = 0; - - function updateTotalChunksRecorded() { - if (!session.cleanOutput) { - getById("progressContainer").classList.remove("hidden"); - } - totalChunksRecorded++; - updateProgressBar(); - } - - function updateTotalChunksUploaded() { - totalChunksUploaded++; - updateProgressBar(); - } - - function finishedChunksUploaded() { - try { - getById("progressBar").style.width = "100%"; - setTimeout(function () { - if (getById("progressBar").style.width == "100%") { - getById("progressContainer").classList.add("hidden"); - } - }, 1000); - } catch (e) { - errorlog(e); - } - } - - function updateProgressBar() { - if (totalChunksRecorded > 0) { - var progressPercentage = (totalChunksUploaded / (totalChunksRecorded || 1)) * 100; - getById("progressBar").style.width = progressPercentage + "%"; - getById("progressBar").innerHTML = "Upload progress to Dropbox: " + progressPercentage.toFixed(2) + "%"; - } - } - - function notifyDropboxQueueSize() { - if (!session.directorUUID) { - return; - } - var msg = {}; - msg.dropbox = DBXqueue.length; - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendPeers(msg, msg.UUID); - } - } - - function resolveNext(value) { - var resolver = resolverQueue.shift(); - if (resolver && resolver.resolve) { - resolver.resolve(value); - } - } - - function rejectNext(error) { - var resolver = resolverQueue.shift(); - if (resolver && resolver.reject) { - resolver.reject(error); - } - } - - function rejectPending(error) { - while (resolverQueue.length) { - var pending = resolverQueue.shift(); - if (pending && pending.reject) { - pending.reject(error); - } - } - } - - function handleDropboxUploadFailure(error) { - errorlog(error); - if (dropboxErrorSuggestsReauth(error)) { - clearDropboxAuthState({ clearToken: true, clearOAuth: true }); - } - if (!session.cleanOutput) { - warnUser("Dropbox upload failed. Please verify your token and try again.", 8000); - } - } - - async function processQueue() { - if (uploadActive) { - return; - } - uploadActive = true; - while (DBXqueue.length) { - var current = DBXqueue[0]; - try { - if (current === false) { - await session.dbx.filesUploadSessionFinish({ cursor: { session_id: sessionId, offset: offset }, commit: { path: "/" + filename } }); - DBXqueue.shift(); - resolveNext(true); - var sessionData = localStorage.getItem("dropboxSession"); - if (sessionData) { - try { - sessionData = JSON.parse(sessionData); - sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename); - } catch (e) { - errorlog(sessionData); - errorlog(e); - sessionData = []; - } - } else { - sessionData = []; - } - if (sessionData.length) { - localStorage.setItem("dropboxSession", JSON.stringify(sessionData)); - } else { - localStorage.removeItem("dropboxSession"); - } - sessionId = false; - notifyDropboxQueueSize(); - finishedChunksUploaded(); - } else { - await session.dbx.filesUploadSessionAppendV2({ cursor: { session_id: sessionId, offset: offset }, close: false, contents: current }); - offset += current.size; - main.vdo.offset = offset; - var sessionData = localStorage.getItem("dropboxSession"); - if (sessionData) { - try { - sessionData = JSON.parse(sessionData); - sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename); - sessionData.push(main); - } catch (e) { - errorlog(sessionData); - errorlog(e); - sessionData = [main]; - } - } else { - sessionData = [main]; - } - localStorage.setItem("dropboxSession", JSON.stringify(sessionData)); - DBXqueue.shift(); - resolveNext(true); - updateTotalChunksUploaded(); - chunkCounter += 1; - notifyDropboxQueueSize(); - } - } catch (error) { - rejectNext(error); - rejectPending(error); - DBXqueue = []; - uploadActive = false; - handleDropboxUploadFailure(error); - return; - } - } - uploadActive = false; - } - - function enqueueChunk(chunk) { - return new Promise((resolve, reject) => { - if (!sessionId) { - reject(new Error("Dropbox session inactive")); - return; - } - if (DBXqueue.length && DBXqueue[DBXqueue.length - 1] === false) { - resolve(); - return; - } - DBXqueue.push(chunk); - resolverQueue.push({ resolve: resolve, reject: reject }); - updateTotalChunksRecorded(); - notifyDropboxQueueSize(); - processQueue(); - }); - } - - function uploadChunk(chunk) { - var promise = enqueueChunk(chunk); - promise.catch(() => { }); - return promise; - } - - return uploadChunk; -} - -var recordingBitratePromise = false; -var defaultRecordingBitrate = false; -var lastConfiguredRecordingSetup = false; -async function recordVideo(target, event = null, videoKbps = false) { - // event.currentTarget,this.parentNode.parentNode.dataset.UUID - - if (session.record === false) { - warnlog("recordings are disabled by decree of thy host magistrate"); - } - - if (!target) { return; } - - var UUID = target.dataset.UUID; - - if (!UUID) { - return; - } - - var video = session.rpcs[UUID].videoElement; - - if (!video) { - return; - } - - if (video.stopWriter) { - video.stopWriter(); - updateLocalRecordButton(UUID, -1); - return; - } else if (video.startWriter) { - await video.startWriter(); - updateLocalRecordButton(UUID, 0); - return; - } - - if (event === null) { - if (defaultRecordingBitrate === null) { - updateLocalRecordButton(UUID, -1); - return; - } - } else if (event.ctrlKey || event.metaKey) { - updateLocalRecordButton(UUID, -3); - Callbacks.push([recordVideo, target, null, false]); - log("Record Video queued"); - defaultRecordingBitrate = false; - recordingBitratePromise = false; - return; - } else { - defaultRecordingBitrate = false; - recordingBitratePromise = false; - } - - log("Record Video Clicked"); - if ("recording" in video) { - log("ALREADY RECORDING!"); - updateLocalRecordButton(UUID, -2); - video.recorder.stop(); - session.requestRateLimit(35, UUID); // 100kbps - if (session.audiobitrate === false) { - session.requestAudioRateLimit(-1, UUID); - } - - var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - } - var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - } - var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - } - return; - } else { - updateLocalRecordButton(UUID, 0); - //target.style.backgroundColor = "#FCC"; - //target.innerHTML = " Download"; - video.recording = true; - } - - video.recorder = {}; - - var configureRecording = { - bitrate: videoKbps, - usePCM: (videoKbps === 0 || session.pcm) ? true : false, - audioOnly: (videoKbps !== false && videoKbps <= 0) ? true : false - }; - - if (configureRecording.bitrate === false) { - if (defaultRecordingBitrate == false) { - configureRecording.bitrate = session.recordDefault; - - if (session.recordLocal !== false) { - configureRecording.bitrate = session.recordLocal; - } else if (lastConfiguredRecordingSetup !== false) { - configureRecording = lastConfiguredRecordingSetup; - } - - if (session.pcm) { - configureRecording.usePCM = true; - } - - if (!recordingBitratePromise) { - window.focus(); - recordingBitratePromise = promptRecordingOptions(getTranslation("press-ok-to-record"), false, configureRecording); - } - configureRecording = await recordingBitratePromise; - if (configureRecording === null) { - //target.style.backgroundColor = null; - //target.innerHTML = ' record local'; - updateLocalRecordButton(UUID, -1); - target.style.backgroundColor = ""; - - try { - clearInterval(video.recorder.writer.interval); - } catch (e) { } - - delete video.recorder; - delete video.recording; - defaultRecordingBitrate = null; - return; - } - - defaultRecordingBitrate = configureRecording; - lastConfiguredRecordingSetup = configureRecording; - } else { - configureRecording = defaultRecordingBitrate; - } - } - - if (configureRecording.audioOnly) { - if (session.audiobitrate === false) { - if (configureRecording.usePCM) { - session.requestAudioRateLimit(session.audiobitratePRO || 128, UUID); // PCM - } else { - session.requestAudioRateLimit(configureRecording.bitrate || 32, UUID); // exact? sure. why not. - } - } - } else { - if (configureRecording.bitrate < 50) { - configureRecording.bitrate = 50; - } - session.requestRateLimit(configureRecording.bitrate, UUID); // 3200kbps transfer bitrate. Less than the recording bitrate, to avoid waste. - - if (configureRecording.bitrate > 4000) { - if (session.audiobitrate === false) { - if (session.pcm) { - session.requestAudioRateLimit(session.audiobitratePRO, UUID); - } else { - session.requestAudioRateLimit(128, UUID); - } - } - } else if (configureRecording.bitrate > 2500) { - if (session.audiobitrate === false) { - if (session.pcm) { - session.requestAudioRateLimit(session.audiobitratePRO, UUID); - } else { - session.requestAudioRateLimit(80, UUID); - } - } - } - } - - // - - var cancell = false; - if (typeof video.srcObject === "undefined" || !video.srcObject) { - return; - } - - video.recorder.stop = function (restart = false, notify = false) { - if (session.dbx && video.dropbox && video.dropbox[filename]) { - video.dropbox[filename](false); - } - if (video.gdrive && video.gdrive[filename]) { - video.gdrive[filename].addChunk(false); - } - - if (!video.recording) { - errorlog("ALREADY STOPPED"); - updateLocalRecordButton(UUID, -1); - return; - } - - if (notify) { - if (!session.cleanOutput) { - warnUser("A local recording has stopped unexpectedly."); - } - if (session.beepToNotify) { - playtone(); - } - target.classList.remove("shake"); - setTimeout( - function (target) { - target.classList.add("shake"); - }, - 10, - target - ); - } - - video.recording = false; - updateLocalRecordButton(UUID, -2); - try { - if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) { - if (video.recorder.mediaRecorder.state !== "inactive" || video.recorder.mediaRecorder.state === "recording") { - video.recorder.mediaRecorder.stop(); - } - } - } catch (e) { - errorlog(e); - try { - video.recorder.mediaRecorder.stop(); - } catch (e1) { - errorlog(e1); - } - } - - session.requestRateLimit(35, UUID); // 100kbps - if (session.audiobitrate === false) { - session.requestAudioRateLimit(-1, UUID); - } - var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - } - var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - } - var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - } - - cancell = true; - // log('Recorded Blobs: ', recordedBlobs); - // download(); - setTimeout( - (writer1, UUID1, video1) => { - try { - writer1.close(); - } catch (e) { } - updateLocalRecordButton(UUID1, -1); - delete video1.recorder; - delete video1.recording; - }, - 1200, - video.recorder.writer, - UUID, - video - ); - }; - - const { readable, writable } = new TransformStream({ - transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b))) - }); - - var filext = ".webm"; - - let options = {}; - - if (!configureRecording.audioOnly) { - var tryCodec = session.recordingVideoCodec || ""; // Simplified condition to assign tryCodec - - if (tryCodec && MediaRecorder.isTypeSupported("video/webm;codecs=" + tryCodec)) { - if (!session.cleanOutput) { - console.log("👍 The browser 'says' it supports " + tryCodec); - } - options.mimeType = "video/webm;codecs=" + tryCodec; - - if (session.pcm) { - // Fixed the format of the MIME type string - var mimeTypeWithPCM = "video/webm;codecs=" + tryCodec + ",pcm"; - if (MediaRecorder.isTypeSupported(mimeTypeWithPCM)) { - options.mimeType = mimeTypeWithPCM; - } else { - options.mimeType = "video/webm;codecs=pcm"; - } - } - } else { - // Simplified conditions for PCM support - if (tryCodec) { - warnlog("video/webm;codecs=" + tryCodec + " - is not supported"); - } - options.mimeType = session.pcm && MediaRecorder.isTypeSupported("video/webm;codecs=pcm") ? "video/webm;codecs=pcm" : "video/webm"; - } - - // Simplified bitrate settings - options.videoBitsPerSecond = parseInt(configureRecording.bitrate * 1024); - if (configureRecording.bitrate < 1000) { - options.audioBitsPerSecond = parseInt(100 * 1024); - } else if (configureRecording.bitrate < 6000) { - options.audioBitsPerSecond = parseInt(130 * 1024); - } else if (configureRecording.bitrate < 20000) { - options.audioBitsPerSecond = parseInt(256 * 1024); - } else { - // If configureRecording.bitrate is >= 20000, use bitsPerSecond for total bitrate - options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024); - } - - if (iOS && options.mimeType) { - if (!MediaRecorder.isTypeSupported(options.mimeType)) { - options.mimeType = "video/mp4"; - filext = ".mp4"; - } - } - - video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); - - //if (session.dbx){ - // video.recorder.dropbox = await streamVideoToDropbox(); // i don't want to upload to dropbox remote streams; just local - //} - } else { - options.mimeType = "audio/webm"; - if (configureRecording.usePCM) { - if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) { - options.mimeType = "audio/webm;codecs=pcm"; - } - } else { - options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024); - } - var stream = createMediaStream(); - video.srcObject.getAudioTracks().forEach(track => { - stream.addTrack(track, video.srcObject); - }); - - if (iOS && options.mimeType) { - if (!MediaRecorder.isTypeSupported(options.mimeType)) { - options.mimeType = "video/mp4"; - filext = ".mp4"; - } - } - video.recorder.mediaRecorder = new MediaRecorder(stream, options); - - //if (session.dbx){ - // video.recorder.dropbox = await streamVideoToDropbox(); - //} - } - - var timestamp = Date.now(); - var filename = ""; - if (session.rpcs[UUID].label && session.rpcs[UUID].streamID) { - filename = session.rpcs[UUID].label || session.rpcs[UUID].streamID; - } else { - filename = session.rpcs[UUID].label + "_" + session.rpcs[UUID].streamID; - } - - filename = filename.replace(/[\W]+/g, "_"); - filename = filename.substring(0, 200); - filename += "_" + timestamp.toString(); - - var writer = writable.getWriter(); - video.recorder.writer = writer; - readable.pipeTo(streamSaver.createWriteStream(filename.toString() + filext, video.recorder.stop)); - pokeIframeAPI("recording-started"); - - log(options); - - function download() { - const blob = new Blob(recordedBlobs, { - type: "video/webm" - }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.style.display = "none"; - a.href = url; - a.download = filename + filext; - document.body.appendChild(a); - a.click(); - setTimeout( - function (uu, aa) { - document.body.removeChild(aa); - window.URL.revokeObjectURL(uu); - }, - 100, - url, - a - ); - } - - function handleDataAvailable(event) { - if (event.data && event.data.size > 0) { - //recordedBlobs.push(event.data); - try { - writer.write(event.data); //////////// - if (video.recording) { - updateLocalRecordButton(UUID, parseInt((Date.now() - timestamp) / 1000) || 0); - } - } catch (e) { - warnlog("Stream recording error or ended"); - } - - try { - if (session.dbx && video.dropbox && video.dropbox[filename]) { - video.dropbox[filename](event.data); - } - if (video.gdrive && video.gdrive[filename]) { - video.gdrive[filename].addChunk(event.data); - } - } catch (e) { - errorlog(e); - } - } - } - - video.recorder.mediaRecorder.ondataavailable = handleDataAvailable; - - video.recorder.mediaRecorder.onerror = function (event) { - errorlog(event); - console.log("It's possible using &recordcodec=vp8 might resolve recording errors if caused by an incompatible hardware encoder or codec"); - video.recorder.stop(); - session.requestRateLimit(35, UUID); - if (!session.cleanOutput) { - setTimeout(function () { - warnUser("an error occured with the media recorder; stopping recording"); - }, 1); - } - }; - - video.recorder.mediaRecorder.onstop = function (event) { - log("mediaRecorder stopped"); - }; - - video.srcObject.onended = function (event) { - video.recorder.stop(); - //session.requestRateLimit(35, UUID); // changed DEc 05 2023 - not sure this makes sense. - if (!session.cleanOutput) { - setTimeout(function () { - warnUser("stream ended! stopping recording"); - }, 1); - } - }; - - setTimeout( - function (v) { - if (v && v.recorder) { - v.recorder.mediaRecorder.start(1000); - } - }, - 500, - video - ); // 100ms chunks - - return; -} -function updateGdriveButton(UUID, gdrive, screen = false) { - if (screen) { - var elements = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + UUID + '_screen"]'); - } else { - var elements = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + UUID + '"]'); - } - if (elements[0]) { - var progressPercentage = parseInt((1000 * gdrive.up) / gdrive.rec) / 10; - elements[0].innerText = progressPercentage + "%"; - - if (gdrive.state && gdrive.state == 2) { - elements[0].innerText = "GDrive " + elements[0].innerText + " (done)"; - } else { - elements[0].innerText += ", " + convertKilobytes(gdrive.rec) + " left"; - } - } - - if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") { - try { - window.dispatchEvent( - new CustomEvent("vdoninja:gdrive-progress", { - detail: { - UUID: UUID, - gdrive: gdrive, - screen: screen - } - }) - ); - } catch (e) { } - } -} - -function updateRemoteRecordButton(UUID, recorder, screen = false) { - if (screen) { - var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '_screen"]'); - } else { - var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '"]'); - } - if (elements[0]) { - var time = parseInt(recorder) || 0; - if (time == -4) { - if (!session.cleanOutput) { - warnUser("A remote recording has stopped unexpectedly.\n\nDid a user cancel the file downlaod?"); - } - if (session.beepToNotify) { - playtone(); - } - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - elements[0].classList.remove("shake"); - elements[0].innerHTML = ' stopping...'; - setTimeout( - function (ele) { - ele.classList.add("shake"); - }, - 10, - elements[0] - ); - } else if (time == -3) { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - elements[0].disabled = true; - elements[0].innerHTML = ' Not Supported'; - if (!session.cleanOutput) { - setTimeout(function () { - warnUser("The remote browser does not support recording.\n\nPerhaps try local recording instead."); - }, 0); - } - } else if (time == -5) { - if (!session.cleanOutput) { - setTimeout(function () { - warnUser("The remote browser has only experimental support for media recording.\n\nAlso, when this download stops, the remote user may be asked to download the file for it to save."); - }, 0); - } - } else if (time == -2) { - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - elements[0].innerHTML = ' stopping...'; - } else if (time == -1) { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - elements[0].innerHTML = ' Record Remote'; - } else { - var minutes = Math.floor(time / 60); - var seconds = time - minutes * 60; - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - elements[0].innerHTML = ' ' + minutes + "m : " + zpadTime(seconds) + "s"; - } - } -} - -function updateLocalRecordButton(UUID, recorder) { - var elements = document.querySelectorAll('[data-action-type="recorder-local"][data--u-u-i-d="' + UUID + '"]'); - if (elements[0]) { - var time = parseInt(recorder) || 0; - - //target.innerHTML = ' ARMED'; - // - if (time == -3) { - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - elements[0].innerHTML = ' ARMED'; - elements[0].style.backgroundColor = "#BF3F3F"; - } else if (time == -2) { - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - elements[0].innerHTML = ' stopping...'; - elements[0].style.backgroundColor = ""; - } else if (time == -1) { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - elements[0].innerHTML = ' Record Local'; - elements[0].style.backgroundColor = ""; - } else { - var minutes = Math.floor(time / 60); - var seconds = time - minutes * 60; - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - elements[0].innerHTML = ' ' + minutes + "m : " + zpadTime(seconds) + "s"; - elements[0].style.backgroundColor = ""; - } - } -} - -async function recordLocalVideoToggle(startonly = false) { - if (!session.videoElement) { - return; - } - log("recordLocalVideoToggle()"); - - var ele = getById("recordLocalbutton"); - if (ele.dataset.state == "0") { - if (session.videoElement.recorder && session.videoElement.recorder.closing) { - warnlog("already closing"); - getById("recordLocalbutton").classList.remove("shake"); - getById("recordLocalbutton").classList.add("shake"); - setTimeout(function () { - getById("recordLocalbutton").classList.remove("shake"); - }, 1000); - - return false; - } - - ele.dataset.state = "1"; - ele.style.backgroundColor = "red"; - ele.innerHTML = ''; - if ("recording" in session.videoElement) { - errorlog("its already recording ??"); - } else { - var res = await recordLocalVideo("start"); - log(res); - } - - if (session.director) { - var elements = document.querySelectorAll('[data-action-type="recorder-local"][data-sid="' + session.streamID + '"]'); - if (elements[0]) { - elements[0].classList.add("pressed"); - elements[0].ariaPressed = "true"; - elements[0].innerHTML = ' Record'; - } - } - return true; - } else if (!startonly) { - if ("recording" in session.videoElement) { - var res = await recordLocalVideo("stop"); - log(res); - } - ele.dataset.state = "0"; - ele.style.backgroundColor = ""; - ele.innerHTML = ''; - - if (session.director) { - var elements = document.querySelectorAll('[data-action-type="recorder-local"][data-sid="' + session.streamID + '"]'); - if (elements[0]) { - elements[0].classList.remove("pressed"); - elements[0].ariaPressed = "false"; - elements[0].innerHTML = ' Record'; - } - } - return false; - } -} - -function cleanupSensorData(data) { - for (const key in data) { - let nonNullFound = false; - for (const subKey in data[key]) { - if (data[key][subKey] === null) { - delete data[key][subKey]; - } else if (subKey !== "t") { - nonNullFound = true; - } - } - if (!nonNullFound) { - delete data[key]; - } - } -} - -function setupSensorData(pollrate = 30) { - session.sensors = {}; - session.sensors.data = {}; - // session.sensorDataFilter = ["pos","lin","ori","mag","gyro","acc"]; - - const startSensor = (SensorType, sensorKey) => { - if (window[SensorType] && session.sensorDataFilter.includes(sensorKey)) { - try { - session.sensors.data[sensorKey] = {}; - let sensor = new window[SensorType]({ frequency: pollrate }); - sensor.addEventListener("reading", () => { - try { - session.sensors.data[sensorKey].x = sensor.x !== null ? parseFloat(sensor.x.toFixed(5)) : null; - session.sensors.data[sensorKey].y = sensor.y !== null ? parseFloat(sensor.y.toFixed(5)) : null; - session.sensors.data[sensorKey].z = sensor.z !== null ? parseFloat(sensor.z.toFixed(5)) : null; - } catch (e) { } - try { - session.sensors.data[sensorKey].t = parseInt(Math.round(sensor.timeStamp || 0)) || Date.now(); - } catch (e) { - errorlog(e); - } - }); - sensor.start(); - session.sensors[sensorKey] = sensor; - } catch (e) { - errorlog(e); - } - } - }; - - startSensor("Accelerometer", "acc"); - startSensor("Gyroscope", "gyro"); - startSensor("Magnetometer", "mag"); - startSensor("LinearAccelerationSensor", "lin"); - - if (session.sensorDataFilter.includes("ori")) { - try { - window.addEventListener("deviceorientation", e => { - if (e.alpha || e.beta || e.gamma || e.absolute) { - session.sensors.data.ori = { - a: e.alpha !== null ? parseFloat(e.alpha.toFixed(5)) : null, - b: e.beta !== null ? parseFloat(e.beta.toFixed(5)) : null, - g: e.gamma !== null ? parseFloat(e.gamma.toFixed(5)) : null, - d: e.absolute || null, - t: parseInt(Math.round(e.timeStamp || 0)) || Date.now() - }; - } - }); - } catch (e) { - errorlog("Device Orientation Error:", e); - } - } - - let isFirstUpdate = true; - if (navigator.geolocation && session.sensorDataFilter.includes("pos")) { - try { - navigator.geolocation.watchPosition( - pos => { - session.sensors.data.pos = { - speed: pos.coords.speed !== null ? parseFloat(pos.coords.speed.toFixed(3)) : null, - alt: pos.coords.altitude !== null ? parseFloat(pos.coords.altitude.toFixed(3)) : null, - acc: pos.coords.accuracy !== null ? parseFloat(pos.coords.accuracy.toFixed(3)) : null, - lat: pos.coords.latitude !== null ? parseFloat(pos.coords.latitude.toFixed(3)) : null, - lon: pos.coords.longitude !== null ? parseFloat(pos.coords.longitude.toFixed(3)) : null, - t: parseInt(Math.round(pos.timeStamp || 0)) || Date.now() - }; - - if (isFirstUpdate && (pos.coords.latitude || pos.coords.longitude)) { - isFirstUpdate = false; - console.log(session.sensors.data); - warnUser("🌎🌍🌏 Geo-location sharing is enabled.\n\nIf being tracked is unwanted, please disable the 'Location' permissions in your browser's site settings.", 10000); - } - }, - error => { - errorlog("Geolocation Error:", error); - if (error.code === error.PERMISSION_DENIED) { - warnUser("Geolocation permission was denied."); - } - }, - { - enableHighAccuracy: true, - timeout: 5000, - maximumAge: 0 - } - ); - } catch (e) { - errorlog("Device Orientation Error:", e); - } - } - - setInterval(function () { - if (session.sensors && session.sensors.data) { - cleanupSensorData(session.sensors.data); - session.sendMessage({ sensors: session.sensors.data }); - } - }, parseInt(1000 / pollrate)); -} - -function setupExternalSensorBridge() { - if (session.externalSensorBridgeAttached) { - return; - } - - session.externalSensorBridgeAttached = true; - log("External sensor bridge attached"); - - window.addEventListener("message", event => { - const payload = event.data; - if (!payload || !payload.sensors) { - return; - } - - if (session.externalSensorOrigin && event.origin && event.origin !== "null" && event.origin !== session.externalSensorOrigin) { - log("Sensor message rejected due to origin mismatch: " + event.origin); - return; - } - - session.sensors = session.sensors || {}; - session.sensors.data = session.sensors.data || {}; - - try { - Object.assign(session.sensors.data, payload.sensors); - } catch (e) {} - - try { - session.sendMessage({ sensors: payload.sensors }); - } catch (e) { - errorlog(e); - } - }); -} - -//// PCM 16 SAVING LOGIC -function PCM16(stream) { - if (!stream || !stream.getAudioTracks().length) { - errorlog("no audio track found"); - return null; - } - - var PCM = stream.getAudioTracks()[0].getSettings(); - - function audioBufferToWav(buffer, options = {}) { - const numChannels = buffer.numberOfChannels; - const sampleRate = buffer.sampleRate; - const format = options.float32 ? 3 : 1; - const bitDepth = format === 3 ? 32 : 16; - - let samples; - if (numChannels === 2) { - samples = interleave(buffer.getChannelData(0), buffer.getChannelData(1)); - } else { - samples = buffer.getChannelData(0); - } - - return encodeWAV(samples, format, sampleRate, numChannels, bitDepth); - } - - function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) { - const bytesPerSample = bitDepth / 8; - const blockAlign = numChannels * bytesPerSample; - const bufferLength = 44 + samples.length * bytesPerSample; - const buffer = new ArrayBuffer(bufferLength); - const dataView = new DataView(buffer); - - // referenced from: https://github.com/steveseguin/audiobuffer-to-wav (by Jam3 - MIT lic) - writeString(dataView, 0, "RIFF"); - dataView.setUint32(4, 36 + samples.length * bytesPerSample, true); - writeString(dataView, 8, "WAVE"); - writeString(dataView, 12, "fmt "); - dataView.setUint32(16, 16, true); - dataView.setUint16(20, format, true); - dataView.setUint16(22, numChannels, true); - dataView.setUint32(24, sampleRate, true); - dataView.setUint32(28, sampleRate * blockAlign, true); - dataView.setUint16(32, blockAlign, true); - dataView.setUint16(34, bitDepth, true); - writeString(dataView, 36, "data"); - dataView.setUint32(40, samples.length * bytesPerSample, true); - - if (format === 1) { - floatTo16BitPCM(dataView, 44, samples); - } else { - writeFloat32(dataView, 44, samples); - } - - return buffer; - } - function interleave(inputL, inputR) { - const length = inputL.length + inputR.length; - const result = new Float32Array(length); - - for (let index = 0, inputIndex = 0; index < length; index += 2, inputIndex++) { - result[index] = inputL[inputIndex]; - result[index + 1] = inputR[inputIndex]; - } - - return result; - } - function floatTo16BitPCM(output, offset, input) { - for (let i = 0; i < input.length; i++, offset += 2) { - const s = Math.max(-1, Math.min(1, input[i])); - output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); - } - } - function writeFloat32(output, offset, input) { - for (let i = 0; i < input.length; i++, offset += 4) { - output.setFloat32(offset, input[i], true); - } - } - function writeString(dataView, offset, string) { - for (let i = 0; i < string.length; i++) { - dataView.setUint8(offset + i, string.charCodeAt(i)); - } - } - // end reference - - PCM.audioContext = new AudioContext({ sampleRate: PCM.sampleRate }); - PCM.source = PCM.audioContext.createMediaStreamSource(stream); - PCM.numberOfChannels = PCM.source.channelCount; - PCM.scriptNode = PCM.audioContext.createScriptProcessor(4096, PCM.numberOfChannels, PCM.numberOfChannels); // buffer size, input channels, output channels - - PCM.recording = false; - PCM.audioData = []; - for (let i = 0; i < PCM.numberOfChannels; i++) { - PCM.audioData.push([]); - } - PCM.scriptNode.onaudioprocess = audioProcessingEvent => { - if (!PCM.recording) return; - for (let channel = 0; channel < PCM.numberOfChannels; channel++) { - const inputData = audioProcessingEvent.inputBuffer.getChannelData(channel); - PCM.audioData[channel].push(new Float32Array(inputData)); - } - }; - PCM.source.connect(PCM.scriptNode); - PCM.scriptNode.connect(PCM.audioContext.destination); - - PCM.startRecording = function () { - PCM.audioData = []; - for (let i = 0; i < PCM.numberOfChannels; i++) { - PCM.audioData.push([]); - } - PCM.recording = true; - }; - PCM.stopRecording = function (filename = "filename") { - PCM.recording = false; - - const bufferLength = PCM.audioData[0].length * 4096; - const audioBuffer = PCM.audioContext.createBuffer(PCM.numberOfChannels, bufferLength, PCM.audioContext.sampleRate); - - for (let channel = 0; channel < PCM.numberOfChannels; channel++) { - const channelData = audioBuffer.getChannelData(channel); - PCM.audioData[channel].forEach((chunk, index) => { - channelData.set(chunk, index * 4096); - }); - } - const wavArrayBuffer = audioBufferToWav(audioBuffer); - const blob = new Blob([wavArrayBuffer], { type: "audio/wav" }); - const url = URL.createObjectURL(blob); - const anchor = document.createElement("a"); - anchor.href = url; - anchor.download = filename + ".wav"; - anchor.click(); - URL.revokeObjectURL(url); - }; - - return PCM; -} -//// END OF PCM 16 SAVING CODE - -async function recordLocalVideo(action = null, configureRecording = false, remote = false, altUUID = false) { - // event.currentTarget,this.parentNode.parentNode.dataset.UUID - - if (session.record === false) { - warnlog("recordings are disabled by decree of thy host magistrate"); - } - log("original", configureRecording); - if (typeof configureRecording !== "object") { - let bitrate = configureRecording !== false ? configureRecording : (session.recordLocal !== false ? session.recordLocal : session.recordDefault); - configureRecording = { - bitrate: bitrate, - usePCM: (bitrate === 0 || session.pcm) ? true : false, - audioOnly: (bitrate !== false && bitrate <= 0) ? true : false - }; - } - - if (remote) { - var video = remote; - if (remote.id === "videosource" || remote.id === "screensharesource") { - remote = false; - } - } else if (altUUID) { - var video = session.screenShareElement; - } else { - var video = session.videoElement; - } - - if (!video) { - warnlog("video not found"); - return; - } - - log(video.id); - - if ("recording" in video) { - if (action == "estop") { - video.recorder.eStop(); - warnlog("EMERGENCY Stopping RECORDING!"); - video.recorder.stop(); - return; - } else if (action == "stop") { - log("Stopping RECORDING!"); - video.recorder.stop(); - return; - } else if (action == "start") { - errorlog("ALREADY RECORDING!"); - if (remote) { - getById("recordLocalbutton").dataset.state = "1"; - getById("recordLocalbutton").style.backgroundColor = "red"; - getById("recordLocalbutton").innerHTML = ''; - } - return; - } else { - errorlog("STOPPING RECORDING by default toggle!"); - video.recorder.stop(); - return; - } - return; // this should never happen - } else if (action == "start") { - if (video && video.recorder && video.recorder.closing) { - errorlog("Ingore request. Haven't finished closing the previous recording."); - return; - } - - if (video.srcObject && video.srcObject.getTracks && !video.srcObject.getTracks().length) { - warnlog("No video or audio tracks to record"); - return; - } - - if (!MediaRecorder) { - var msg = {}; - msg.recorder = -3; - if (altUUID) { - msg.alt = true; - } - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - errorlog("no MediaRecorder"); - return; - } else if (SafariVersion || iPad || iOS) { - var msg = {}; - msg.recorder = -5; - if (altUUID) { - msg.alt = true; - } - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - log("SAFARI/IOS MODE ENABLED"); - } - video.recording = true; - if (remote) { - getById("recordLocalbutton").dataset.state = "1"; - getById("recordLocalbutton").style.backgroundColor = "red"; - getById("recordLocalbutton").innerHTML = ''; - } - } else if (action == "stop") { - warnlog("stop not sensible"); - return; - } else { - log("action is :" + action); - if (video && video.recorder && video.recorder.closing) { - errorlog("Ingore request. Haven't finished closing the previous recording."); - return; - } - - if (!remote) { - getById("recordLocalbutton").dataset.state = "1"; - getById("recordLocalbutton").style.backgroundColor = "red"; - getById("recordLocalbutton").innerHTML = ''; - } - video.recording = true; - } - - video.recorder = {}; - - if (!configureRecording.audioOnly && configureRecording.bitrate < 50) { - configureRecording.bitrate = 50; - } - - if (typeof video.srcObject === "undefined" || !video.srcObject) { - errorlog("video.srcObject undefined"); - return; - } - - log(configureRecording); - - var timestamp = Date.now(); - var filename = ""; - if (session.label || session.streamID) { - filename = session.label || session.streamID; - filename = filename.replace(/[\W]+/g, "_"); - filename = filename.substring(0, 200); - } - - filename += "_" + timestamp.toString(); - log("filename: " + filename); - - video.recorder.eStop = function () { - try { - video.recorder.writer.close(); - clearInterval(video.recorder.writer.interval); - } catch (e) { } - }; - - video.recorder.stop = function (restart = false, notify = false) { - if (session.dbx && video.dropbox && video.dropbox[filename]) { - video.dropbox[filename](false); - } - if (video.gdrive && video.gdrive[filename]) { - video.gdrive[filename].addChunk(false); - } - - try { - if (!remote) { - if (restart) { - if (getById("recordLocalbutton").dataset.state == 2) { - getById("recordLocalbutton").dataset.state = "0"; - getById("recordLocalbutton").style.backgroundColor = ""; - getById("recordLocalbutton").innerHTML = ''; - if (restart !== true) { - warnUser("Media Recording Stopped due to an error: " + restart); - } else { - warnUser("Media Recording Stopped due to an error."); - } - restart = false; - } else { - getById("recordLocalbutton").innerHTML = ''; - getById("recordLocalbutton").dataset.state = "2"; - } - } else { - getById("recordLocalbutton").dataset.state = "0"; - getById("recordLocalbutton").style.backgroundColor = ""; - getById("recordLocalbutton").innerHTML = ''; - if (notify) { - if (!session.cleanOutput) { - warnUser("A recording has stopped unexpectedly."); - } - if (session.beepToNotify) { - playtone(); - } - getById("recordLocalbutton").classList.remove("shake"); - setTimeout(function () { - getById("recordLocalbutton").classList.add("shake"); - }, 10); - } - } - } - } catch (e) { - errorlog(e); - } - - if (!video.recording) { - errorlog("ALREADY STOPPED"); - return; - } - - if (!video.recorder || video.recorder.closing) { - errorlog("it's still closing; can't start until its done"); - return; - } - video.recorder.closing = true; // start the closing process - - try { - if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) { - if (video.recorder.mediaRecorder.state !== "inactive") { - video.recorder.mediaRecorder.stop(); - } - } - } catch (e) { - errorlog(e); - try { - video.recorder.mediaRecorder.stop(); - } catch (e1) { - errorlog(e1); - } - } - - // video.recording = false; - - setTimeout( - (configureRecording, altUUID, video) => { - try { - video.recorder.writer.close(); - } catch (e) { - errorlog(e); - } - try { - clearInterval(video.recorder.writer.interval); - } catch (e) { - errorlog(e); - } - pokeIframeAPI("recording-stopped"); - if (!remote) { - try { - if (session.directorUUID) { - var msg = {}; - msg.recorder = -1; - if (altUUID) { - msg.alt = true; - } - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - } - } catch (e) { - errorlog(e); - } - } - - try { - if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) { - if (video.recorder.mediaRecorder.state !== "inactive") { - video.recorder.mediaRecorder.stop(); - } - } - } catch (e) { - errorlog(e); - try { - video.recorder.mediaRecorder.stop(); - } catch (e1) { - errorlog(e1); - } - } - try { - delete video.recorder; - delete video.recording; - } catch (e) { } - - if (!remote) { - if (restart) { - setTimeout( - function (configureRecording, altUUID) { - recordLocalVideo("start", configureRecording, false, altUUID); - }, - 0, - configureRecording, - altUUID - ); - } - } - }, - 500, - configureRecording, - altUUID, - video - ); - - if (!remote) { - try { - if (session.directorUUID) { - var msg = {}; - if (notify) { - msg.recorder = -4; // user aborted - } else { - msg.recorder = -2; - } - if (altUUID) { - msg.alt = true; - } - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - } - } catch (e) { - errorlog(e); - } - } - }; - - let options = {}; - let filext = ".webm"; - log("setting up options"); - - if (!configureRecording.audioOnly) { - log("videoKbps: " + configureRecording.bitrate); - var tryCodec = session.recordingVideoCodec || ""; // Simplified condition to assign tryCodec - - if (tryCodec && MediaRecorder.isTypeSupported("video/webm;codecs=" + tryCodec)) { - if (!session.cleanOutput) { - console.log("👍 The browser 'says' it supports " + tryCodec); - } - options.mimeType = "video/webm;codecs=" + tryCodec; - - if (configureRecording.usePCM) { - // Fixed the format of the MIME type string - var mimeTypeWithPCM = "video/x-matroska;codecs=" + tryCodec + ",pcm"; - if (MediaRecorder.isTypeSupported(mimeTypeWithPCM)) { - options.mimeType = mimeTypeWithPCM; - } else { - options.mimeType = "video/webm;codecs=pcm"; - } - } - } else { - // Simplified conditions for PCM support - if (tryCodec) { - warnlog("video/webm;codecs=" + tryCodec + " - is not supported"); - } - options.mimeType = configureRecording.usePCM && MediaRecorder.isTypeSupported("video/webm;codecs=pcm") ? "video/webm;codecs=pcm" : "video/webm"; - } - - // Simplified bitrate settings - options.videoBitsPerSecond = parseInt(configureRecording.bitrate * 1024); - if (configureRecording.bitrate < 1000) { - options.audioBitsPerSecond = parseInt(100 * 1024); - } else if (configureRecording.bitrate < 6000) { - options.audioBitsPerSecond = parseInt(130 * 1024); - } else if (configureRecording.bitrate < 20000) { - options.audioBitsPerSecond = parseInt(256 * 1024); - } else { - // If configureRecording.bitrate is >= 20000, use bitsPerSecond for total bitrate - options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024); - } - - if (iOS && options.mimeType) { - if (!MediaRecorder.isTypeSupported(options.mimeType)) { - options.mimeType = "video/mp4"; - filext = ".mp4"; - } - } - - video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); - try { - log(options); - video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); - } catch (e) { - warnlog(e); - try { - errorlog("options failed"); - video.recorder.mediaRecorder = new MediaRecorder(video.srcObject); - } catch (e) { - errorlog(e); - errorlog("Failing the recording"); - var msg = {}; - msg.recorder = -3; - if (altUUID) { - msg.alt = true; - } - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - getById("recordLocalbutton").dataset.state = "0"; - getById("recordLocalbutton").style.backgroundColor = ""; - getById("recordLocalbutton").innerHTML = ''; - return; - } - } - - if (session.dbx) { - if (!video.dropbox) { - video.dropbox = {}; - } - - video.dropbox[filename] = await streamVideoToDropbox(filename.toString() + filext); // i don't want to upload to dropbox remote streams; just local - if (!video.dropbox[filename]) { - delete video.dropbox[filename]; - } - } - if (session.gdrive) { - if (!video.gdrive) { - video.gdrive = {}; - } - if (session.gdrive === true) { - video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext); - } else if (session.gdrive.sessionUri) { - video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext, session.gdrive.sessionUri); // filename isn't actually being used here - session.gdrive = false; - } else { - errorlog("gdrive partially setup?"); - video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext); - } - if (!video.gdrive[filename]) { - delete video.gdrive[filename]; - } - } - - log(video.recorder.mediaRecorder); - } else { - log("Audio only?"); - options.mimeType = "audio/webm"; - if (configureRecording.usePCM) { - if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) { - options.mimeType = "audio/webm;codecs=pcm"; - } - } else { - options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024); - } - var stream = createMediaStream(); - var audioTrack = false; - video.srcObject.getAudioTracks().forEach(track => { - audioTrack = true; - stream.addTrack(track, video.srcObject); - }); - - if (!audioTrack) { - errorlog("Failing the recording; no audio track"); - try { - video.recorder.writer.close(); - } catch (e) { } - - try { - clearInterval(video.recorder.writer.interval); - } catch (e) { } - - try { - delete video.recorder; - delete video.recording; - } catch (e) { } - var msg = {}; - msg.recorder = -3; - if (altUUID) { - msg.alt = true; - } - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - getById("recordLocalbutton").dataset.state = "0"; - getById("recordLocalbutton").style.backgroundColor = ""; - getById("recordLocalbutton").innerHTML = ''; - return; - } else { - if (iOS && options.mimeType) { - if (!MediaRecorder.isTypeSupported(options.mimeType)) { - options.mimeType = "video/mp4"; - } - } - - try { - video.recorder.mediaRecorder = new MediaRecorder(stream, options); - } catch (e) { - warnlog(e); - try { - errorlog("options failed. failing safe.."); - video.recorder.mediaRecorder = new MediaRecorder(stream); - } catch (e) { - errorlog(e); - errorlog("Fail safe failed; closing the recording"); - try { - video.recorder.writer.close(); - } catch (e) { } - - try { - clearInterval(video.recorder.writer.interval); - } catch (e) { } - - try { - delete video.recorder; - delete video.recording; - } catch (e) { } - var msg = {}; - msg.recorder = -3; - if (altUUID) { - msg.alt = true; - } - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - getById("recordLocalbutton").dataset.state = "0"; - getById("recordLocalbutton").style.backgroundColor = ""; - getById("recordLocalbutton").innerHTML = ''; - return; - } - } - if (session.dbx) { - if (!video.dropbox) { - video.dropbox = {}; - } - video.dropbox[filename] = await streamVideoToDropbox(filename.toString() + filext); // i don't want to upload to dropbox remote streams; just local - if (!video.dropbox[filename]) { - delete video.dropbox[filename]; - } - } - if (session.gdrive) { - if (!video.gdrive) { - video.gdrive = {}; - } - if (session.gdrive === true) { - video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext); - } else if (session.gdrive.sessionUri) { - video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext, session.gdrive.sessionUri); // filename isn't actually being used here - session.gdrive = false; - } else { - video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext); - errorlog("Gdrive only partially setup"); - } - if (!video.gdrive[filename]) { - delete video.gdrive[filename]; - } - } - } - } - log(options); - - function createLock() { - let isLocked = false; - let queue = Promise.resolve(); - - return { - acquire: async () => { - const release = () => { isLocked = false; }; - while (isLocked) { - await new Promise(resolve => setTimeout(resolve, 10)); - } - isLocked = true; - return release; - } - }; - } - - var chunkQueue = []; - async function handleDataAvailable(event, process = true) { - if (!video.recorder.writerLock) { - video.recorder.writerLock = createLock(); - } - - // Handle existing queue first - while (chunkQueue.length && process) { - const ret = await handleDataAvailable(chunkQueue.shift(), false); - if (ret === false) { - return; - } - } - - if (event.data && event.data.size > 0) { - try { - const release = await video.recorder.writerLock.acquire(); - try { - if (video && video.recorder && video.recorder.writer && video.recorder.writer._ownerWritableStream && video.recorder.writer._ownerWritableStream._state === "writable") { - await video.recorder.writer.write(event.data); - } else { - throw new Error("Writer not open"); - } - } catch (e) { - if (process === true) { - chunkQueue.push(event); - } else { - chunkQueue.unshift(event); - release(); - return false; - } - } finally { - release(); - } - - // Rest of the existing code for messaging and cloud uploads - if (session.directorList.length) { - if (video.recording) { - var msg = {}; - if (altUUID) { - msg.alt = true; - } - msg.recorder = parseInt((Date.now() - timestamp) / 1000) || 0; - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - } - } - - if (session.dbx && video.dropbox && video.dropbox[filename]) { - video.dropbox[filename](event.data); - } - if (video.gdrive && video.gdrive[filename]) { - video.gdrive[filename].addChunk(event.data); - } - } catch (e) { - errorlog(e); - } - } - } - - video.recorder.mediaRecorder.ondataavailable = (event) => { - handleDataAvailable(event).catch(e => errorlog(e)); - }; - - video.recorder.mediaRecorder.onerror = function (event) { - errorlog(event); - console.log("It's possible using &recordcodec=vp8 might resolve recording errors if caused by an incompatible hardware encoder or codec"); - if (event && event.error && event.error.name) { - video.recorder.stop(event.error.name); - } else { - video.recorder.stop(true); - } - }; - - video.srcObject.onended = function (event) { - video.recorder.stop(); - }; - - try { - video.recorder.iteration = 0; - - video.recorder.setupWriter = async function setupWriter(video) { - if (video.recorder.writer) { - try { - video.recorder.writer.close(); - await sleep(1000); - // we don't cancel the interval obviously - } catch (e) { - errorlog(e); - } - } - var { readable, writable } = new TransformStream({ - transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b))) - }); - var writer = await writable.getWriter(); - var addon = ""; - if (video.recorder.iteration != 0) { - addon = "_" + video.recorder.iteration; - } - video.recorder.iteration += 1; - readable.pipeTo(streamSaver.createWriteStream(filename.toString() + filext + addon, video.recorder.stop)); - video.recorder.writer = writer; - }; - - await video.recorder.setupWriter(video); - - if (session.recordingInterval) { - // minutes - function intervalClosure(video) { - var intervalId = setInterval( - function (video) { - try { - video.recorder.setupWriter(video); - } catch (e) { - clearInterval(intervalId); - } - }, - 1000 * 60 * session.recordingInterval, - video - ); - video.recorder.writer.interval = intervalId; - } - intervalClosure(video); - } - - video.recorder.mediaRecorder.start(1000); // 100ms chunks - - log("started recording"); - - pokeIframeAPI("recording-started"); - - getById("recordLocalbutton").dataset.state = "1"; - getById("recordLocalbutton").style.backgroundColor = "red"; - getById("recordLocalbutton").innerHTML = ''; - - if (session.directorList.length) { - var msg = {}; - if (altUUID) { - msg.alt = true; - } - msg.recorder = 0; - for (var i = 0; i < session.directorList.length; i++) { - msg.UUID = session.directorList[i]; - session.sendMessage(msg, msg.UUID); - } - } - } catch (e) { - errorlog(e); - } - return; -} - -async function recordWindowCapture(bitrate = 6000) { - // Streamlined window/tab recording for scenes - // Captures the current browser tab and records to disk - // - // Customizable via URL parameters: - // &recordwindow=BITRATE - recording bitrate in kbps (default: 6000) - // &pcm - use PCM audio (lossless, larger files) - // &screensharefps=FPS - capture framerate (default: 60) - // &screensharequality=X - resolution: 4k, 2k, 1080p, 720p, etc. - // &width=W&height=H - custom resolution - - if (session.recordWindowElement && session.recordWindowElement.recording) { - log("Window recording already in progress"); - return; - } - - try { - // Determine resolution from session parameters - var targetWidth = 1920; - var targetHeight = 1080; - - if (session.screensharequality) { - var q = parseInt(session.screensharequality); - if (q === -2) { // 4k - targetWidth = 3840; - targetHeight = 2160; - } else if (q === -3) { // 2k/1440p - targetWidth = 2560; - targetHeight = 1440; - } else if (q === 1) { // 720p - targetWidth = 1280; - targetHeight = 720; - } else if (q === 2) { // 360p - targetWidth = 640; - targetHeight = 360; - } - } - if (session.width) { - targetWidth = parseInt(session.width) || targetWidth; - } - if (session.height) { - targetHeight = parseInt(session.height) || targetHeight; - } - - // Determine framerate - var targetFps = 60; - if (session.screensharefps) { - targetFps = parseInt(session.screensharefps) || 60; - } - - var constraints = { - video: { - frameRate: { ideal: targetFps }, - width: { ideal: targetWidth }, - height: { ideal: targetHeight }, - cursor: "never" - }, - audio: true, - preferCurrentTab: true, - selfBrowserSurface: "include", - surfaceSwitching: "exclude" - }; - - if (session.displaySurface) { - constraints.video.displaySurface = session.displaySurface; - } - if (session.suppressLocalAudioPlayback) { - constraints.audio = { suppressLocalAudioPlayback: true }; - } - - log("Starting window capture: " + targetWidth + "x" + targetHeight + "@" + targetFps + "fps"); - var stream = await navigator.mediaDevices.getDisplayMedia(constraints); - - // Create a temp video element to hold the capture - var video = document.createElement("video"); - video.id = "recordWindowSource"; - video.srcObject = stream; - video.muted = true; - video.autoplay = true; - video.playsInline = true; - video.style.display = "none"; - document.body.appendChild(video); - - session.recordWindowElement = video; - - // Handle stream ending (user stops sharing) - stream.getVideoTracks()[0].onended = function() { - log("Window capture ended"); - if (video.recording) { - recordLocalVideo("stop", false, video); - } - video.remove(); - session.recordWindowElement = null; - // Reset button state if exists - var btn = document.getElementById("recordWindowButton"); - if (btn) { - btn.innerHTML = "● Start Recording"; - btn.title = "Record this scene to a local video file"; - btn.style.background = "#d00"; - btn.style.opacity = "1"; - btn.dataset.recording = "0"; - } - // Stop Go Live if active - if (session.goLivePC) { - try { - session.goLivePC.close(); - } catch(e) {} - session.goLivePC = null; - } - var liveBtn = document.getElementById("goLiveButton"); - if (liveBtn) { - liveBtn.innerHTML = "📡 Go Live"; - liveBtn.title = "Stream to Twitch via WHIP (requires stream key)"; - liveBtn.style.background = "#6441a5"; - liveBtn.dataset.live = "0"; - } - }; - - await video.play(); - - // Start recording using existing infrastructure - var usePCM = session.pcm || false; - var configureRecording = { - bitrate: bitrate, - usePCM: usePCM, - audioOnly: false - }; - - log("Starting window recording at " + bitrate + " kbps" + (usePCM ? " with PCM audio" : "")); - recordLocalVideo("start", configureRecording, video); - - } catch (e) { - errorlog("Window capture failed: " + e); - if (session.recordWindowElement) { - session.recordWindowElement.remove(); - session.recordWindowElement = null; - } - // Reset button state on error/cancel - var btn = document.getElementById("recordWindowButton"); - if (btn) { - btn.innerHTML = "● Start Recording"; - btn.style.background = "#d00"; - btn.style.opacity = "1"; - btn.dataset.recording = "0"; - } - } -} - -function localGlobalRecordStart() { - document.querySelectorAll("[data-action-type='recorder-local']").forEach(target => { - var UUID = target.dataset.UUID; - if (!UUID) { - return; - } - var video = session.rpcs[UUID].videoElement; - if (!video) { - return; - } - if (!video.stopWriter) { - recordVideo(target); // if not started, start - } - }); - recordLocalVideo("start"); // self -} -function localGlobalRecordStop() { - document.querySelectorAll("[data-action-type='recorder-local']").forEach(target => { - var UUID = target.dataset.UUID; - if (!UUID) { - return; - } - var video = session.rpcs[UUID].videoElement; - if (!video) { - return; - } - if (video.stopWriter) { - recordVideo(target); // if started, stop - } - }); - recordLocalVideo("stop"); // self -} -async function remoteGlobalRecordStart() { - window.focus(); - var bitrate = await promptAlt(miscTranslations["what-bitrate"], false, false, 6000); - document.querySelectorAll("[data-action-type='recorder-remote']").forEach(target => { - requestVideoRecord(target, true, bitrate); - }); -} -function remoteGlobalRecordStop() { - document.querySelectorAll("[data-action-type='recorder-remote']").forEach(target => { - if (target.classList.contains("pressed")) { - requestVideoRecord(target, false); - } - }); -} -session.onTrack = function (event, UUID) { - if (session.badStreamList.includes(session.rpcs[UUID].streamID)) { - errorlog("new connection is contained in badStreamList 2! This shouldn't happen"); - // we will have none of this. - return; - } - - var newTracks = []; - var newStream = false; - if (event.streams && event.streams[0]) { - newStream = event.streams[0]; - newTracks = newStream.getTracks(); - } else if (event.track) { - newTracks.push(event.track); - } else { - errorlog("Something went wrong with incoming track.."); - return; - } - - if (session.rpcs[UUID].streamSrc) { - var tracks = session.rpcs[UUID].streamSrc.getTracks(); - for (var i = 0; i < newTracks.length; i++) { - for (var j = 0; j < tracks.length; j++) { - if (newTracks[i].id == tracks[j].id && newTracks[i].kind == tracks[j].kind) { - // FIX: Only replace if old track is dead (ended) - // This prevents audio clicks during normal operation - if (tracks[j].readyState === "ended") { - try { - session.rpcs[UUID].streamSrc.removeTrack(tracks[j]); - log("Replaced dead " + tracks[j].kind + " track"); - } catch(e) { warnlog(e); } - } else { - // Old track is still live - skip duplicate as before - newTracks.splice(i, 1); - i--; - } - break; - } - } - } - } - - var screenshare = false; - var screenshareParentOverride = null; - if (session.rpcs[UUID].screenIndexes && session.rpcs[UUID].getReceivers && session.rpcs[UUID].screenIndexes.length) { - log("session.rpcs[UUID].screenIndexes: " + session.rpcs[UUID].screenIndexes); - var receievers = session.rpcs[UUID].getReceivers(); // excluded - for (var i = 0; i < receievers.length; i++) { - for (var j = 0; j < newTracks.length; j++) { - if (receievers[i].track && receievers[i].track.id == newTracks[j].id && receievers[i].track.kind == newTracks[j].kind) { - for (var k = 0; k < session.rpcs[UUID].screenIndexes.length; k++) { - if (session.rpcs[UUID].screenIndexes[k] == i) { - screenshare = true; - break; - } - } - } - if (screenshare) { - break; - } - } - if (screenshare) { - break; - } - } - } - if (typeof UUID === "string" && UUID.endsWith("_screen")) { - if (!screenshare) { - screenshare = true; - } - if (session.rpcs[UUID] && session.rpcs[UUID].realUUID) { - screenshareParentOverride = session.rpcs[UUID].realUUID; - } else { - screenshareParentOverride = UUID.slice(0, -7); - } - if (session.rpcs[UUID]) { - session.rpcs[UUID].screenShareState = true; - if (typeof session.rpcs[UUID].smallScreen === "undefined" || session.rpcs[UUID].smallScreen === null) { - session.rpcs[UUID].smallScreen = false; - } - } - } - - if (screenshare) { - const parentUUID = screenshareParentOverride || (typeof UUID === "string" && UUID.endsWith("_screen") ? UUID.slice(0, -7) : UUID); - if (parentUUID && session.rpcs[parentUUID]) { - session.rpcs[parentUUID].screenShareState = true; - } - if (parentUUID && session.rpcs[parentUUID + "_screen"]) { - session.rpcs[parentUUID + "_screen"].screenShareState = true; - } - } - - log("screenshare: " + screenshare); - log(session.rpcs[UUID].streamID); - - try { - var index = newTracks.length; - while (index--) { - if (newTracks[index].kind == "video") { - if (session.novideo !== false && !session.novideo.includes(session.rpcs[UUID].streamID)) { - if (!(screenshare && session.novideo.includes(session.rpcs[UUID].streamID + ":s"))) { - newTracks.splice(index, 1); - } - continue; - } else if (session.rpcs[UUID].settings && session.rpcs[UUID].settings.allowscreenvideo && screenshare) { - //newTracks.splice(index,1); - continue; - } else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.video) { - newTracks.splice(index, 1); - continue; - } - } else if (newTracks[index].kind == "audio") { - if (session.noaudio !== false && !session.noaudio.includes(session.rpcs[UUID].streamID)) { - if (!(screenshare && session.noaudio.includes(session.rpcs[UUID].streamID + ":s"))) { - newTracks.splice(index, 1); - } - continue; - } else if (session.excludeaudio && session.excludeaudio.includes(session.rpcs[UUID].streamID)) { - newTracks.splice(index, 1); - continue; - } else if (session.rpcs[UUID].settings && session.rpcs[UUID].settings.allowscreenaudio && screenshare) { - //newTracks.splice(index,1); - continue; - } else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.audio) { - newTracks.splice(index, 1); - continue; - } - } - } - } catch (e) { - errorlog(e); - } - - if (!newTracks.length) { - warnlog("NO NEW TRACKS?"); - return; - } - - if (session.encodedInsertableStreams && session.rpcs[UUID] && session.rpcs[UUID].getReceivers) { - var receievers = session.rpcs[UUID].getReceivers(); // excluded - for (var i = 0; i < receievers.length; i++) { - for (var j = 0; j < newTracks.length; j++) { - if (receievers[i].track && receievers[i].track.id == newTracks[j].id && receievers[i].track.kind == newTracks[j].kind) { - try { - setupReceiverTransform(receievers[i]); - } catch (e) { - errorlog(e); - } - } - } - } - } - - if (screenshare) { - var targetUUID = screenshareParentOverride || UUID; - if (session.rpcs[targetUUID]) { - session.setupScreenShareAddon(newTracks, targetUUID); - } else { - session.setupScreenShareAddon(newTracks, UUID); - } - return; - } - - //if (session.buffer!==false){ - playoutdelay(UUID); - //} - - session.directorSpeakerMute(); // apply any mute states to new tracks. - session.directorDisplayMute(); - - if (newStream) { - newStream.onremovetrack = function (e1) { - try { - warnlog("Track was removed"); - session.rpcs[UUID].streamSrc.getTracks().forEach(trk => { - if (trk.id == e1.track.id && trk.kind == e1.track.kind) { - session.rpcs[UUID].streamSrc.removeTrack(trk); - } - }); - if (e1.track.kind == "video") { - updateIncomingVideoElement(UUID, true, false); - } else { - updateIncomingVideoElement(UUID, false, true); - } - // updateIncomingVideoElement(UUID); // session.rpcs[UUID].videoElement.srcObject = session.rpcs[UUID].streamSrc; - setTimeout(function () { - updateMixer(); - }, 1); - } catch (e) { } - }; - - newStream.onerror = function (e1) { - errorlog(e1); - try { - warnlog("Track threw an error; going to reconnect it"); - session.rpcs[UUID].streamSrc.getTracks().forEach(trk => { - try { - if (trk.id == e1.track.id && trk.kind == e1.track.kind) { - session.rpcs[UUID].streamSrc.removeTrack(trk); - } - } catch (e) { } - }); - if (e1.track.kind == "video") { - updateIncomingVideoElement(UUID, true, false); - } else { - updateIncomingVideoElement(UUID, false, true); - } - setTimeout(function () { - updateMixer(); - }, 1); - } catch (e) { - errorlog(e); - } - }; - } - - createRichVideoElement(UUID); - - if (!session.rpcs[UUID].streamSrc) { - session.rpcs[UUID].streamSrc = createMediaStream(); - mediaSourceUpdated(UUID, session.rpcs[UUID].streamID); - } - - var videoAdded = false; - var audioAdded = false; - - newTracks.forEach(trk => { - if (trk.kind == "video") { - videoAdded = true; - } else if (trk.kind == "audio") { - audioAdded = true; - } - log("adding track"); - session.rpcs[UUID].streamSrc.addTrack(trk); - }); - - if (newTracks.length > session.rpcs[UUID].streamSrc.getTracks().length) { - errorlog("Not all the tracks were added to the local stream; are the tracks' IDs not unique?"); - console.log("streamSrc total tracks: " + session.rpcs[UUID].streamSrc.getTracks().length); - } - - if (isIFrame && session.sendframes) { - sendFrameHandler(newTracks, UUID); - } - - if (audioAdded && videoAdded) { - updateIncomingVideoElement(UUID); - } else if (videoAdded) { - updateIncomingVideoElement(UUID, true, false); - } else if (audioAdded) { - updateIncomingVideoElement(UUID, false, true); - if (!session.roomid && session.view && !session.permaid) { - setTimeout(function () { - updateMixer(); - }, 10); // video already has an auto-start, with aspect ratio size change. audio doesn't. - } - } - - if (session.twilio && audioAdded) { - session.twilio.updateMixer(UUID); - } - return session; -}; - -function sendFrameHandler(tracks, UUID = null) { - tracks.forEach(async trk => { - if (trk.kind !== "video") return; - - log("STARTING NEW SEND STREAM VIDEO TRACK"); - - const startImageStream = async () => { - - log("startImageStream"); - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d", { willReadFrequently: true }); - const processor = new MediaStreamTrackProcessor(trk); - const reader = processor.readable.getReader(); - - while (true) { - try { - const { done, value: frame } = await reader.read(); - if (done) break; - try { - canvas.width = frame.displayWidth; - canvas.height = frame.displayHeight; - ctx.drawImage(frame, 0, 0); - const format = typeof session.sendframes === "string" ? session.sendframes : "webp"; - const imageData = canvas.toDataURL(`image/${format}`, 0.8); - - parent.postMessage({ - type: 'frame', - frame: imageData, - UUID, - streamID: (session.rpcs[UUID] ? session.rpcs[UUID].streamID : null), - trackID: trk.id, - kind: trk.kind, - format: format - }, session.sendframes); - } finally { - frame.close(); - } - } catch (e) { - console.error("Error processing image frame:", e); - break; - } - } - }; - - const startFrameStream = async () => { - log("startFrameStream"); - const processor = new MediaStreamTrackProcessor(trk); - const reader = processor.readable.getReader(); - - while (true) { - try { - const { done, value } = await reader.read(); - if (done) { - if (value) value.close(); - break; - } - - try { - parent.postMessage({ - frame: value, - UUID, - streamID: (session.rpcs[UUID] ? session.rpcs[UUID].streamID : null), - trackID: trk.id, - kind: trk.kind, - type: "frame" - }, session.sendframes, [value]); - } finally { - value.close(); - } - } catch (e) { - console.error("Error processing video frame:", e); - if (e.name === "DataCloneError") { - // Fall back to image stream if frame transfer fails - return startImageStream(); - } - break; - } - } - }; - - try { - if (typeof MediaStreamTrackProcessor === 'function') { - try { - new SharedArrayBuffer(1); - await startFrameStream(); - } catch (e) { - console.warn(e); - await startImageStream(); - } - } else { - await startImageStream(); - } - } catch (e) { - console.error("Stream processing failed:", e); - } - }); -} - -function updateIncomingVideoElement(UUID, video = true, audio = true) { - if (!session.rpcs[UUID].videoElement) { - return; - } - if (!session.rpcs[UUID].streamSrc) { - return; - } - - if (!session.rpcs[UUID].videoElement.srcObject) { - session.rpcs[UUID].videoElement.srcObject = createMediaStream(); - } - - if (video) { - var tracks = session.rpcs[UUID].videoElement.srcObject.getVideoTracks(); // add video track - - session.rpcs[UUID].streamSrc.getVideoTracks().forEach(trk => { - var added = false; - tracks.forEach(trk2 => { - if (trk.id == trk2.id && trk.kind == trk2.kind) { - added = true; - } - }); - if (!added) { - session.rpcs[UUID].videoElement.srcObject.getVideoTracks().forEach(trk2 => { - // make sure only one video track is added at a time. - log("removetrack"); - session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2); - }); - - if (trk.muted && trk.kind == "video" && session.director) { - trk.onunmute = function (e) { - if (!session.rpcs[UUID]) { - return; - } - this.onunmute = null; - warnlog("ON UN-MUTE"); - updateIncomingVideoElement(UUID, true, false); - }; - } else { - if (session.rpcs[UUID].videoElement.controls) { - session.rpcs[UUID].videoElement.controls = session.showControls || false; - if (session.showControls === null) { - setTimeout( - function (ele) { - if (ele) { - ele.controls = true; - } - }, - 500, - session.rpcs[UUID].videoElement - ); - } - } - session.rpcs[UUID].videoElement.srcObject.addTrack(trk); - mediaVideoTrackUpdated(UUID, session.rpcs[UUID].streamID); - } - } - }); - - if ((session.motionSwitch || session.motionRecord) && !session.rpcs[UUID].motionDetectionInterval) { - session.rpcs[UUID].motionDetectionInterval = setTimeout(function () { - setInterval(function () { - motionDetection(session.rpcs[UUID].videoElement, session.motionSwitch || session.motionRecord); - }, 400); - }, 2000); - } - } - if (audio) { - updateIncomingAudioElement(UUID); // do the same for audio now. - if (!video) { - if (session.rpcs[UUID] && session.rpcs[UUID].videoElement) { - // this bit of code fixes an issue where the volume button doesn't show, after adding an audio track for the first time - var pastMuteState = session.rpcs[UUID].videoElement.muted; - session.rpcs[UUID].videoElement.muted = !pastMuteState; - session.rpcs[UUID].videoElement.muted = pastMuteState; - } - } - } - if ((session.optimize === 0) && session.activatedStreams.size && session.rpcs[UUID] && session.rpcs[UUID].streamID) { - if (session.activatedStreams.has(session.rpcs[UUID].streamID)) { - if (session.activatedStreamsQueue[session.rpcs[UUID].streamID]) { - let msgs = session.activatedStreamsQueue[session.rpcs[UUID].streamID]; - delete session.activatedStreamsQueue[session.rpcs[UUID].streamID]; - msgs.forEach(msgWithTime => { - log(msgWithTime); - if (msgWithTime.time && (msgWithTime.time > Date.now() - 10000)) { - session.directorActions(msgWithTime.msg); - } - }); - } - } - } -} - -function updateIncomingAudioElement(UUID) { - // this can be called when turning on/off inbound audio processing. - if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].streamSrc) { - return; - } - - if (!session.rpcs[UUID].videoElement.srcObject) { - session.rpcs[UUID].videoElement.srcObject = createMediaStream(); - } - - log("updateIncomingAudioElement: " + UUID); - if (session.audioEffects === true || session.pushLoudness || (session.rpcs[UUID].isolatedChannel !== undefined)) { - var tracks = session.rpcs[UUID].streamSrc.getAudioTracks(); - if (tracks.length) { - var track = tracks[0]; - track = addAudioPipeline(UUID, track); - log(track); - var added = false; - var tracks2 = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); - log(tracks2); - tracks2.forEach(trk2 => { - if (trk2.label && trk2.label == "MediaStreamAudioDestinationNode") { - // an old morphed node; delete it. - session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2); - } else if (track.id == trk2.id && track.kind == trk2.kind) { - // maybe it didn't morph; already added either way - added = true; - } else if (tracks[0].id == trk2.id && tracks[0].kind == trk2.kind && track.id != tracks[0].id) { - // remove original audio track that is now morphed - session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2); - } - }); - if (!added) { - session.rpcs[UUID].videoElement.srcObject.addTrack(track); - mediaAudioTrackUpdated(UUID, session.rpcs[UUID].streamID); - } - } else { - session.rpcs[UUID].videoElement.srcObject.getAudioTracks().forEach(trk => { - // make sure to remove all tracks. - session.rpcs[UUID].videoElement.srcObject.remove(trk); - }); - } - } else { - var expected = []; - tracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); // add audio tracks - session.rpcs[UUID].streamSrc.getAudioTracks().forEach(trk => { - var added = false; - tracks.forEach(trk2 => { - if (trk.id == trk2.id && trk.kind == trk2.kind) { - added = true; - expected.push(trk2); // - } - }); - if (!added) { - session.rpcs[UUID].videoElement.srcObject.addTrack(trk); - mediaAudioTrackUpdated(UUID, session.rpcs[UUID].streamID); - } - }); - tracks.forEach(trk => { - var added = false; - expected.forEach(trk2 => { - if (trk.id == trk2.id && trk.kind == trk2.kind) { - added = true; - } - }); - if (!added) { - // not expected. so lets delete. - warnlog("this shouldn't happen that often, audio track orphaned. removing it"); - session.rpcs[UUID].videoElement.srcObject.removeTrack(trk); - } - }); - } - - if (session.mixMinus) { - stream = mixMinusAudio(UUID); // only works with p2p; no chunked mode. - } -} - -function cycleStyleOptions() { - session.style += 1; - if (session.style > 6) { - session.style = 1; - } else if (session.style == 4) { - session.style = 5; - } - - for (var UUID in session.rpcs) { - if (session.rpcs[UUID].canvas) { - try { - if (session.rpcs[UUID].canvas) { - session.rpcs[UUID].canvas.remove(); - } - } catch (e) { } - session.rpcs[UUID].canvas = null; - } - updateIncomingAudioElement(UUID); - } - updateMixer(); -} - -function addAudioPipeline(UUID, track) { - // INBOUND AUDIO EFFECTS ; audio tracks only - try { - if (session.disableViewerWebAudioPipeline) { - log("ignoring addAudioPipeline - disableViewerWebAudioPipeline is enabled (noap)"); - return track; - } - - log("Triggered webaudio effects path"); - - for (var tid in session.rpcs[UUID].inboundAudioPipeline) { - delete session.rpcs[UUID].inboundAudioPipeline[tid]; // get rid of old nodes. - } - var trackid = track.id; // this is an audio track, or should be. - - session.rpcs[UUID].inboundAudioPipeline[trackid] = {}; - - session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream = createMediaStream(); - session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream.addTrack(track); - - if (ChromiumVersion && session.audioEffects) { - // I'm going to deprecate this. - session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio = createAudioElement(); // TODO: I don't know if this mutedAudio thing matters any more, in recent versions of Chrome, since it won't play even if muted. - session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = true; - session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.playsinline = true; // ## Added Oct 9th 2022. Not sure it's does anything, but might help with iPhones? - session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.srcObject = session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream; // needs to be added as an streamed element to be usable, even if its hidden - session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = true; - //session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.volume = 0.01; - session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio - .play() - .then(_ => { - //session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = false; - log("playing 1"); - }) - .catch(warnlog); - } - - // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaStreamTrackSource - var source = session.audioCtx.createMediaStreamSource(session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream); - - ////////////////// - - var screwedUp = false; - session.rpcs[UUID].inboundAudioPipeline[trackid].destination = false; - - if (session.rpcs[UUID].isolatedChannel !== undefined) { - log("Isolating channel: " + session.rpcs[UUID].isolatedChannel); - session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); - source = isolateChannel(source, session.rpcs[UUID].isolatedChannel); - screwedUp = true; - } - - if (session.sync !== false) { - log("adding a delay node to audio"); - source = addDelayNode(source, UUID, trackid); - screwedUp = true; - } - if (session.style === 2) { - log("adding a fftwave node to audio"); - try { - if (session.rpcs[UUID].inboundAudioPipeline[trackid]) { - // clear audioMeterGuest, if active. - clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval); - } - } catch (e) { } - source = fftWaveform(source, UUID, trackid); - } else if (session.style === 3 || session.meterStyle) { - log("adding a loudness meter node to audio"); - source = audioMeterGuest(source, UUID, trackid); - } else if (session.audioMeterGuest) { - log("adding a loudness meter node to audio"); - source = audioMeterGuest(source, UUID, trackid); - } else if (session.activeSpeaker) { - log("adding a loudness meter node to audio"); - source = audioMeterGuest(source, UUID, trackid); - } else if (session.quietOthers) { - log("adding a loudness meter node to audio"); - source = audioMeterGuest(source, UUID, trackid); - } else if (session.pushLoudness) { - source = audioMeterGuest(source, UUID, trackid); - } else { - try { - if (session.rpcs[UUID].inboundAudioPipeline[trackid]) { - // nothign active, so clear - clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval); - } - } catch (e) { } - } - - if (session.playChannel) { - session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); - source = selectChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.playChannel); - screwedUp = true; - } else if (session.rpcs[UUID].channelOffset !== false) { - log("custom offset set"); - session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); - source = offsetChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.rpcs[UUID].channelOffset, session.rpcs[UUID].channelWidth); - screwedUp = true; - } else if (session.offsetChannel !== false) { - // proably better to do this last. - log("adding offset channels"); - session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); - source = offsetChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.offsetChannel, session.channelWidth); - screwedUp = true; - } else if (session.panning !== false) { - // proably better to do this last. - log("adding offset channels"); - session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); - source = stereoPanning(source, UUID, trackid, session.panning); - screwedUp = true; - } else if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.manualSink) { - screwedUp = true; // added June-3-22 to allow for custom outputs to different audio output destinations. - } - - if (screwedUp) { - warnlog("screwedUp mode activated. dun dun"); - if (session.rpcs[UUID].inboundAudioPipeline[trackid].destination === false) { - session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); - } - source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].destination); - - try { - if (session.firstPlayTriggered && session.audioCtx.state == "suspended") { - log("trying to resume.."); - session.audioCtx.resume(); - } - } catch (e) { - warnlog("session.audioCtx.resume(); failed"); - } - - return session.rpcs[UUID].inboundAudioPipeline[trackid].destination.stream.getAudioTracks()[0]; - } - - try { - if (session.firstPlayTriggered && session.audioCtx.state == "suspended") { - session.audioCtx.resume(); - } - } catch (e) { - warnlog("session.audioCtx.resume(); failed 2"); - } - - return track; - } catch (e) { - errorlog(e); - } - return track; -} - -function processMiniInfoUpdate(miniInfo, UUID) { - if ("qlr" in miniInfo) { - session.rpcs[UUID].stats.info.quality_limitation_reason = miniInfo.qlr; - } - - if ("con" in miniInfo) { - session.rpcs[UUID].stats.info.conn_type = miniInfo.con; - } - - if ("cpu" in miniInfo) { - session.rpcs[UUID].stats.info.cpuLimited = miniInfo.cpu; - if (session.rpcs[UUID].signalMeter) { - if (miniInfo.cpu) { - session.rpcs[UUID].signalMeter.dataset.cpu = "1"; - } else if ("cpu" in miniInfo) { - session.rpcs[UUID].signalMeter.dataset.cpu = "0"; - } - } - } - - if ("hw_enc" in miniInfo) { - session.rpcs[UUID].stats.info.hardware_video_encoder = miniInfo.hw_enc; - } - - if ("bat" in miniInfo) { - if (typeof miniInfo.bat == "number") { - session.rpcs[UUID].stats.info.power_level = miniInfo.bat * 100; - } else { - session.rpcs[UUID].stats.info.power_level = null; - } - } - if ("chrg" in miniInfo) { - session.rpcs[UUID].stats.info.plugged_in = miniInfo.chrg; - } - - if ("out" in miniInfo && "c" in miniInfo.out) { - session.rpcs[UUID].stats.info.total_outbound_p2p_connections = miniInfo.out.c; - if (session.showConnections && session.rpcs[UUID].connectionDetails) { - session.rpcs[UUID].connectionDetails.innerText = "🔗" + session.rpcs[UUID].stats.info.total_outbound_p2p_connections; - session.rpcs[UUID].connectionDetails.dataset.value = session.rpcs[UUID].stats.info.total_outbound_p2p_connections; - } - } - - if (session.rpcs[UUID].batteryMeter) { - batteryMeterInfoUpdate(UUID); - } -} - -function batteryMeterInfoUpdate(UUID) { - if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.power_level !== null) { - var level = session.rpcs[UUID].batteryMeter.querySelector(".battery-level"); - if (level) { - var value = session.rpcs[UUID].stats.info.power_level; - if (value > 100) { - value = 100; - } - if (value < 0) { - value = 0; - } - level.style.height = parseInt(value) + "%"; - if (value < 15) { - session.rpcs[UUID].batteryMeter.classList.remove("warn"); - session.rpcs[UUID].batteryMeter.classList.add("alert"); - } else if (value < 25) { - session.rpcs[UUID].batteryMeter.classList.remove("alert"); - session.rpcs[UUID].batteryMeter.classList.add("warn"); - } else { - session.rpcs[UUID].batteryMeter.classList.remove("alert"); - session.rpcs[UUID].batteryMeter.classList.remove("warn"); - } - if (value < 100) { - session.rpcs[UUID].batteryMeter.classList.remove("hidden"); - } - //session.rpcs[UUID].batteryMeter.title = value+"% battery remaining"; - session.rpcs[UUID].batteryMeter.title = parseInt(value) + "% battery remaining"; - } - } - - if (session.rpcs[UUID].stats.info && "plugged_in" in session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.plugged_in === false) { - session.rpcs[UUID].batteryMeter.dataset.plugged = "0"; - session.rpcs[UUID].batteryMeter.classList.remove("hidden"); - } else { - session.rpcs[UUID].batteryMeter.dataset.plugged = "1"; - // add on - session.rpcs[UUID].batteryMeter.title = parseInt(value) + "% charging"; - session.rpcs[UUID].batteryMeter.classList.add("hidden"); - } -} - -function setupGuestLabelControl(UUID) { - var labelID = getById("label_" + UUID); - if (labelID) { - labelID.classList.add("contolboxLabel"); - labelID.dataset.UUID = UUID; - - if (session.rpcs[UUID].label) { - labelID.innerText = session.rpcs[UUID].label; // Replace underscores with a Space when publishing to HTML. No Double spaces. - labelID.classList.remove("addALabel"); - } else if (session.directorUUID === UUID) { - miniTranslate(labelID, "main-director"); - labelID.classList.remove("addALabel"); - } else if (session.directorList.indexOf(UUID) >= 0) { - miniTranslate(labelID, "co-director"); - labelID.classList.remove("addALabel"); - } else { - miniTranslate(labelID, "add-a-label"); - labelID.classList.add("addALabel"); - } - labelID.onclick = async function (ee) { - var oldlabel = ee.target.innerText; - if (session.rpcs[ee.target.dataset.UUID].label === false) { - oldlabel = ""; - } - window.focus(); - var newlabel = await promptAlt(getTranslation("new-display-name"), false, false, oldlabel); - if (newlabel !== null) { - newlabel = newlabel.trim(); - if (newlabel == "") { - newlabel = false; - if (session.directorUUID === UUID) { - miniTranslate(ee.target, "main-director"); - //ee.target.innerHTML = getTranslation("main-director"); - ee.target.classList.remove("addALabel"); - } else if (session.directorList.indexOf(UUID) >= 0) { - miniTranslate(ee.target, "co-director"); - //ee.target.innerHTML = getTranslation("co-director"); - ee.target.classList.remove("addALabel"); - } else { - miniTranslate(ee.target, "add-a-label"); - //ee.target.innerHTML = getTranslation("add-a-label"); - ee.target.classList.add("addALabel"); - } - } else { - ee.target.innerText = newlabel; - ee.target.classList.remove("addALabel"); - } - var data = {}; - data.UUID = ee.target.dataset.UUID; - data.changeLabel = true; - data.value = newlabel; - session.sendRequest(data, data.UUID); - - // Update local label immediately and sync to co-directors - try { - session.rpcs[UUID].label = newlabel; - session.rpcs[UUID].labelSetByDirector = true; // Prevent info message from overwriting - // Push label update to co-directors via directorState sync - if (session.director && session.rpcs[UUID].streamID) { - var labelPayload = { directorState: {} }; - labelPayload.directorState[session.rpcs[UUID].streamID] = getDetailedState(session.rpcs[UUID].streamID); - session.pushDirectorStateUpdate(labelPayload, "label-change"); - } - } catch (e) { errorlog(e); } - - pokeAPI("details", getDetailedState(session.rpcs[UUID].streamID)); - } - - }; - } - updateAriaLabel(UUID); -} - -function updateAriaLabel(UUID = false) { - if (!(UUID in session.rpcs)) { - return; - } - - var element = document.getElementById("container_" + UUID); - if (!element) { - return; - } - var sid = ""; - if (session.rpcs[UUID].streamID) { - sid = ": " + session.rpcs[UUID].streamID; - } - - if (session.rpcs[UUID].label) { - element.setAttribute("aria-label", session.rpcs[UUID].label); - } else if (session.directorUUID === UUID) { - element.setAttribute("aria-label", (getTranslation("main-director") || "Main Director") + sid); - } else if (session.directorList.indexOf(UUID) >= 0) { - element.setAttribute("aria-label", (getTranslation("co-director") || "Co Director") + sid); - } else if (sid) { - element.setAttribute("aria-label", "ID" + sid); - } else { - element.setAttribute("aria-label", getTranslation("undefined") || "Undefined"); - } - - element.setAttribute("role", "region"); -} - -function updateLabelDirectors(UUID) { - var elements = getById("label_" + UUID); - if (elements) { - if (session.rpcs[UUID].label) { - elements.innerText = session.rpcs[UUID].label; - elements.classList.remove("addALabel"); - } else if (session.directorUUID === UUID) { - miniTranslate(elements, "main-director"); - elements.classList.remove("addALabel"); - } else if (session.directorList.indexOf(UUID) >= 0) { - miniTranslate(elements, "co-director"); - elements.classList.remove("addALabel"); - } else { - miniTranslate(elements, "add-a-label"); - elements.classList.add("addALabel"); - } - } - updateAriaLabel(UUID); -} - -function updateLabelDirectors2(UUID) { - var elements = getById("label_" + UUID); - if (elements) { - if (session.directorUUID === UUID) { - miniTranslate(elements, "main-director"); - elements.classList.remove("addALabel"); - } else if (session.directorList.indexOf(UUID) >= 0) { - miniTranslate(elements, "co-director"); - elements.classList.remove("addALabel"); - } else { - miniTranslate(elements, "add-a-label"); - elements.classList.add("addALabel"); - } - } - updateAriaLabel(UUID); -} - -function directorCoDirectorColoring(UUID) { - if (UUID === session.directorUUID) { - try { - session.rpcs[UUID].stats.info.director = true; - getById("container_" + UUID).classList.add("directorBox"); - } catch (e) { } - } else if (session.directorList.indexOf(UUID) >= 0) { - try { - session.rpcs[UUID].stats.info.coDirector = true; - addDirectorBlue(UUID); - } catch (e) { } - } -} - -function addDirectorBlue(UUID) { - try { getById("container_" + UUID).classList.add("directorBlue"); log("[ui] addDirectorBlue for UUID=" + UUID); } catch (e) { errorlog(e); } -} - -function soloLinkGeneratorInit(UUID) { - document.querySelectorAll("container_" + UUID).forEach(ele => { - ele.querySelectorAll("[data-sololink]").forEach(ele2 => { - // value='" + soloLink + "' href='" + soloLink + "'/>" + soloLink + " - var soloLink = soloLinkGenerator(session.rpcs[UUID].streamID, false); - ele2.value = soloLink; - ele2.href = soloLink; - ele2.innerText = soloLink; - }); - }); -} - -function initRecordingImpossible(UUID) { - var ele = document.querySelectorAll('[data-action-type="mute-guest"][data--u-u-i-d="' + UUID + '"]'); - if (ele) { - ele.disabled = true; - ele.title = getTranslation("Audio processing is disabled with this guest. Can't mute or change volume"); - } - var ele = document.querySelectorAll('[data-action-type="volume"][data--u-u-i-d="' + UUID + '"]'); - if (ele) { - ele.disabled = true; - ele.title = title = getTranslation("Audio processing is disabled with this guest. Can't mute or change volume"); - ele.style.opacity = 0.2; - } -} - -function initGroupButtons(UUID) { - var elements = document.querySelectorAll('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"]'); - for (var i = 0; i < elements.length; i++) { - elements[i].classList.remove("pressed"); - elements[i].ariaPressed = "false"; - for (var g = 0; g < session.rpcs[UUID].group.length; g++) { - if (elements[i].dataset.group === session.rpcs[UUID].group[g]) { - elements[i].classList.add("pressed"); - elements[i].ariaPressed = "true"; - } - } - } -} - -function changeGroupDirector(ele, state = null) { - var group = ele.dataset.group; - - var index = session.group.indexOf(group); - - var change = false; - - if (state === true) { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - if (index === -1) { - session.group.push(group); - change = true; - } - } else if (state === false) { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - if (index > -1) { - session.group.splice(index, 1); - change = true; - } - } else if (ele.classList.contains("pressed")) { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - if (index > -1) { - session.group.splice(index, 1); - change = true; - } - } else { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - if (index === -1) { - session.group.push(group); - change = true; - } - } - - if (session.group.length || session.allowNoGroup) { - session.sendMessage({ group: session.group.join(",") }); - } else { - session.sendMessage({ group: false }); - } - if (change) { - updateMixer(); - pokeIframeAPI("group-set-updated", session.group); - } - - if (session.group.indexOf(group) === -1) { - return false; - } else { - return true; - } -} - -function changeGroupDirectorAPI(group, state = null, update = true) { - log("changeGroupDirectorAPI()"); - group = sanitizeLabel(group); - - if (document.getElementById("container_director")) { - var ele = getById("container_director").querySelector('[data-action-type="toggle-group"][data-group="' + group + '"]'); - if (ele) { - if (update) { - ele.click(); - } else if (state === true) { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } - if (session.group.indexOf(group) === -1) { - return false; - } else { - return true; - } - } - } - - var index = session.group.indexOf(group); - - var eleGroup = getById("groups"); - eleGroup.classList.remove("hidden"); - var ele = eleGroup.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"'); - - if (eleGroup.showDirector) { - if (!ele) { - ele = htmlToElement('"); - - var added = false; - eleGroup.querySelectorAll("[data-group]").forEach(ele2 => { - log(ele2); - if (!added && ele2.dataset.group > group + "") { - ele2.parentNode.insertBefore(ele, ele2); - added = true; - } - }); - if (!added) { - eleGroup.appendChild(ele); - } - } - } else if (!ele) { - ele = document.createElement("div"); - ele.dataset.actionType = "toggle-group"; - ele.dataset.group = group; - ele.classList.add("float"); - ele.style.display = "inline-block"; - ele.role = "button"; - ele.innerHTML = '
    ' + group; - eleGroup.appendChild(ele); - ele.onclick = function () { - changeGroupDirectorAPI(this.dataset.group); - }; - } - var changed = false; - - if (state === true) { - if (eleGroup.showDirector) { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } else { - ele.classList.add("green"); - ele.ariaPressed = "true"; - } - if (index === -1) { - session.group.push(group); - changed = true; - } - } else if (state === false) { - if (eleGroup.showDirector) { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } else { - ele.classList.remove("green"); - ele.ariaPressed = "false"; - } - if (index > -1) { - session.group.splice(index, 1); - changed = true; - } - } else if (ele.classList.contains("green")) { - if (eleGroup.showDirector) { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } else { - ele.classList.remove("green"); - ele.ariaPressed = "false"; - } - if (index > -1) { - session.group.splice(index, 1); - changed = true; - } - } else { - if (eleGroup.showDirector) { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } else { - ele.classList.add("green"); - ele.ariaPressed = "true"; - } - if (index === -1) { - session.group.push(group); - changed = true; - } - } - if (update) { - if (session.group.length || session.allowNoGroup) { - session.sendMessage({ group: session.group.join(",") }); - } else { - session.sendMessage({ group: false }); - } - } - if (changed) { - updateMixer(); - pokeIframeAPI("group-set-updated", session.group); - } - - if (state !== null) { - return true; - } else if (session.group.indexOf(group) === -1) { - return false; - } else { - return true; - } -} - -function changeGroupViewDirectorAPI(group, state = null) { - log("changeGroupViewDirectorAPI()"); - group = sanitizeLabel(group); - - var index = session.groupView.indexOf(group); - - var changed = false; - - if (state === true) { - if (index === -1) { - session.groupView.push(group); - changed = true; - } - } else if (state === false) { - if (index > -1) { - session.groupView.splice(index, 1); - changed = true; - } - } else { - if (index > -1) { - session.groupView.splice(index, 1); - } else { - session.groupView.push(group); - } - changed = true; - } - - if (changed) { - updateMixer(); - pokeIframeAPI("group-view-set-updated", session.groupView); - } - - if (state !== null) { - return true; - } else if (session.groupView.indexOf(group) === -1) { - return false; - } else { - return true; - } -} - -function changeGroup(ele, state = null) { - var group = ele.dataset.group; - - var index = session.rpcs[ele.dataset.UUID].group.indexOf(group); - - var changed = false; - - if (state === true) { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - if (index === -1) { - session.rpcs[ele.dataset.UUID].group.push(group); - changed = true; - } - } else if (state === false) { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - if (index > -1) { - session.rpcs[ele.dataset.UUID].group.splice(index, 1); - changed = true; - } - } else if (ele.classList.contains("pressed")) { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - if (index > -1) { - session.rpcs[ele.dataset.UUID].group.splice(index, 1); - changed = true; - } - } else { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - if (index === -1) { - session.rpcs[ele.dataset.UUID].group.push(group); - changed = true; - } - } - if (session.rpcs[ele.dataset.UUID].group.length) { - session.sendRequest({ group: session.rpcs[ele.dataset.UUID].group.join(",") }, ele.dataset.UUID); - } else { - session.sendRequest({ group: false }, ele.dataset.UUID); - } - syncDirectorState(ele); - - if (changed) { - updateMixer(); - } - - if (session.rpcs[ele.dataset.UUID].group.indexOf(group) === -1) { - return false; - } else { - return true; - } -} - -function changeChannelOffset(UUID, channel) { - var ele = document.querySelectorAll('[data-action-type="add-channel"][data--u-u-i-d="' + UUID + '"]'); - for (var i = 0; i < ele.length; i++) { - if (channel === i) { - if (ele[i].classList.contains("pressed")) { - ele[i].classList.remove("pressed"); - ele[i].ariaPressed = "false"; - channel = false; - } else { - ele[i].classList.add("pressed"); - ele[i].ariaPressed = "true"; - } - } else { - ele[i].classList.remove("pressed"); - ele[i].ariaPressed = "false"; - } - } - session.rpcs[UUID].channelOffset = channel; - - updateIncomingVideoElement(UUID, false, true); - if (channel === false) { - return false; - } else { - return true; - } -} - -function toggleMonoStereoMic(ele) { - if (ele.checked) { - session.audioInputChannels = 1; - - getById("micStereoMonoInput").checked = true; - getById("micStereoMonoInput3").checked = true; - } else if (urlParams.has("channelcount") || urlParams.has("ac") || urlParams.has("inputchannels")) { - // not ideal having this here - session.audioInputChannels = urlParams.get("channelcount") || urlParams.get("ac") || urlParams.get("inputchannels") || 0; - session.audioInputChannels = parseInt(session.audioInputChannels); - if (!session.audioInputChannels) { - session.audioInputChannels = false; - } - getById("micStereoMonoInput").checked = false; - getById("micStereoMonoInput3").checked = false; - } else { - session.audioInputChannels = false; - getById("micStereoMonoInput").checked = false; - getById("micStereoMonoInput3").checked = false; - } - try { - activatedPreview = false; - if (ele.id == "micStereoMonoInput3") { - grabAudio("#audioSource3"); - } else { - grabAudio("#audioSource"); - } - } catch (e) { - errorlog(e); - } -} - -function selectChannel(destination, source, channel) { - session.audioCtx.destination.channelCountMode = "explicit"; - session.audioCtx.destination.channelInterpretation = "discrete"; - destination.channelCountMode = "explicit"; - destination.channelInterpretation = "discrete"; - - try { - destination.channelCount = 1; - } catch (e) { - errorlog("Max channels: " + destination.channelCount); - } - - var splitter = session.audioCtx.createChannelSplitter(6); - var merger = session.audioCtx.createChannelMerger(1); // mono - - source.connect(splitter); - splitter.connect(merger, channel - 1, 0); - - return merger; -} - -function offsetChannel(destination, source, offset, width = false) { - session.audioCtx.destination.channelCountMode = "explicit"; - session.audioCtx.destination.channelInterpretation = "discrete"; - destination.channelCountMode = "explicit"; - destination.channelInterpretation = "discrete"; - - try { - destination.channelCount = session.audioChannels; - } catch (e) { - errorlog("Max channels: " + destination.channelCount); - } - - if (width) { - var splitter = session.audioCtx.createChannelSplitter(width); - var merger = session.audioCtx.createChannelMerger(width + offset); - } else { - var splitter = session.audioCtx.createChannelSplitter(2); - var merger = session.audioCtx.createChannelMerger(2 + offset); - } - - source.connect(splitter); - splitter.connect(merger, 0, offset); - - if (session.stereo && session.stereo != 3) { - splitter.connect(merger, 1, 1 + offset); - } - return merger; -} - -function addReverb(source, UUID, trackid, value) { - // not yet actually working. requires a buffer; bleh! - if (value === true) { - value = Math.random() * (Math.random() * 2 - 1); - errorlog(value); - } else if (value === false) { - return source; - } else { - value = parseFloat(value / 90) - 1 || 0; - if (value < -1) { - value = -1; - } - if (value > 1) { - value = 1; - } - } - //// some reverb logic goes here... - ///var reverbNode = session.audioCtx.createStereoPanner(); - ///session.rpcs[UUID].inboundAudioPipeline[trackid].reverbNode = reverbNode; - //// - - source.connect(reverbNode); - return reverbNode; -} - -function stereoPanning(source, UUID, trackid, value) { - // Normalize value to [-1, 1] where 0=center - if (parseInt(value) === -1) { - value = Math.random() * (Math.random() * 2 - 1); - warnlog(value); - } else if (value === false) { - return source; - } else if (value === true) { - value = 90; - } else { - // input 0..180 => -1..1 - value = parseFloat(value / 90) - 1 || 0; - } - if (value < -1) value = -1; - if (value > 1) value = 1; - - // Pre-pan gain trim to avoid clipping - var gainNode = session.audioCtx.createGain(); - session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode = gainNode; - gainNode.gain.value = 1 - Math.abs(value) / 2; - source.connect(gainNode); - - // Create panner with Safari fallback - var panNode; - try { - if (session.audioCtx.createStereoPanner) { - panNode = session.audioCtx.createStereoPanner(); - session.rpcs[UUID].inboundAudioPipeline[trackid].panType = "stereo"; - panNode.pan.value = value; - } else { - panNode = session.audioCtx.createPanner(); - panNode.panningModel = "equalpower"; - panNode.distanceModel = "inverse"; - var x = value; - var z = 1 - Math.abs(value); - try { - if (typeof panNode.positionX !== "undefined") { - panNode.positionX.value = x; - panNode.positionY.value = 0; - panNode.positionZ.value = z; - } else if (panNode.setPosition) { - panNode.setPosition(x, 0, z); - } - } catch (e) { } - session.rpcs[UUID].inboundAudioPipeline[trackid].panType = "panner"; - } - } catch (e) { - warnlog("Stereo panning node creation failed; bypassing"); - return gainNode; - } - - session.rpcs[UUID].inboundAudioPipeline[trackid].panNode = panNode; - gainNode.connect(panNode); - return panNode; -} - -function adjustPan(UUID, value) { - if (value === true) { - value = Math.random() * (Math.random() * 2 - 1); - } else if (value === false) { - value = 0; - } else { - value = parseFloat(value / 90) - 1 || 0; - if (value < -1) { - value = -1; - } - if (value > 1) { - value = 1; - } - } - - for (var trackid in session.rpcs[UUID].inboundAudioPipeline) { - if ("panNode" in session.rpcs[UUID].inboundAudioPipeline[trackid]) { - try { - if (session.rpcs[UUID].inboundAudioPipeline[trackid].panType === "stereo" && session.rpcs[UUID].inboundAudioPipeline[trackid].panNode.pan) { - session.rpcs[UUID].inboundAudioPipeline[trackid].panNode.pan.setValueAtTime(value, session.audioCtx.currentTime); - } else { - // Fallback panner - var x = value; - var z = 1 - Math.abs(value); - var pn = session.rpcs[UUID].inboundAudioPipeline[trackid].panNode; - if (typeof pn.positionX !== "undefined") { - pn.positionX.setValueAtTime(x, session.audioCtx.currentTime); - pn.positionY.setValueAtTime(0, session.audioCtx.currentTime); - pn.positionZ.setValueAtTime(z, session.audioCtx.currentTime); - } else if (pn.setPosition) { - pn.setPosition(x, 0, z); - } - } - } catch (e) { warnlog(e); } - } - if ("gainPanNode" in session.rpcs[UUID].inboundAudioPipeline[trackid] && session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode.gain) { - try { session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode.gain.setValueAtTime(1 - Math.abs(value) / 2, session.audioCtx.currentTime); } catch (e) { } - } - } -} - -function addDelayNode(source, UUID, trackid) { - // append the delay Node to the track??? WOULD THIS WORK? - - var delay = parseFloat(session.sync) || 0; - if (delay < 0) { - delay = 0; - } - - if ((session.audioBuffer !== false) && session.audioBuffer >= 0) { - delay += parseFloat(session.audioBuffer); - } else if (session.buffer && session.buffer > 0) { - delay += parseFloat(session.buffer); - } - - delay = delay / 1000; - - session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode = session.audioCtx.createDelay(delay + 5); // 5 seconds additionally added for the purpose of flexibility - - session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode.delayTime.value = delay; // delayTime takes it in seconds. - source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode); - log("added new delay node"); - return session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode; -} - -function createStyleCanvas(UUID) { - // append the delay Node to the track??? WOULD THIS WORK? - if (!session.rpcs[UUID].canvas) { - // just make sure that if using &effects or something, to null the canvas after use, else this won't trigger. - session.rpcs[UUID].canvas = document.createElement("canvas"); - session.rpcs[UUID].canvas.dataset.UUID = UUID; - if (session.rpcs[UUID].streamID) { - session.rpcs[UUID].canvas.dataset.sid = session.rpcs[UUID].streamID; - } - session.rpcs[UUID].canvas.style.pointerEvents = "auto"; - session.rpcs[UUID].canvasCtx = session.rpcs[UUID].canvas.getContext("2d", { alpha: session.alpha }); - // - session.rpcs[UUID].canvas.addEventListener("click", function (e) { - // show stats of video if double clicked - log("clicked"); - try { - var uid = e.currentTarget.dataset.UUID; - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - if (session.statsMenu !== false) { - if ("stats" in session.rpcs[uid]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, uid); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); - } - } - e.stopPropagation(); - return false; - } else if ("prePausedBandwidth" in session.rpcs[uid]) { - unPauseVideo(e.currentTarget); - } - } catch (e) { - errorlog(e); - } - }); - - if (session.statsMenu) { - if ("stats" in session.rpcs[UUID]) { - var [menu, innerMenu] = statsMenuCreator(); - printViewStats(innerMenu, UUID); - menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID); - } - } - - if (session.aspectRatio) { - if (session.aspectRatio == 1) { - session.rpcs[UUID].canvas.width = "720"; - session.rpcs[UUID].canvas.height = "1280"; - } else if (session.aspectRatio == 2) { - session.rpcs[UUID].canvas.width = "960"; - session.rpcs[UUID].canvas.height = "960"; - } else if (session.aspectRatio == 3) { - session.rpcs[UUID].canvas.width = "1280"; - session.rpcs[UUID].canvas.height = "960"; - } - } else { - session.rpcs[UUID].canvas.width = "1280"; - session.rpcs[UUID].canvas.height = "720"; - } - - updateMixer(); - return true; - } else { - return false; - } -} - -function applyStyleEffect(UUID) { - if (!session.rpcs[UUID].canvas || !session.rpcs[UUID].canvasCtx) { - return; - } - - /* session.rpcs[UUID].canvasContainer = document.createElement("div"); - session.rpcs[UUID].canvasContainer.appendChild(session.rpcs[UUID].canvas); - session.rpcs[UUID].canvas.style = "width:100%;height:100%;display:block;"; - session.rpcs[UUID].canvasContainer.appendChild(session.rpcs[UUID].videoElement); */ - - if (session.style == 3) { - // black - session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0, 0, 0)"; - session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height); - } else if (session.style == 4) { - session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0, 0, 0)"; - session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height); - } else if (session.style == 5) { - var r = Math.random() * 255; - var g = Math.random() * 255; - var b = Math.random() * 255; - session.rpcs[UUID].canvasCtx.fillStyle = "rgb(" + r + ", " + g + ", " + b + ")"; - session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height); - } else if (session.style == 6) { - session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0,0,0)"; - session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height); - - var r = Math.random() * 150 + 50; - var g = Math.random() * 150 + 50; - var b = Math.random() * 150 + 50; - session.rpcs[UUID].canvasCtx.fillStyle = "rgb(" + r + ", " + g + ", " + b + ")"; - session.rpcs[UUID].canvasCtx.beginPath(); - session.rpcs[UUID].canvasCtx.arc(parseInt(session.rpcs[UUID].canvas.width / 2), parseInt(session.rpcs[UUID].canvas.height / 2), parseInt(session.rpcs[UUID].canvas.height / 4), 0, 2 * Math.PI, false); - session.rpcs[UUID].canvasCtx.fill(); - - if (session.rpcs[UUID].label) { - session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0,0,0)"; - session.rpcs[UUID].canvasCtx.textAlign = "center"; - session.rpcs[UUID].canvasCtx.font = parseInt(session.rpcs[UUID].canvas.height / 2.11) + "px Arial"; - session.rpcs[UUID].canvasCtx.fillText(session.rpcs[UUID].label[0].toUpperCase(), parseInt(session.rpcs[UUID].canvas.width / 2), parseInt((session.rpcs[UUID].canvas.height * 2) / 3)); - } else { - var tmp = getComputedStyle(document.querySelector(":root")).getPropertyValue("--video-background-image").split('"'); - if (tmp.length === 3) { - var img = new Image(); - img.onload = function () { - session.rpcs[UUID].canvasCtx.fillStyle = "rgb(25,0,0)"; - session.rpcs[UUID].canvasCtx.drawImage(img, parseInt(session.rpcs[UUID].canvas.width / 2 - 110), parseInt(session.rpcs[UUID].canvas.height / 2 - 110), 220, 220); - }; - img.src = tmp[1]; - } - } - } -} - -function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); -} - -function fftWaveform(source, UUID, trackid) { - // append the delay Node to the track??? WOULD THIS WORK? - // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser = session.audioCtx.createAnalyser(); - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.fftSize = 512; - var bufferLength = session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.frequencyBinCount; - var dataArray = new Uint8Array(bufferLength); - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteTimeDomainData(dataArray); - // analyser.getByteTimeDomainData(dataArray); - source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser); - - createStyleCanvas(UUID); - clearInterval(session.rpcs[UUID].canvasIntervalAction); - var canvasIntervalAction = setInterval( - function (uuid) { - if (session.style !== 2) { - clearInterval(canvasIntervalAction); // this is FFT only, so okay to kill. - return; - } - - try { - session.rpcs[uuid].inboundAudioPipeline[trackid].analyser.getByteTimeDomainData(dataArray); - session.rpcs[uuid].canvasCtx.fillStyle = "rgba(0, 0, 0, 0.1)"; - session.rpcs[uuid].canvasCtx.fillRect(0, 0, session.rpcs[uuid].canvas.width, session.rpcs[uuid].canvas.height); - session.rpcs[uuid].canvasCtx.lineWidth = 10; - session.rpcs[uuid].canvasCtx.strokeStyle = "rgb(111, 255, 111)"; - - var sliceWidth = (session.rpcs[uuid].canvas.width * 1.0) / bufferLength; - - var loudness = dataArray; - var Squares = loudness.map(val => (val - 128.0) * (val - 128.0)); - var Sum = Squares.reduce((acum, val) => acum + val); - var Mean = Sum / loudness.length; - loudness = Math.sqrt(Mean) * 10; - session.rpcs[uuid].stats.Audio_Loudness = parseInt(loudness); - - if (session.pushLoudness == true) { - var loudnessObj = {}; - loudnessObj[session.rpcs[uuid].streamID] = session.rpcs[uuid].stats.Audio_Loudness; - - if (isIFrame) { - parent.postMessage({ loudness: loudnessObj, action: "loudness", value: loudness, UUID: uuid }, session.iframetarget); - } - } - - if (loudness < 2) { - return; - } - - //log(bufferLength); - session.rpcs[uuid].canvasCtx.beginPath(); - var m = session.rpcs[uuid].canvas.height / 256.0; - session.rpcs[uuid].canvasCtx.moveTo(0, dataArray[0] * m); - var x = 0; - for (var i = 1; i < bufferLength; i++) { - var y = dataArray[i] * m; - session.rpcs[uuid].canvasCtx.lineTo(x, y); - x += sliceWidth; - } - session.rpcs[uuid].canvasCtx.lineTo(session.rpcs[uuid].canvas.width, session.rpcs[uuid].canvas.height / 2); - session.rpcs[uuid].canvasCtx.stroke(); - } catch (e) { - warnlog(e); - warnlog("Did the remote source disconnect?"); - clearInterval(canvasIntervalAction); - warnlog(session.rpcs[uuid]); - } - }, - 50, - UUID - ); - session.rpcs[UUID].canvasIntervalAction = canvasIntervalAction; - return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser; -} - -function audioMeterGuest(mediaStreamSource, UUID, trackid) { - log("audioMeterGuest started"); - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser = session.audioCtx.createAnalyser(); - mediaStreamSource.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser); - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.fftSize = 256; - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.smoothingTimeConstant = 0.05; - - var bufferLength = session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.frequencyBinCount; - var dataArray = new Uint8Array(bufferLength); - - function updateLevels() { - try { - if (!session.rpcs[UUID]) { - return; - } - - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteFrequencyData(dataArray); - var total = 0; - for (var i = 0; i < dataArray.length; i++) { - total += dataArray[i]; - } - total = parseInt(total / 150); - session.rpcs[UUID].stats.Audio_Loudness = total; - - if (session.pushLoudness == true) { - var loudnessObj = {}; - loudnessObj[session.rpcs[UUID].streamID] = session.rpcs[UUID].stats.Audio_Loudness; - - if (isIFrame) { - parent.postMessage({ loudness: loudnessObj, action: "loudness", value: session.rpcs[UUID].stats.Audio_Loudness, UUID: UUID }, session.iframetarget); - } - } - - try { - clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval); - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () { - updateLevels(); - }, 100); - } catch (e) { - log("closing old inaudio pipeline"); - } - - if (session.style == 3 || session.meterStyle) { - // overrides style - if (session.rpcs[UUID].videoElement) { - if (total > 40) { - session.rpcs[UUID].videoElement.dataset.speaking = "2"; - } else if (total > 10) { - session.rpcs[UUID].videoElement.dataset.speaking = "1"; - } else { - session.rpcs[UUID].videoElement.dataset.speaking = "0"; - } - - if (session.meterStyle == 4) { - session.rpcs[UUID].videoElement.dataset.loudness = total; - return; // this is cause we are using the data-loudness - } - } else if (session.meterStyle == 4) { - return; - } - } else if (session.scene !== false) { - // if a scene, cancel - return; - } else if (session.audioMeterGuest === false) { - // don't show if we just want the volume levels - return; - } - - if (session.rpcs[UUID].voiceMeter) { - session.rpcs[UUID].voiceMeter.dataset.level = total; - if (session.meterStyle == 1) { - var perct = Math.min(total, 100); - - session.rpcs[UUID].voiceMeter.style.height = perct + "%"; - if (total > 80) { - var R = parseInt((255 * perct) / 100) - .toString(16) - .padStart(2, "0"); - var G = parseInt(255 - (255 * perct) / 100) - .toString(16) - .padStart(2, "0"); - session.rpcs[UUID].voiceMeter.style.backgroundColor = "#" + R + G + "00"; - } else { - session.rpcs[UUID].voiceMeter.style.backgroundColor = "#00FF00"; - } - } else { - if (total > 15) { - session.rpcs[UUID].voiceMeter.style.opacity = 100; // temporary - } else { - session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary - } - } - } else { - session.rpcs[UUID].voiceMeter = document.createElement("div"); - session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID; - session.rpcs[UUID].voiceMeter.dataset.level = total; - if (session.meterStyle == 1) { - session.rpcs[UUID].voiceMeter.classList.add("video-meter2"); - } else { - if (total > 15) { - session.rpcs[UUID].voiceMeter.style.opacity = 100; // temporary - } else { - session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary - } - if (session.meterStyle == 2) { - session.rpcs[UUID].voiceMeter.classList.add("video-meter-2"); - } else { - session.rpcs[UUID].voiceMeter.classList.add("video-meter"); - } - } - updateMixer(); - } - } catch (e) { - warnlog(e); - // fail as an exception; this is a control close. - return; - } - } - clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval); - session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () { - updateLevels(); - }, 100); - return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser; -} - -function effectsDynamicallyUpdate(event, ele) { - log("effectsDynamicallyUpdate"); - let lastEffectValue = session.effect; - session.effect = ele.options[ele.selectedIndex].value; - - getById("selectImageContent").style.display = "none"; - getById("selectImageContent3").style.display = "none"; - getById("selectImageOverlay").style.display = "none"; - getById("selectImageOverlay3").style.display = "none"; - getById("selectEffectAmount").style.display = "none"; - getById("selectEffectAmount3").style.display = "none"; - - if (session.effect === "1") { - updateRenderOutpipe(); - return; - } - - if (session.effect === "7") { - // Show zoom amount sliders - getById("selectEffectAmount").style.display = "block"; - getById("selectEffectAmount3").style.display = "block"; - - // Show both sets of position controls - getById("zoomPositionControls").style.display = "block"; - getById("zoomPositionControls3").style.display = "block"; - - if (session.effectValue_default) { - session.effectValue = session.effectValue_default; - } else { - session.effectValue = 1; - } - - // Set up zoom amount sliders - const zoomInputs = ["selectEffectAmountInput", "selectEffectAmountInput3"]; - zoomInputs.forEach(id => { - const input = getById(id); - if (input) { - input.min = 1; - input.max = 4; - input.step = 0.1; - input.value = session.effectValue; - } - }); - - // Initialize all position sliders - const sliderPairs = [ - ["zoomPositionX1", "zoomPositionX"], - ["zoomPositionY1", "zoomPositionY"] - ]; - - sliderPairs.forEach(pair => { - pair.forEach(id => { - const slider = getById(id); - if (slider) { - if (id.includes('X')) { - slider.value = xPosition; - } else { - slider.value = yPosition; - } - } - }); - }); - } else { - // Hide all position controls - getById("zoomPositionControls").style.display = "none"; - getById("zoomPositionControls3").style.display = "none"; - } - - if (["8", "overlay"].includes(session.effect)) { - // like zoom but none - if (session.effect === "overlay") { - loadOverlayImages(); - } - updateRenderOutpipe(); - return; - } - - if (session.effect == "3a") { - session.effect = "3"; - session.effectValue = 5; - } - if (session.effectValue_default === false && session.effect == "3") { - session.effectValue = 2; - } else { - session.effectValue = session.effectValue_default; - } - - if (session.effect == "0" || !session.effect) { - updateRenderOutpipe(); - return; - } else if (session.effect === "3" || session.effect === "4") { - if (!["3", "4", "5"].includes(lastEffectValue)) { - attemptTFLiteJsFileLoad(); - if (!session.tfliteModule.looping) { - updateRenderOutpipe(); - } - } - if (session.effect === "3" && session.effectValue_default == false) { - getById("selectEffectAmount").style.display = "block"; - getById("selectEffectAmount3").style.display = "block"; - - getById("selectEffectAmountInput").min = 0; - getById("selectEffectAmountInput").max = 20; - getById("selectEffectAmountInput").step = 1; - getById("selectEffectAmountInput3").min = 0; - getById("selectEffectAmountInput3").max = 20; - getById("selectEffectAmountInput3").step = 1; - - getById("selectEffectAmountInput").value = session.effectValue; - getById("selectEffectAmountInput3").value = session.effectValue; - } - } else if (session.effect === "5") { - if (!["3", "4", "5"].includes(lastEffectValue)) { - attemptTFLiteJsFileLoad(); - if (!session.tfliteModule.looping) { - updateRenderOutpipe(); - } - } - loadContentEffectsImages(); - } else if ((session.effect === "14" || session.effect === "15") && session.effectValue_default == false) { - getById("selectEffectAmount").style.display = "block"; - getById("selectEffectAmount3").style.display = "block"; - - getById("selectEffectAmountInput").min = 1; - getById("selectEffectAmountInput").max = 50; - getById("selectEffectAmountInput").step = 1; - getById("selectEffectAmountInput3").min = 1; - getById("selectEffectAmountInput3").max = 50; - getById("selectEffectAmountInput3").step = 1; - - getById("selectEffectAmountInput").value = parseInt(session.effectValue) || 25; - getById("selectEffectAmountInput3").value = parseInt(session.effectValue) || 25; - - loadContentEffectsImages(); - } else if (session.effect === "6") { - if (!gpgpuSupport) { - if (!session.cleanOutput) { - warnUser("Hardware acceleration isn't detected.

    This effect will not work", 4000, false); - return; - } - } else if (gpgpuSupport == "Google SwiftShader") { - if (!session.cleanOutput) { - warnUser("Hardware acceleration isn't detected.

    Please enable it for this effect to work correctly.

    Settings -> Advanced -> System -> Use hardware-accleration", false, false); - } - return; - } - loadTensorflowJS(); - updateRenderOutpipe(); - //mainMeshMask(); - } else { - //loadEffect(session.effect); - updateRenderOutpipe(); - } - - if (session.permaid === false && session.roomid === false && session.view === false && session.director === false) { - updateURL("effects"); - } -} - -function loadContentEffectsImages() { - if (!["5", "15"].includes(session.effect)) { - return; - } // only load for certain effects - - if (session.defaultBackgroundImages) { - try { - session.defaultBackgroundImages.reverse(); - } catch (e) { - errorlog("Could not process image list"); - session.defaultBackgroundImages = false; - session.selectedImage_contents = getById("selectImage_contents"); - return; - } - session.defaultBackgroundImages.forEach(imgSrc => { - try { - var img = document.createElement("img"); - img.onerror = function () { - this.style.display = "none"; - }; // hide images that fail to load - img.crossOrigin = "Anonymous"; - img.src = imgSrc; - img.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;"; - img.onclick = function (event) { - changeEffectsImage(event, this); - }; - getById("selectImage_contents").prepend(img); - } catch (e) { } - }); - session.defaultBackgroundImages = false; - session.selectedImage_contents = getById("selectImage_contents"); - } else if (!session.selectedImage_contents) { - session.selectedImage_contents = getById("selectImage_contents"); - } - if (document.getElementById("selectImageContent")) { - document.getElementById("selectImageContent").style.display = "block"; - document.getElementById("selectImageContent").appendChild(session.selectedImage_contents); - session.selectedImage_contents.classList.remove("hidden"); - } else if (document.getElementById("selectImageContent3")) { - document.getElementById("selectImageContent3").style.display = "block"; - document.getElementById("selectImageContent3").appendChild(session.selectedImage_contents); - session.selectedImage_contents.classList.remove("hidden"); - } -} - -async function changeOverlayImage(ev, ele) { - if (ele.files && ele.files[0]) { - if (session.foregroundImg) { - session.foregroundImg.classList.remove("selectedContentEffectsImage"); - } - session.foregroundImg = document.createElement("img"); - session.foregroundImg.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;"; - session.foregroundImg.onclick = function (event) { - changeEffectsImage(event, this); - }; - ele.parentNode.parentNode.insertBefore(session.foregroundImg, ele.parentNode); - session.foregroundImg.onload = () => { - URL.revokeObjectURL(session.foregroundImg.src); // no longer needed, free memory - }; - session.foregroundImg.src = URL.createObjectURL(ele.files[0]); // set src to blob url - session.foregroundImg.classList.add("selectedContentEffectsImage"); - } else if (ele.tagName.toLowerCase() == "img") { - if (session.foregroundImg) { - session.foregroundImg.classList.remove("selectedContentEffectsImage"); - } - session.foregroundImg = ele; - session.foregroundImg.classList.add("selectedContentEffectsImage"); - } -} -function loadOverlayImages() { - if (session.defaultForegroundImages) { - try { - session.defaultForegroundImages.reverse(); - } catch (e) { - errorlog("Could not process image list"); - session.defaultForegroundImages = false; - session.selectImageOverlay_contents = getById("selectImageOverlay_contents"); - return; - } - session.defaultForegroundImages.forEach(imgSrc => { - try { - var img = document.createElement("img"); - img.onerror = function () { - this.style.display = "none"; - }; // hide images that fail to load - img.crossOrigin = "Anonymous"; - img.src = imgSrc; - img.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;"; - img.onclick = function (event) { - changeOverlayImage(event, this); - }; - getById("selectImageOverlay_contents").prepend(img); - } catch (e) { } - }); - session.defaultForegroundImages = false; - session.selectImageOverlay_contents = getById("selectImageOverlay_contents"); - } else if (!session.selectImageOverlay_contents) { - session.selectImageOverlay_contents = getById("selectImageOverlay_contents"); - } - if (document.getElementById("selectImageOverlay")) { - document.getElementById("selectImageOverlay").style.display = "block"; - document.getElementById("selectImageOverlay").appendChild(session.selectImageOverlay_contents); - session.selectImageOverlay_contents.classList.remove("hidden"); - } else if (document.getElementById("selectImageOverlay3")) { - document.getElementById("selectImageOverlay3").style.display = "block"; - document.getElementById("selectImageOverlay3").appendChild(session.selectImageOverlay_contents); - session.selectImageOverlay_contents.classList.remove("hidden"); - } -} - -var effectsLoaded = {}; -var JEELIZFACEFILTER = null; -async function loadEffect(effect) { - warnlog("effect:" + effect); - var filename = effect.replace(/\W/g, ""); - if (effectsLoaded[filename]) { - effectsLoaded[filename](); - return; - } else { - effectsLoaded[filename] = function () { }; - } - warnlog("Loading Effect: " + effect); - var script = document.createElement("script"); - script.onload = async function () { - log("LOADED EFFECT"); - effectsLoaded[filename] = await effectsEngine(filename); - log("effectsEngine();"); - if (gpgpuSupport == "Google SwiftShader") { - if (!session.cleanOutput) { - warnUser("Hardware acceleration isn't detected.

    Please enable it for better performance.

    Settings -> Advanced -> System -> Use hardware-accleration", false, false); - } - } - effectsLoaded[filename](); - }; - script.src = "./filters/" + filename + ".js?" + parseInt(1000 * Math.random()); - document.head.appendChild(script); - warnUser("Loading custom effects model...", 1000); -} - -async function loadScript(url, callback = false) { - var res = null; - var rej = null; - var promise = new Promise((resolve, reject) => { - res = resolve; - rej = reject; - }); - - var check = document.querySelector("script[src='" + url + "']"); - if (check) { - if (callback) { - callback(); - } - } else { - var script = document.createElement("script"); - script.type = "text/javascript"; - script.src = url; - script.onload = function () { - res(); - if (callback) { - callback(); - } - }; - document.head.appendChild(script); - } - return await promise; -} - -var tokenClient = false; -function YoutubeChatInterface(remote = false) { - // this lets us query Youtube for chat messages, but its quota limited :( - if (!tokenClient) { - tokenClient = true; - } else { - return; - } - - var gisInited = false; - var gapiInited = false; - var busy = 0; - - function handleAuthClick() { - tokenClient.callback = async resp => { - if (resp.error) { - errorlog(resp.error); - } - closeModal(); - var auths = gapi.client.getToken(); - if (auths) { - setStorage("YoutubeAuth", JSON.stringify(auths), auths.expires_in || 3600); - } - listBroadcasts(); - }; - var saved = getStorage("YoutubeAuth"); - - if (saved) { - gapi.client.setToken(JSON.parse(saved)); - listBroadcasts(); - } else if (gapi.client.getToken() === null) { - if (remote) { - tokenClient.requestAccessToken({ prompt: "consent" }); - } else { - warnUser("", false, false); - } - } else { - if (remote) { - tokenClient.requestAccessToken({ prompt: "" }); - } else { - warnUser("", false, false); - } - } - } - - function maybeEnableButtons() { - if (gapiInited && gisInited) { - handleAuthClick(); - } - } - - async function initializeGapiClient() { - await gapi.client.init({ - apiKey: session.youtubeKey.split(",")[1], - discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest"] - }); - gapiInited = true; - maybeEnableButtons(); - } - - function handleSignoutClick() { - let token = gapi.client.getToken(); - if (token !== null) { - google.accounts.oauth2.revoke(token.access_token); - gapi.client.setToken(""); - } - } - - async function listBroadcasts() { - try { - var response = await gapi.client.youtube.liveBroadcasts.list({ - broadcastStatus: "active" - }); - } catch (err) { - errorlog(err); - return; - } - - let broadcasts = response.result.items; - if (!broadcasts || broadcasts.length == 0) { - return; - } - broadcasts.forEach(broadcast => { - setTimeout( - function (liveChatId) { - listMessages(liveChatId); - busy += 1; - }, - 1000, - broadcast.snippet.liveChatId - ); - }); - } - - async function listMessages(liveChatId, pageToken = false) { - try { - if (pageToken) { - var response = await gapi.client.youtube.liveChatMessages.list({ - liveChatId: liveChatId, - part: ["id", "snippet", "authorDetails"], - pageToken: pageToken - }); - } else { - var response = await gapi.client.youtube.liveChatMessages.list({ - liveChatId: liveChatId, - part: ["id", "snippet", "authorDetails"] - }); - } - - var messages = response.result.items; - messages.forEach(msg => { - pokeIframeAPI("YoutubeChat", msg); - }); - - var polling = response.result.pollingIntervalMillis; - var pageToken = response.result.nextPageToken; - - if (busy > 1) { - // popular eh? Lets quickly check for more. - } else if (busy > 0) { - // a message ! hurrah - if (polling < 2000) { - polling = 2000; - } // Was it just luck? - } else if (polling < 5000) { - polling = 5000; // let's not spam the api, cause we know there isn't anything waiting.. - } - busy = 0; // reset - setTimeout( - function (liveChatId, pageToken) { - listMessages(liveChatId, pageToken); - }, - polling, - liveChatId, - pageToken - ); - } catch (err) { - return; - } - } - - function gisLoaded() { - tokenClient = google.accounts.oauth2.initTokenClient({ - client_id: session.youtubeKey.split(",")[0], - scope: "https://www.googleapis.com/auth/youtube", - callback: "" - }); - gisInited = true; - maybeEnableButtons(); - } - function gapiLoaded() { - gapi.load("client", initializeGapiClient); - } - - loadScript("https://apis.google.com/js/api.js", gapiLoaded); - loadScript("https://accounts.google.com/gsi/client", gisLoaded); -} - -function loadTensorflowJS() { - if (session.TFJSModel != null) { - return; - } - log("loadTensorflowJS()"); - session.TFJSModel = true; - var script = document.createElement("script"); - var script2 = document.createElement("script"); - var script3 = document.createElement("script"); - var script4 = document.createElement("script"); - script.onload = function () { - document.head.appendChild(script2); - }; - script2.onload = function () { - document.head.appendChild(script3); - }; - script3.onload = function () { - document.head.appendChild(script4); - }; - script4.onload = function () { - async function loadModel() { - session.TFJSModel = await faceLandmarksDetection.load(faceLandmarksDetection.SupportedPackages.mediapipeFacemesh); - closeModal(); - warnUser("Almost done loading model...", 3000); - } - loadModel(); - }; - script.src = "./thirdparty/tfjs/tf-core.js"; - script2.src = "./thirdparty/tfjs/tf-converter.js"; - script3.src = "./thirdparty/tfjs/tf-backend-webgl.js"; - script4.src = "./thirdparty/tfjs/face-landmarks-detection.js"; - warnUser("Downloading a big effects model... may take a minute", 15000); - - script.type = "text/javascript"; - script2.type = "text/javascript"; - script3.type = "text/javascript"; - script4.type = "text/javascript"; - document.head.appendChild(script); -} - -var TFLITELOADING = false; -function attemptTFLiteJsFileLoad() { - if (session.tfliteModule !== false) { - return true; - } - warnUser("Loading effects model..."); - TFLITELOADING = true; - session.tfliteModule = {}; - - if (!document.getElementById("tflitesimdjs")) { - var tmpScript = document.createElement("script"); - tmpScript.onload = loadTFLiteModel; - tmpScript.type = "text/javascript"; - tmpScript.src = "./thirdparty/tflite/tflite-simd.js?ver=2"; - tmpScript.id = "tflitesimdjs"; - document.head.appendChild(tmpScript); - } - - return false; -} -async function changeEffectsImage(ev, ele) { - if (ele.files && ele.files[0]) { - if (session.effectsImage) { - session.effectsImage.classList.remove("selectedContentEffectsImage"); - } - session.effectsImage = document.createElement("img"); - session.effectsImage.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;"; - session.effectsImage.onclick = function (event) { - changeEffectsImage(event, this); - }; - ele.parentNode.parentNode.insertBefore(session.effectsImage, ele.parentNode); - session.effectsImage.onload = () => { - URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory - }; - session.effectsImage.src = URL.createObjectURL(ele.files[0]); // set src to blob url - session.effectsImage.classList.add("selectedContentEffectsImage"); - } else if (ele.tagName.toLowerCase() == "img") { - if (session.effectsImage) { - session.effectsImage.classList.remove("selectedContentEffectsImage"); - } - session.effectsImage = ele; - session.effectsImage.classList.add("selectedContentEffectsImage"); - } -} - -async function changeEffectAmount(ev, ele) { - session.effectValue = ele.value; - if (ele.id === "selectEffectAmountInput") { - getById("selectEffectAmountInput3").value = ele.value; - } - log("session.effectValue: " + session.effectValue); -} -async function loadTFLiteModel() { - try { - if (session.tfliteModule && session.effectsImage) { - var img = session.effectsImage; - session.tfliteModule = await createTFLiteSIMDModule(); - session.effectsImage = img; - } else { - session.tfliteModule = {}; - session.tfliteModule = await createTFLiteSIMDModule(); - } - if (!session.tfliteModule.simd) { - var elements = document.querySelectorAll("[data-warnSimdNotice]"); - for (let i = 0; i < elements.length; i++) { - elements[i].style.display = "inline-block"; - } - } - } catch (e) { - warnlog("TF-LITE FAILED TO LOAD"); - closeModal(); - return; - } - const modelResponse = await fetch("./thirdparty/tflite/segm_full_v679.tflite"); - session.tfliteModule.model = await modelResponse.arrayBuffer(); - - session.tfliteModule.HEAPU8.set(new Uint8Array(session.tfliteModule.model), session.tfliteModule._getModelBufferMemoryOffset()); - session.tfliteModule._loadModel(session.tfliteModule.model.byteLength); - session.tfliteModule.activelyProcessing = false; - TFLITELOADING = false; - closeModal(); - if (LaunchTFWorkerCallback) { - TFLiteWorker(); - } -} -function smdInfo() { - warnUser("For improved performance, use Chrome v87 or newer with SIMD support enabled.
    Enable SIMD here: chrome://flags/#enable-webassembly-simd", false, false); -} - -async function startPublishing() { - if (query("#publishOutURL input[type='text']").dataset.twitch == "true") { - session.whipOutput = "https://g.webrtc.live-video.net:4443/v2/offer"; - } else { - session.whipOutput = query("#publishOutURL input[type='text']").value || session.whipOutput || null; - } - - if (!session.whipOutput) { - warnUser("Please first provided an output destination", 2500); - return; - } - - if (!session.whipOutputToken) { - session.whipOutputToken = query("#publishOutToken input[type='password']").value || false; - } - - if (!session.whipOutputToken && query("#publishOutURL input[type='text']").dataset.twitch == "true") { - warnUser("Please enter a Twitch stream token first", 2000); - return; - } - getById("publishSettings").classList.add("hidden"); - - if (!getById("whipoutvbrcbr").classList.contains("hidden")) { - if (getById("whipoutvbrcbr").value === "cbr") { - session.cbr = 1; - } else { - session.cbr = 0; - } - } - if (!getById("whipoutdenoise").classList.contains("hidden")) { - if (getById("whipoutdenoise").value === "1") { - session.noiseSuppression = true; - } else { - session.noiseSuppression = false; - } - } - if (!getById("whipoutisolation").classList.contains("hidden")) { - if (getById("whipoutisolation").value === "1") { - session.voiceIsolation = true; - } else { - session.voiceIsolation = false; - } - } - - if (!getById("whipoutautogain").classList.contains("hidden")) { - if (getById("whipoutautogain").value === "1") { - session.autoGainControl = true; - } else { - session.autoGainControl = false; - } - } - if (!getById("whipoutstereo").classList.contains("hidden")) { - if (getById("whipoutstereo").value === "1") { - session.stereo = 1; - } else { - session.stereo = 0; - } - } - if (!getById("whipoutbitrateGroupFlag").classList.contains("hidden")) { - session.whipOutVideoBitrate = parseInt(getById("whipoutbitrateGroupFlag").value); - } - if (!getById("whipoutaudiobitrate").classList.contains("hidden")) { - session.whipOutAudioBitrate = parseInt(getById("whipoutaudiobitrate").value); - } - - var ret = await publishScreen(); - if (ret) { - getById("publishSettings").classList.add("hidden"); - resizeWindow(1280, 720); - document.title = "PUBLISHING🔴" + document.title; - } else { - getById("publishSettings").classList.remove("hidden"); - } - return ret; -} - -async function startRecording() { - session.recordLocal = session.recordLocal || 6000; - - var ret = await publishScreen(); - if (ret) { - getById("publishSettings").classList.add("hidden"); - resizeWindow(1280, 720); - document.title = "RECORDING🔴" + document.title; - recordLocalVideoToggle(); - } else { - getById("publishSettings").classList.remove("hidden"); - } -} - -function twitchSelect(ele) { - if (ele.checked) { - //query("#publishOutURL input[type='text']").value = - query("#publishOutURL input[type='text']").disabled = true; - query("#publishOutURL input[type='text']").classList.add("disable"); - query("#publishOutURL input[type='text']").dataset.twitch = "true"; - - query("#publishOutToken input[type='password']").placeholder = "Twitch stream token here"; - } else { - query("#publishOutURL input[type='text']").disabled = null; - query("#publishOutURL input[type='text']").classList.remove("disable"); - delete getById("publishOutURL").disabled; - query("#publishOutURL input[type='text']").dataset.twitch = "false"; - query("#publishOutToken input[type='password']").placeholder = "WHIP auth token here"; - } -} - -function resizeWindow(width, height) { - if (window.outerWidth) { - window.resizeTo(width + (window.outerWidth - window.innerWidth), height + (window.outerHeight - window.innerHeight)); - } else { - window.resizeTo(500, 500); - window.resizeTo(width + (500 - document.body.offsetWidth), height + (500 - document.body.offsetHeight)); - } - - setInterval(function () { - if (window.innerWidth / window.innerHeight > 17 / 9 && window.innerWidth / window.innerHeight < 15 / 9) { - return; - } - if (window.outerWidth) { - window.resizeTo(width + (window.outerWidth - window.innerWidth), height + (window.outerHeight - window.innerHeight)); - } else { - window.resizeTo(500, 500); - window.resizeTo(width + (500 - document.body.offsetWidth), height + (500 - document.body.offsetHeight)); - } - }, 5000); -} - -// compress SDP -/* -function compressSDP(sdp) { - // Extract critical values - const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1]; - const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1]; - - // Extract fingerprint (removing colons) - const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/); - const fingerprint = fingerprintMatch[1].replace(/:/g, ''); - - // Extract ICE candidates if they exist - const candidates = []; - const candidateRegex = /a=candidate:([^\r\n]+)/g; - let candidateMatch; - while ((candidateMatch = candidateRegex.exec(sdp)) !== null) { - const parts = candidateMatch[1].split(' '); - - // Skip IPv6 candidates - if (parts[4].includes(':')) continue; - - // Only keep foundation, component, protocol, priority, ip, port, type and related values - const foundation = parts[0]; - const component = parts[1]; - const protocol = parts[2].toLowerCase() === 'udp' ? 'u' : 't'; - const priority = parseInt(parts[3]); - const ip = parts[4]; - const port = parseInt(parts[5]); - const type = parts[7]; - - // Encode type: host=h, srflx=s, relay=r - let typeCode; - switch(type) { - case 'host': typeCode = 'h'; break; - case 'srflx': typeCode = 's'; break; - case 'relay': typeCode = 'r'; break; - default: typeCode = 'x'; - } - - // Compact representation of candidate - // Format: foundation,component,protocol,priority(shortened),ip,port,type - candidates.push( - `${foundation},${component},${protocol},${priorityToCompact(priority)},${ip},${port},${typeCode}` - ); - } - - // Convert fingerprint from hex to a more compact representation - const compactFingerprint = hexToCompact(fingerprint); - - // Build the compressed string - // Format: C(version)|ufrag|pwd|fingerprint|[candidates] - let result = `C1|${iceUfrag}|${icePwd}|${compactFingerprint}`; - - // Add candidates if they exist - if (candidates.length > 0) { - result += `|${candidates.join('/')}`; - } - - return result; -} -function playCompressedSDP(sdp){ - sdp = decompressSDP(sdp) - let msg = {}; - msg.description = {sdp, type:"offer"}; - msg.UUID = session.generateStreamID(15); - session.processDescription2(msg); -} - -function decompressSDP(compressed) { - // Parse compressed string - // Format: C(version)|ufrag|pwd|fingerprint|[candidates] - const parts = compressed.split('|'); - - // Version check - if (parts[0] !== 'C1') { - throw new Error('Unsupported compression version'); - } - - const iceUfrag = parts[1]; - const icePwd = parts[2]; - const fingerprint = compactToHex(parts[3]); - - // Format fingerprint with colons - const formattedFingerprint = fingerprint.match(/.{2}/g).join(':'); - - // Build the base SDP - let sdp = [ - 'v=0', - 'o=- 1 1 IN IP4 127.0.0.1', - 's=-', - 't=0 0', - 'a=group:BUNDLE 0', - 'a=extmap-allow-mixed', - 'a=msid-semantic: WMS', - 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel', - 'c=IN IP4 0.0.0.0', - `a=ice-ufrag:${iceUfrag}`, - `a=ice-pwd:${icePwd}`, - 'a=ice-options:trickle', - `a=fingerprint:sha-256 ${formattedFingerprint}`, - 'a=setup:actpass', - 'a=mid:0', - 'a=sctp-port:5000', - 'a=max-message-size:262144' - ].join('\r\n'); - - // Add candidates if they exist - if (parts.length > 4 && parts[4]) { - const candidates = parts[4].split('/'); - - for (const candidate of candidates) { - const candParts = candidate.split(','); - - // Parse the candidate parts - const foundation = candParts[0]; - const component = candParts[1]; - const protocol = candParts[2] === 'u' ? 'UDP' : 'TCP'; - const priority = compactToPriority(candParts[3]); - const ip = candParts[4]; - const port = candParts[5]; - - // Decode type - let type; - switch(candParts[6]) { - case 'h': type = 'host'; break; - case 's': type = 'srflx'; break; - case 'r': type = 'relay'; break; - default: type = 'unknown'; - } - - // Build the candidate line - sdp += `\r\na=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`; - } - } - - return sdp; -} - -function priorityToCompact(priority) { - // Simplified priority encoding - actual implementation would use a more sophisticated approach - // For typical values, this could be a mapping to shorter codes - return priority.toString(36); -} - -function compactToPriority(compact) { - return parseInt(compact, 36); -} - -function hexToCompact(hex) { - // Convert hex pairs to numbers, then to a more compressed alphabet - // This uses a custom encoding that maps to URL-safe characters - const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"; - let result = ''; - - // Process 3 bytes (6 hex chars) at a time to produce 4 output chars - for (let i = 0; i < hex.length; i += 6) { - const chunk = hex.substr(i, 6); - if (chunk.length < 6) { - // Handle the last partial chunk - const value = parseInt(chunk, 16); - result += chars[value % 64]; - if (chunk.length > 2) { - result += chars[Math.floor(value / 64) % 64]; - } - } else { - // Process full 6-hex-char chunk - const value = parseInt(chunk, 16); - result += chars[value & 0x3F]; - result += chars[(value >> 6) & 0x3F]; - result += chars[(value >> 12) & 0x3F]; - result += chars[(value >> 18) & 0x3F]; - } - } - - return result; -} - -function compactToHex(compact) { - const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"; - let result = ''; - - // Process 4 input chars at a time to produce 6 hex chars - for (let i = 0; i < compact.length; i += 4) { - let value = 0; - - for (let j = 0; j < 4 && i + j < compact.length; j++) { - const char = compact[i + j]; - const charValue = chars.indexOf(char); - if (charValue === -1) { - throw new Error(`Invalid character in compact string: ${char}`); - } - value |= charValue << (j * 6); - } - - // Convert to hex - const hex = value.toString(16).padStart(6, '0'); - result += hex; - } - - // Ensure result is the right length for a SHA-256 hash (64 hex chars) - return result.padEnd(64, '0'); -} - -function compressSDPForQR(sdp) { - // Extract critical values - const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1]; - const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1]; - - // Extract fingerprint (removing colons) - const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/); - const fingerprint = fingerprintMatch[1].replace(/:/g, ''); - - // Extract ICE candidates if they exist - const candidates = []; - const candidateRegex = /a=candidate:([^\r\n]+)/g; - let candidateMatch; - while ((candidateMatch = candidateRegex.exec(sdp)) !== null) { - const parts = candidateMatch[1].split(' '); - - // Skip IPv6 candidates - if (parts[4].includes(':')) continue; - - // Compact candidate representation - const foundation = parts[0]; - const component = parts[1]; - const protocol = parts[2].toLowerCase() === 'udp' ? '1' : '2'; - const priority = parseInt(parts[3]); - const ip = compressIP(parts[4]); - const port = parseInt(parts[5]); - - // Encode type: host=1, srflx=2, relay=3 - let typeCode; - switch(parts[7]) { - case 'host': typeCode = '1'; break; - case 'srflx': typeCode = '2'; break; - case 'relay': typeCode = '3'; break; - default: typeCode = '0'; - } - - // Ultra-compact representation, optimized for QR - candidates.push(`${foundation}${component}${protocol}${compressNumber(priority)}${ip}${port}${typeCode}`); - } - - // Compact encoding of fingerprint, optimized for QR code alphanumeric mode - const qrFingerprint = fingerprintToQR(fingerprint); - - // Use very compact delimiter (single character) - let result = `Q${iceUfrag}~${icePwd}~${qrFingerprint}`; - - // Add candidates with minimal separator - if (candidates.length > 0) { - result += `~${candidates.join(',')}`; - } - - return result; -} - -function decompressSDPFromQR(compressed) { - // Parse compressed string - // Format: Q[iceUfrag]~[icePwd]~[fingerprint]~[candidates] - if (!compressed.startsWith('Q')) { - throw new Error('Unsupported QR compression format'); - } - - const parts = compressed.substring(1).split('~'); - const iceUfrag = parts[0]; - const icePwd = parts[1]; - const fingerprint = qrToFingerprint(parts[2]); - - // Format fingerprint with colons - const formattedFingerprint = fingerprint.match(/.{2}/g).join(':'); - - // Build the base SDP - let sdp = [ - 'v=0', - 'o=- 1 1 IN IP4 127.0.0.1', - 's=-', - 't=0 0', - 'a=group:BUNDLE 0', - 'a=extmap-allow-mixed', - 'a=msid-semantic: WMS', - 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel', - 'c=IN IP4 0.0.0.0', - `a=ice-ufrag:${iceUfrag}`, - `a=ice-pwd:${icePwd}`, - 'a=ice-options:trickle', - `a=fingerprint:sha-256 ${formattedFingerprint}`, - 'a=setup:actpass', - 'a=mid:0', - 'a=sctp-port:5000', - 'a=max-message-size:262144' - ].join('\r\n'); - - // Add candidates if they exist - if (parts.length > 3 && parts[3]) { - const candidates = parts[3].split(','); - - for (const candidate of candidates) { - // Extract the packed candidate information - let i = 0; - - // Extract foundation (variable length, numeric) - let j = i; - while (j < candidate.length && /^\d$/.test(candidate[j])) j++; - const foundation = candidate.substring(i, j); - i = j; - - // Extract component (1 digit) - const component = candidate.substring(i, i + 1); - i += 1; - - // Extract protocol (1 digit code) - const protocolCode = candidate.substring(i, i + 1); - const protocol = protocolCode === '1' ? 'UDP' : 'TCP'; - i += 1; - - // Extract priority (variable length, encoded) - j = i; - while (j < candidate.length && /^[0-9A-Z]$/.test(candidate[j])) j++; - const priorityEncoded = candidate.substring(i, j); - const priority = decompressNumber(priorityEncoded); - i = j; - - // Extract IP (encoded) - j = i; - while (j < candidate.length && !/^\d$/.test(candidate[j])) j++; - const ipEncoded = candidate.substring(i, j); - const ip = decompressIP(ipEncoded); - i = j; - - // Extract port (variable length, numeric) - j = i; - while (j < candidate.length && /^\d$/.test(candidate[j])) j++; - const port = candidate.substring(i, j); - i = j; - - // Extract type (1 digit code) - const typeCode = candidate.substring(i, i + 1); - let type; - switch(typeCode) { - case '1': type = 'host'; break; - case '2': type = 'srflx'; break; - case '3': type = 'relay'; break; - default: type = 'unknown'; - } - - // Build the candidate line - sdp += `\r\na=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`; - } - } - - return sdp; -} - -function compressIP(ip) { - // For common local IPs, use a single character code - if (ip === '127.0.0.1') return 'L'; - if (ip === '0.0.0.0') return 'Z'; - if (ip.startsWith('192.168.')) return 'P' + ip.split('.').slice(2).join(''); - if (ip.startsWith('10.')) return 'T' + ip.split('.').slice(1).join(''); - if (ip.startsWith('172.')) return 'S' + ip.split('.').slice(1).join(''); - - // For other IPs, convert to a base36 representation - const parts = ip.split('.'); - let num = 0; - for (let i = 0; i < 4; i++) { - num = num * 256 + parseInt(parts[i]); - } - return num.toString(36).toUpperCase(); -} - -function decompressIP(compressed) { - if (compressed === 'L') return '127.0.0.1'; - if (compressed === 'Z') return '0.0.0.0'; - if (compressed.startsWith('P')) { - const suffix = compressed.substring(1); - if (suffix.length === 0) return '192.168.0.0'; - if (suffix.length === 1) return `192.168.${suffix}.0`; - return `192.168.${suffix.substring(0, 1)}.${suffix.substring(1)}`; - } - if (compressed.startsWith('T')) { - const suffix = compressed.substring(1); - if (suffix.length === 0) return '10.0.0.0'; - if (suffix.length === 1) return `10.${suffix}.0.0`; - if (suffix.length === 2) return `10.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`; - return `10.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`; - } - if (compressed.startsWith('S')) { - const suffix = compressed.substring(1); - if (suffix.length === 0) return '172.0.0.0'; - if (suffix.length === 1) return `172.${suffix}.0.0`; - if (suffix.length === 2) return `172.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`; - return `172.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`; - } - - // Default case: convert from base36 - const num = parseInt(compressed, 36); - return [ - (num >> 24) & 0xFF, - (num >> 16) & 0xFF, - (num >> 8) & 0xFF, - num & 0xFF - ].join('.'); -} - -function compressNumber(num) { - // Convert to base36 for compactness, prefer uppercase for QR - return num.toString(36).toUpperCase(); -} - -function decompressNumber(compressed) { - return parseInt(compressed, 36); -} - -function fingerprintToQR(hex) { - // Convert 64-char hex to a more compact representation - // Using base36 for better QR code efficiency (alphanumeric mode) - let result = ''; - - // Process 4 hex chars (16 bits) at a time to produce 3 base36 chars - for (let i = 0; i < hex.length; i += 4) { - const chunk = hex.substr(i, 4); - if (chunk.length < 4) { - // Handle the last partial chunk - const value = parseInt(chunk, 16); - result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3); - } else { - // Process full chunk - const value = parseInt(chunk, 16); - result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3); - } - } - - return result; -} - -function qrToFingerprint(qrFp) { - let result = ''; - - // Process 3 base36 chars at a time to produce 4 hex chars - for (let i = 0; i < qrFp.length; i += 3) { - const chunk = qrFp.substr(i, 3); - const value = parseInt(chunk, 36); - result += value.toString(16).padStart(4, '0').substring(0, 4); - } - - // Ensure result has correct length for SHA-256 (64 hex chars) - return result.padEnd(64, '0'); -} - -function sdpToQRCode(sdp, element) { - const compressed = compressSDPForQR(sdp); - - // Use the provided qrcodejs library - new QRCode(element, { - text: compressed, - width: 128, - height: 128, - colorDark: "#000000", - colorLight: "#ffffff", - correctLevel: QRCode.CorrectLevel.M // Use medium error correction - }); - - return compressed; // Return compressed string for reference -} - -function qrToSDP(qrData) { - return decompressSDPFromQR(qrData); -} - */ -/// - -function configureWhipOutSDP(description) { - // THIS IS FOR WHIP-OUTPUT; it has - - var configs = {}; - - if (SafariVersion && SafariVersion <= 13 && (iOS || iPad)) { - return description; // skip. Not going to try to tinker with older iOS SDPs - } else if (session.stereo == 3 || session.stereo == 5 || session.stereo == 6 || session.stereo == 1) { - // stereo out - configs = { stereo: 1 }; - log("stereo enabled"); - } else if (iOS || iPad) { - // iOS doesn't have multichannel, so why even bother - configs = {}; - } else if (session.stereo == 4) { - configs = { stereo: 2 }; - log("stereo enabled"); - } else { - configs = { stereo: 0 }; - } - - if (session.whipOutAudioCodec === "pcm") { - if (session.audioInputChannels && session.audioInputChannels == 1) { - description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, false); // mono - } else if (session.stereo) { - description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, true); // mono - } else { - description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, false); - } - } else { - if (session.whipOutAudioCodec) { - description.sdp = CodecsHandler.preferAudioCodec(description.sdp, session.whipOutAudioCodec, session.predAudio, session.pfecAudio); // "red" codec - } - - if (session.noFEC !== null) { - configs.useinbandfec = session.noFEC ? 0 : 1; - } - if (session.maxptime !== false) { - configs.maxptime = session.maxptime; - } - if (session.minptime !== false) { - configs.minptime = session.minptime; - } - if (session.ptime !== false) { - configs.ptime = session.ptime; - } - if (session.dtx !== false) { - configs.dtx = session.dtx; // "usedtx", if no loud audio, stops sending audio for 400ms. default. - } - - if (session.whipOutAudioBitrate) { - configs.maxaveragebitrate = session.whipOutAudioBitrate * 1024; - configs.cbr = session.cbr; - } - - if (Object.keys(configs).length) { - log("Processing sdp of type: " + description.type + " ..."); - log(configs); - description.sdp = CodecsHandler.setOpusAttributes(description.sdp, configs, true); - } - } - - if (iOS || iPad) { - // solves issues with iOS rotation not being correct - if (session.removeOrientationFlag && description.sdp.includes("a=extmap:3 urn:3gpp:video-orientation\r\n")) { - description.sdp = description.sdp.replace("a=extmap:3 urn:3gpp:video-orientation\r\n", ""); - } - } - - if (session.screenShareState && typeof session.whipOutScreenShareCodec === "object") { - session.whipOutScreenShareCodec.reverse().forEach(codec => { - description.sdp = CodecsHandler.preferCodec(description.sdp, codec, session.videoErrorCorrection); - - if (session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) { - description.sdp = CodecsHandler.setVideoBitrates( - description.sdp, - { - min: parseInt((session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) / 10) || 1, - max: session.whipOutScreenShareBitrate || session.whipOutVideoBitrate || 1 - }, - codec - ); - } - }); - } else if (session.screenShareState && session.whipOutScreenShareCodec) { - description.sdp = CodecsHandler.preferCodec(description.sdp, session.whipOutScreenShareCodec, session.videoErrorCorrection); - - if (session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) { - description.sdp = CodecsHandler.setVideoBitrates( - description.sdp, - { - min: parseInt((session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) / 10) || 1, - max: session.whipOutScreenShareBitrate || session.whipOutVideoBitrate || 1 - }, - session.whipOutScreenShareCodec - ); - } - } else if (typeof session.whipOutCodec === "object") { - session.whipOutCodec.reverse().forEach(codec => { - description.sdp = CodecsHandler.preferCodec(description.sdp, codec, session.videoErrorCorrection); - - if (session.whipOutVideoBitrate) { - description.sdp = CodecsHandler.setVideoBitrates( - description.sdp, - { - min: parseInt(session.whipOutVideoBitrate / 10) || 1, - max: session.whipOutVideoBitrate || 1 - }, - codec - ); - } - }); - } else if (session.whipOutCodec) { - description.sdp = CodecsHandler.preferCodec(description.sdp, session.whipOutCodec, session.videoErrorCorrection); - if (session.whipOutVideoBitrate) { - description.sdp = CodecsHandler.setVideoBitrates( - description.sdp, - { - min: parseInt(session.whipOutVideoBitrate / 10) || 1, - max: session.whipOutVideoBitrate || 1 - }, - session.whipOutCodec - ); - } - } else { - log("preferring h264 in sdp"); - description.sdp = CodecsHandler.preferCodec(description.sdp, "h264", session.videoErrorCorrection); // h264 default. openh264? well, this was breaking with Pion, so, meh. whatever h264 - if (session.whipOutVideoBitrate) { - description.sdp = CodecsHandler.setVideoBitrates( - description.sdp, - { - min: parseInt(session.whipOutVideoBitrate / 10) || 1, - max: session.whipOutVideoBitrate || 1 - }, - "h264" - ); - } - } - - var bitrate = 2500; - if (session.whipOutVideoBitrate !== false) { - bitrate = session.whipOutVideoBitrate; - } - if (session.screenShareState && session.whipOutScreenShareBitrate !== false) { - bitrate = session.whipOutScreenShareBitrate; - } - - session.whipOut.savedBitrate = bitrate; // actual target - session.whipOut.setBitrate = bitrate; // max - - return description; -} -const scalabilityModes = ["L1T1", "L1T2", "L1T3", "L2T1", "L2T2", "L2T3", "L3T1", "L3T2", "L3T3", "L2T1h", "L2T2h", "L2T3h", "S2T1", "S2T2", "S2T3", "S2T1h", "S2T2h", "S2T3h", "S3T1", "S3T2", "S3T3", "S3T1h", "S3T2h", "S3T3h", "L2T2_KEY", "L2T3_KEY", "L3T2_KEY", "L3T3_KEY"]; - -function completeLocationURL(originalURL, locationURL) { - if (!originalURL) { - return locationURL; - } else if (!locationURL) { - return originalURL; - } - const parsedOriginalURL = new URL(originalURL); - - // Check if the location URL is already absolute - if (locationURL.startsWith("http://") || locationURL.startsWith("https://")) { - return locationURL; - } else { - // Check if the original URL's pathname ends with a slash or not - let basePath = parsedOriginalURL.origin + parsedOriginalURL.pathname; - if (!basePath.endsWith("/")) { - // If the pathname does not end with a slash, remove the last segment - basePath = basePath.substring(0, basePath.lastIndexOf("/") + 1); - } - - // Resolve "." and ".." in the relative URL against the base path - const fullPath = new URL(locationURL, basePath).href; - - return fullPath; - } -} - -function getWhipOutCanvasTrack(baseRTC = session.whipOut) { - if (!baseRTC) { - errorlog("Meshcast/WHIP not connected; cant' create canvas for it"); - } - if (!baseRTC.canvas) { - baseRTC.canvas = document.createElement("canvas"); - baseRTC.canvas.width = 320; - baseRTC.canvas.height = 180; - } - if (!baseRTC.ctx) { - baseRTC.ctx = baseRTC.canvas.getContext("2d", { alpha: false }); - baseRTC.ctx.fillStyle = "#000"; - baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height); - } - - if (!baseRTC.canvasStream) { - (function loop() { - baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height); - setTimeout(loop, 250); // drawing at 30fps - })(); - try { - baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height); - baseRTC.canvasStream = baseRTC.canvas.captureStream(4); - - baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height); - } catch (e) { - errorlog("Error creating whip output placeholder track"); - } - } - var tracks = baseRTC.canvasStream.getVideoTracks(); - if (tracks.length) { - return tracks[0] - } - errorlog("Meschast canvas not working"); - return false; -} - -function whipOut() { - log("whipOut"); - if (session.whipPublishPrimary === false) { - log("whipOut skipped: primary WHIP disabled"); - return false; - } - - if (!session.videoElement || !session.videoElement.srcObject) { - log("no videoElement yet created; can't do whip out until then"); - return false; - } - - for (const UUID in session.pcs) { - if (!session.pcs.hasOwnProperty(UUID)) { - continue; - } - if (session.pcs[UUID] && session.pcs[UUID].whipout === true) { - session.pcs[UUID].whipout = null; - } - } - - var candidates = []; - var codec = false; - var keyframe = false; - async function whipConnect() { - try { - if (!session.configuration) { - await chooseBestTURN(); - } - - if (session.encodedInsertableStreams) { - // most servers won't support this - session.configuration.encodedInsertableStreams = true; - } - - if (session.bundlePolicy) { - session.configuration.bundlePolicy = session.bundlePolicy; - } - var config = { ...session.configuration }; - log(config); - - // do anything whip specific here - - session.whipOut = new RTCPeerConnection(config); - session.whipOut.stats = {}; - session.whipOut.maxBandwidth = null; // based on max available bitrate - session.whipOut.scale = false; - session.whipOut.offerToReceiveAudio = false; - session.whipOut.offerToReceiveVideo = false; - session.whipOut.keyframeTimeout = null; - } catch (err) { - errorlog(err); - if (!session.cleanOutput) { - warnUser("An RTC error occured"); - } - } - - try { - /// audio tracks - - var tracks = false; - if (session.videoElement && session.videoElement.srcObject) { - tracks = session.videoElement.srcObject.getAudioTracks(); - } - var streamsource = false; - - if (!tracks || !tracks.length) { - if (!session.audioCtx) { - session.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - } - - // Create an oscillator at an audible frequency - const oscillator = session.audioCtx.createOscillator(); - oscillator.frequency.value = 440; // Standard A note - - // Create a gain to make it nearly silent - const gainNode = session.audioCtx.createGain(); - gainNode.gain.value = 0.00001; - - const destination = session.audioCtx.createMediaStreamDestination(); - - oscillator.connect(gainNode); - gainNode.connect(destination); - - oscillator.start(); - - streamsource = destination.stream; - destination.stream.getAudioTracks().forEach(trk => { - tracks = trk; - - trk.enabled = true; // Force the track to appear active - - const constraints = { // keep it quite and not denoised out back to zero - channelCount: { ideal: 1 }, - autoGainControl: { ideal: false }, - echoCancellation: { ideal: false }, - noiseSuppression: { ideal: false } - }; - - try { - trk.applyConstraints(constraints); - } catch (e) { - console.warn("Could not apply audio constraints", e); - } - }); - } else { - tracks = tracks[0]; - streamsource = session.videoElement.srcObject; - } - - if (session.audioContentHint && tracks.kind === "audio") { - try { - tracks.contentHint = session.audioContentHint; - } catch (e) { - errorlog(e); - } - } - - if (tracks) { - try { - session.whipOut.addTransceiver(tracks, { - streams: [streamsource], - direction: "sendonly" - }); - } catch (e) { - errorlog(e); - session.whipOut.addTrack(tracks); - } - } - } catch (e) { - errorlog(e); - } - - try { - //// video tracks /// - - var tracks = false; - if (session.videoElement && session.videoElement.srcObject) { - tracks = session.videoElement.srcObject.getVideoTracks(); - } - - if (!tracks || !tracks.length) { - var track = getWhipOutCanvasTrack(session.whipOut); - } else { - var track = tracks[0]; - } - - if (track) { // it's actually just "track" now. - if (session.screenShareState && session.screenshareContentHint && track.kind === "video") { - try { - track.contentHint = session.screenshareContentHint; - } catch (e) { - errorlog(e); - } - } else if (session.contentHint && track.kind === "video") { - try { - track.contentHint = session.contentHint; - } catch (e) { - errorlog(e); - } - } - - try { - var transceiverSetup = { - streams: [session.videoElement.srcObject], - direction: "sendonly" - }; - if (session.scalabilityMode) { - // might be a good time to validate the scalabilityMode at this point; check to see if requested codec is available,etc. - try { - transceiverSetup.sendEncodings = [{ scalabilityMode: session.scalabilityMode }]; - } catch (e) { - errorlog("Invalid scalability mode provided"); - } - if (session.whipOutCodec && session.whipOutCodec.length) { - //var svcCodecPref = []; - //for (var i = 0;i= 0) { - session.whipOut.location = this.getResponseHeader("location") || ""; - session.whipOut.location = completeLocationURL(session.whipOutput, session.whipOut.location); - } else if (!session.whipOut.location && session.whipOutput) { - session.whipOut.location = session.whipOutput; - session.whipOut.location = completeLocationURL(session.whipOutput, session.whipOut.location); - } - } catch (e) { - errorlog(e); - } - try { - log(this.getAllResponseHeaders()); + +function getStoredDropboxToken() { + try { + return localStorage.getItem("dropboxAccessToken") || null; + } catch (e) { + return null; + } +} + +function persistDropboxToken(token) { + if (!token) { + return; + } + try { + localStorage.setItem("dropboxAccessToken", token); + } catch (e) { } +} + +function clearDropboxAuthState({ clearToken = false, clearOAuth = false } = {}) { + if (typeof session !== "undefined") { + session.dbx = false; + if (clearToken) { + session.dropboxAccessToken = null; + try { + localStorage.removeItem("dropboxAccessToken"); + } catch (e) { } + } + } + if (clearOAuth) { + if (typeof session !== "undefined") { + session.dropboxAccessToken = null; + } + clearDropboxOAuthTokens(); + clearDropboxAuthSession(); + } + try { + localStorage.removeItem("dropboxSession"); + } catch (e) { } +} + +function dropboxErrorSuggestsReauth(error) { + if (!error) { + return false; + } + var summary = ""; + if (error.error && error.error.error_summary) { + summary = error.error.error_summary; + } else if (error.error_summary) { + summary = error.error_summary; + } + if (summary && (summary.indexOf("expired_access_token") !== -1 || summary.indexOf("invalid_access_token") !== -1)) { + return true; + } + var status = error.status || (error.error && error.error.status) || false; + if (status && parseInt(status) === 401) { + return true; + } + return false; +} + +function ensureDropboxSDKLoaded() { + if (window.Dropbox && window.Dropbox.Dropbox) { + return Promise.resolve(); + } + if (dropboxScriptPromise) { + return dropboxScriptPromise; + } + dropboxScriptPromise = new Promise((resolve, reject) => { + var existing = document.querySelector("script[src='" + DROPBOX_SDK_URL + "']"); + if (existing) { + existing.addEventListener("load", () => resolve(), { once: true }); + existing.addEventListener("error", () => reject(new Error("Failed to load Dropbox SDK")), { once: true }); + } else { + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = DROPBOX_SDK_URL; + script.onload = () => resolve(); + script.onerror = () => reject(new Error("Failed to load Dropbox SDK")); + document.head.appendChild(script); + } + }).catch(error => { + dropboxScriptPromise = null; + throw error; + }); + return dropboxScriptPromise; +} + +async function setupDropbox(accessToken = null, options = {}) { + if (typeof session === "undefined") { + return null; + } + var opts = typeof options === "object" && options !== null ? options : {}; + var interactive = opts.interactive === true; + var forceReauth = opts.forceReauth === true; + var token = null; + var manualToken = null; + if (typeof accessToken === "string" && accessToken.trim().length) { + manualToken = accessToken.trim(); + token = manualToken; + } + if (forceReauth && manualToken) { + forceReauth = false; + } + if (forceReauth) { + clearDropboxOAuthTokens(); + if (typeof session !== "undefined") { + session.dropboxOAuth = null; + } + } + var preferOAuth = !manualToken && (forceReauth || (interactive && Boolean(session.dropboxOAuth || getStoredDropboxOAuthTokens()))); + var oauthTokens = null; + if (!forceReauth) { + oauthTokens = session.dropboxOAuth || getStoredDropboxOAuthTokens(); + } + if (oauthTokens && dropboxTokenExpired(oauthTokens)) { + try { + oauthTokens = await refreshDropboxAccessToken(oauthTokens); + } catch (e) { + errorlog(e); + clearDropboxOAuthTokens(); + oauthTokens = null; + } + } + if (oauthTokens && oauthTokens.accessToken) { + session.dropboxOAuth = oauthTokens; + } + if (!token && oauthTokens && oauthTokens.accessToken) { + token = oauthTokens.accessToken; + } + if (!token) { + var legacySessionToken = session.dropboxAccessToken || null; + if (legacySessionToken && (!preferOAuth || (oauthTokens && oauthTokens.accessToken === legacySessionToken))) { + token = legacySessionToken; + } + } + if (!token && !preferOAuth) { + var paramToken = typeof urlParams !== "undefined" && urlParams.get("dropbox"); + token = paramToken || getStoredDropboxToken(); + } + if (forceReauth) { + token = null; + } + if (!token) { + try { + var oauthResponse = await ensureDropboxOAuthAccessToken({ interactive: interactive || preferOAuth }); + if (oauthResponse && oauthResponse.accessToken) { + token = oauthResponse.accessToken; + } + } catch (e) { + throw e; + } + } + if (!token) { + return null; + } + if (!forceReauth && session.dbx && session.dropboxAccessToken === token) { + return session.dbx; + } + session.dropboxAccessToken = token; + if (manualToken && opts.persist !== false) { + persistDropboxToken(token); + } + if (dropboxInitPromise) { + return dropboxInitPromise; + } + dropboxInitPromise = (async currentToken => { + await ensureDropboxSDKLoaded(); + if (!window.Dropbox || !window.Dropbox.Dropbox) { + throw new Error("Dropbox SDK unavailable"); + } + var client = new Dropbox.Dropbox({ accessToken: currentToken }); + session.dbx = client; + session.dropboxAccessToken = currentToken; + resumeDropbox(); + return client; + })(token) + .catch(error => { + var needsReauth = dropboxErrorSuggestsReauth(error); + clearDropboxAuthState({ clearToken: needsReauth, clearOAuth: needsReauth }); + throw error; + }) + .finally(() => { + dropboxInitPromise = null; + }); + return dropboxInitPromise; +} + +if (typeof window !== "undefined") { + window.setupDropbox = setupDropbox; +} + +function resumeDropbox() { + var sessionData = localStorage.getItem("dropboxSession"); + if (sessionData) { + sessionData = JSON.parse(sessionData); + sessionData.forEach(main => { + session.dbx + .filesUploadSessionFinish({ cursor: { session_id: main.result.session_id, offset: main.vdo.offset }, commit: { path: "/" + main.vdo.filename } }) + .then(function (response) { + console.log(response); + console.log("File uploaded to Dropbox:", response.result.path_display); + DBXqueue = []; + //localStorage.removeItem('dropboxSession'); + }) + .catch(function (error) { + localStorage.removeItem("dropboxSession"); + console.error("Error uploading file:", error); + if (!session.cleanOutput) { + confirmAlt("There was an error finalizing a previous file upload. \n" + (error.error_summary || "") + "\n\nWould you like to keep trying?").then(res => { + if (!res) { + localStorage.removeItem("dropboxSession"); + } + }); + } + }); + }); + } +} + +async function streamVideoToDropbox(filename) { + if (!session.dbx && typeof setupDropbox === "function") { + try { + await setupDropbox(); + } catch (e) { + errorlog(e); + } + } + if (!session.dbx) { + if (session.directorUUID) { + var failMsg = {}; + failMsg.dropbox = -2; + for (var di = 0; di < session.directorList.length; di++) { + failMsg.UUID = session.directorList[di]; + session.sendPeers(failMsg, failMsg.UUID); + } + } + return; + } + + var main; + try { + main = await session.dbx.filesUploadSessionStart({ close: false }); + } catch (e) { + errorlog(e); + if (!session.cleanOutput) { + warnUser("Dropbox failed to initialize.\n\nAre your credentials valid? Tokens may expire after a few hours.", 8000); + } + if (dropboxErrorSuggestsReauth(e)) { + clearDropboxAuthState({ clearToken: true, clearOAuth: true }); + } + return; + } + + var sessionId = main.result.session_id; + var offset = 0; + var chunkCounter = 0; + var DBXqueue = []; + var resolverQueue = []; + var uploadActive = false; + + log(main); + main.vdo = { filename: filename, offset: offset }; + + var persisted = localStorage.getItem("dropboxSession"); + if (persisted) { + try { + persisted = JSON.parse(persisted); + } catch (e) { + errorlog(e); + persisted = []; + } + persisted.push(main); + } else { + persisted = [main]; + } + localStorage.setItem("dropboxSession", JSON.stringify(persisted)); + + if (session.directorUUID) { + var initMsg = {}; + initMsg.dropbox = -1; + for (var ii = 0; ii < session.directorList.length; ii++) { + initMsg.UUID = session.directorList[ii]; + session.sendPeers(initMsg, initMsg.UUID); + } + } + + var totalChunksRecorded = 0; + var totalChunksUploaded = 0; + + function updateTotalChunksRecorded() { + if (!session.cleanOutput) { + getById("progressContainer").classList.remove("hidden"); + } + totalChunksRecorded++; + updateProgressBar(); + } + + function updateTotalChunksUploaded() { + totalChunksUploaded++; + updateProgressBar(); + } + + function finishedChunksUploaded() { + try { + getById("progressBar").style.width = "100%"; + setTimeout(function () { + if (getById("progressBar").style.width == "100%") { + getById("progressContainer").classList.add("hidden"); + } + }, 1000); + } catch (e) { + errorlog(e); + } + } + + function updateProgressBar() { + if (totalChunksRecorded > 0) { + var progressPercentage = (totalChunksUploaded / (totalChunksRecorded || 1)) * 100; + getById("progressBar").style.width = progressPercentage + "%"; + getById("progressBar").innerHTML = "Upload progress to Dropbox: " + progressPercentage.toFixed(2) + "%"; + } + } + + function notifyDropboxQueueSize() { + if (!session.directorUUID) { + return; + } + var msg = {}; + msg.dropbox = DBXqueue.length; + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendPeers(msg, msg.UUID); + } + } + + function resolveNext(value) { + var resolver = resolverQueue.shift(); + if (resolver && resolver.resolve) { + resolver.resolve(value); + } + } + + function rejectNext(error) { + var resolver = resolverQueue.shift(); + if (resolver && resolver.reject) { + resolver.reject(error); + } + } + + function rejectPending(error) { + while (resolverQueue.length) { + var pending = resolverQueue.shift(); + if (pending && pending.reject) { + pending.reject(error); + } + } + } + + function handleDropboxUploadFailure(error) { + errorlog(error); + if (dropboxErrorSuggestsReauth(error)) { + clearDropboxAuthState({ clearToken: true, clearOAuth: true }); + } + if (!session.cleanOutput) { + warnUser("Dropbox upload failed. Please verify your token and try again.", 8000); + } + } + + async function processQueue() { + if (uploadActive) { + return; + } + uploadActive = true; + while (DBXqueue.length) { + var current = DBXqueue[0]; + try { + if (current === false) { + await session.dbx.filesUploadSessionFinish({ cursor: { session_id: sessionId, offset: offset }, commit: { path: "/" + filename } }); + DBXqueue.shift(); + resolveNext(true); + var sessionData = localStorage.getItem("dropboxSession"); + if (sessionData) { + try { + sessionData = JSON.parse(sessionData); + sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename); + } catch (e) { + errorlog(sessionData); + errorlog(e); + sessionData = []; + } + } else { + sessionData = []; + } + if (sessionData.length) { + localStorage.setItem("dropboxSession", JSON.stringify(sessionData)); + } else { + localStorage.removeItem("dropboxSession"); + } + sessionId = false; + notifyDropboxQueueSize(); + finishedChunksUploaded(); + } else { + await session.dbx.filesUploadSessionAppendV2({ cursor: { session_id: sessionId, offset: offset }, close: false, contents: current }); + offset += current.size; + main.vdo.offset = offset; + var sessionData = localStorage.getItem("dropboxSession"); + if (sessionData) { + try { + sessionData = JSON.parse(sessionData); + sessionData = sessionData.filter(entry => entry.vdo.filename !== filename && entry.vdo && entry.vdo.filename); + sessionData.push(main); + } catch (e) { + errorlog(sessionData); + errorlog(e); + sessionData = [main]; + } + } else { + sessionData = [main]; + } + localStorage.setItem("dropboxSession", JSON.stringify(sessionData)); + DBXqueue.shift(); + resolveNext(true); + updateTotalChunksUploaded(); + chunkCounter += 1; + notifyDropboxQueueSize(); + } + } catch (error) { + rejectNext(error); + rejectPending(error); + DBXqueue = []; + uploadActive = false; + handleDropboxUploadFailure(error); + return; + } + } + uploadActive = false; + } + + function enqueueChunk(chunk) { + return new Promise((resolve, reject) => { + if (!sessionId) { + reject(new Error("Dropbox session inactive")); + return; + } + if (DBXqueue.length && DBXqueue[DBXqueue.length - 1] === false) { + resolve(); + return; + } + DBXqueue.push(chunk); + resolverQueue.push({ resolve: resolve, reject: reject }); + updateTotalChunksRecorded(); + notifyDropboxQueueSize(); + processQueue(); + }); + } + + function uploadChunk(chunk) { + var promise = enqueueChunk(chunk); + promise.catch(() => { }); + return promise; + } + + return uploadChunk; +} + +var recordingBitratePromise = false; +var defaultRecordingBitrate = false; +var lastConfiguredRecordingSetup = false; +async function recordVideo(target, event = null, videoKbps = false) { + // event.currentTarget,this.parentNode.parentNode.dataset.UUID + + if (session.record === false) { + warnlog("recordings are disabled by decree of thy host magistrate"); + } + + if (!target) { return; } + + var UUID = target.dataset.UUID; + + if (!UUID) { + return; + } + + var video = session.rpcs[UUID].videoElement; + + if (!video) { + return; + } + + if (video.stopWriter) { + video.stopWriter(); + updateLocalRecordButton(UUID, -1); + return; + } else if (video.startWriter) { + await video.startWriter(); + updateLocalRecordButton(UUID, 0); + return; + } + + if (event === null) { + if (defaultRecordingBitrate === null) { + updateLocalRecordButton(UUID, -1); + return; + } + } else if (event.ctrlKey || event.metaKey) { + updateLocalRecordButton(UUID, -3); + Callbacks.push([recordVideo, target, null, false]); + log("Record Video queued"); + defaultRecordingBitrate = false; + recordingBitratePromise = false; + return; + } else { + defaultRecordingBitrate = false; + recordingBitratePromise = false; + } + + log("Record Video Clicked"); + if ("recording" in video) { + log("ALREADY RECORDING!"); + updateLocalRecordButton(UUID, -2); + video.recorder.stop(); + session.requestRateLimit(35, UUID); // 100kbps + if (session.audiobitrate === false) { + session.requestAudioRateLimit(-1, UUID); + } + + var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + } + var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + } + var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + } + return; + } else { + updateLocalRecordButton(UUID, 0); + //target.style.backgroundColor = "#FCC"; + //target.innerHTML = " Download"; + video.recording = true; + } + + video.recorder = {}; + + var configureRecording = { + bitrate: videoKbps, + usePCM: (videoKbps === 0 || session.pcm) ? true : false, + audioOnly: (videoKbps !== false && videoKbps <= 0) ? true : false + }; + + if (configureRecording.bitrate === false) { + if (defaultRecordingBitrate == false) { + configureRecording.bitrate = session.recordDefault; + + if (session.recordLocal !== false) { + configureRecording.bitrate = session.recordLocal; + } else if (lastConfiguredRecordingSetup !== false) { + configureRecording = lastConfiguredRecordingSetup; + } + + if (session.pcm) { + configureRecording.usePCM = true; + } + + if (!recordingBitratePromise) { + window.focus(); + recordingBitratePromise = promptRecordingOptions(getTranslation("press-ok-to-record"), false, configureRecording); + } + configureRecording = await recordingBitratePromise; + if (configureRecording === null) { + //target.style.backgroundColor = null; + //target.innerHTML = ' record local'; + updateLocalRecordButton(UUID, -1); + target.style.backgroundColor = ""; + + try { + clearInterval(video.recorder.writer.interval); + } catch (e) { } + + delete video.recorder; + delete video.recording; + defaultRecordingBitrate = null; + return; + } + + defaultRecordingBitrate = configureRecording; + lastConfiguredRecordingSetup = configureRecording; + } else { + configureRecording = defaultRecordingBitrate; + } + } + + if (configureRecording.audioOnly) { + if (session.audiobitrate === false) { + if (configureRecording.usePCM) { + session.requestAudioRateLimit(session.audiobitratePRO || 128, UUID); // PCM + } else { + session.requestAudioRateLimit(configureRecording.bitrate || 32, UUID); // exact? sure. why not. + } + } + } else { + if (configureRecording.bitrate < 50) { + configureRecording.bitrate = 50; + } + session.requestRateLimit(configureRecording.bitrate, UUID); // 3200kbps transfer bitrate. Less than the recording bitrate, to avoid waste. + + if (configureRecording.bitrate > 4000) { + if (session.audiobitrate === false) { + if (session.pcm) { + session.requestAudioRateLimit(session.audiobitratePRO, UUID); + } else { + session.requestAudioRateLimit(128, UUID); + } + } + } else if (configureRecording.bitrate > 2500) { + if (session.audiobitrate === false) { + if (session.pcm) { + session.requestAudioRateLimit(session.audiobitratePRO, UUID); + } else { + session.requestAudioRateLimit(80, UUID); + } + } + } + } + + // + + var cancell = false; + if (typeof video.srcObject === "undefined" || !video.srcObject) { + return; + } + + video.recorder.stop = function (restart = false, notify = false) { + if (session.dbx && video.dropbox && video.dropbox[filename]) { + video.dropbox[filename](false); + } + if (video.gdrive && video.gdrive[filename]) { + video.gdrive[filename].addChunk(false); + } + + if (!video.recording) { + errorlog("ALREADY STOPPED"); + updateLocalRecordButton(UUID, -1); + return; + } + + if (notify) { + if (!session.cleanOutput) { + warnUser("A local recording has stopped unexpectedly."); + } + if (session.beepToNotify) { + playtone(); + } + target.classList.remove("shake"); + setTimeout( + function (target) { + target.classList.add("shake"); + }, + 10, + target + ); + } + + video.recording = false; + updateLocalRecordButton(UUID, -2); + try { + if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) { + if (video.recorder.mediaRecorder.state !== "inactive" || video.recorder.mediaRecorder.state === "recording") { + video.recorder.mediaRecorder.stop(); + } + } + } catch (e) { + errorlog(e); + try { + video.recorder.mediaRecorder.stop(); + } catch (e1) { + errorlog(e1); + } + } + + session.requestRateLimit(35, UUID); // 100kbps + if (session.audiobitrate === false) { + session.requestAudioRateLimit(-1, UUID); + } + var elements = document.querySelectorAll('[data-action-type="change-quality2"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + } + var elements = document.querySelectorAll('[data-action-type="change-quality1"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + } + var elements = document.querySelectorAll('[data-action-type="change-quality3"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + } + + cancell = true; + // log('Recorded Blobs: ', recordedBlobs); + // download(); + setTimeout( + (writer1, UUID1, video1) => { + try { + writer1.close(); + } catch (e) { } + updateLocalRecordButton(UUID1, -1); + delete video1.recorder; + delete video1.recording; + }, + 1200, + video.recorder.writer, + UUID, + video + ); + }; + + const { readable, writable } = new TransformStream({ + transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b))) + }); + + var filext = ".webm"; + + let options = {}; + + if (!configureRecording.audioOnly) { + var tryCodec = session.recordingVideoCodec || ""; // Simplified condition to assign tryCodec + + if (tryCodec && MediaRecorder.isTypeSupported("video/webm;codecs=" + tryCodec)) { + if (!session.cleanOutput) { + console.log("👍 The browser 'says' it supports " + tryCodec); + } + options.mimeType = "video/webm;codecs=" + tryCodec; + + if (session.pcm) { + // Fixed the format of the MIME type string + var mimeTypeWithPCM = "video/webm;codecs=" + tryCodec + ",pcm"; + if (MediaRecorder.isTypeSupported(mimeTypeWithPCM)) { + options.mimeType = mimeTypeWithPCM; + } else { + options.mimeType = "video/webm;codecs=pcm"; + } + } + } else { + // Simplified conditions for PCM support + if (tryCodec) { + warnlog("video/webm;codecs=" + tryCodec + " - is not supported"); + } + options.mimeType = session.pcm && MediaRecorder.isTypeSupported("video/webm;codecs=pcm") ? "video/webm;codecs=pcm" : "video/webm"; + } + + // Simplified bitrate settings + options.videoBitsPerSecond = parseInt(configureRecording.bitrate * 1024); + if (configureRecording.bitrate < 1000) { + options.audioBitsPerSecond = parseInt(100 * 1024); + } else if (configureRecording.bitrate < 6000) { + options.audioBitsPerSecond = parseInt(130 * 1024); + } else if (configureRecording.bitrate < 20000) { + options.audioBitsPerSecond = parseInt(256 * 1024); + } else { + // If configureRecording.bitrate is >= 20000, use bitsPerSecond for total bitrate + options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024); + } + + if (iOS && options.mimeType) { + if (!MediaRecorder.isTypeSupported(options.mimeType)) { + options.mimeType = "video/mp4"; + filext = ".mp4"; + } + } + + video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); + + //if (session.dbx){ + // video.recorder.dropbox = await streamVideoToDropbox(); // i don't want to upload to dropbox remote streams; just local + //} + } else { + options.mimeType = "audio/webm"; + if (configureRecording.usePCM) { + if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) { + options.mimeType = "audio/webm;codecs=pcm"; + } + } else { + options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024); + } + var stream = createMediaStream(); + video.srcObject.getAudioTracks().forEach(track => { + stream.addTrack(track, video.srcObject); + }); + + if (iOS && options.mimeType) { + if (!MediaRecorder.isTypeSupported(options.mimeType)) { + options.mimeType = "video/mp4"; + filext = ".mp4"; + } + } + video.recorder.mediaRecorder = new MediaRecorder(stream, options); + + //if (session.dbx){ + // video.recorder.dropbox = await streamVideoToDropbox(); + //} + } + + var timestamp = Date.now(); + var filename = ""; + if (session.rpcs[UUID].label && session.rpcs[UUID].streamID) { + filename = session.rpcs[UUID].label || session.rpcs[UUID].streamID; + } else { + filename = session.rpcs[UUID].label + "_" + session.rpcs[UUID].streamID; + } + + filename = filename.replace(/[\W]+/g, "_"); + filename = filename.substring(0, 200); + filename += "_" + timestamp.toString(); + + var writer = writable.getWriter(); + video.recorder.writer = writer; + readable.pipeTo(streamSaver.createWriteStream(filename.toString() + filext, video.recorder.stop)); + pokeIframeAPI("recording-started"); + + log(options); + + function download() { + const blob = new Blob(recordedBlobs, { + type: "video/webm" + }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = filename + filext; + document.body.appendChild(a); + a.click(); + setTimeout( + function (uu, aa) { + document.body.removeChild(aa); + window.URL.revokeObjectURL(uu); + }, + 100, + url, + a + ); + } + + function handleDataAvailable(event) { + if (event.data && event.data.size > 0) { + //recordedBlobs.push(event.data); + try { + writer.write(event.data); //////////// + if (video.recording) { + updateLocalRecordButton(UUID, parseInt((Date.now() - timestamp) / 1000) || 0); + } + } catch (e) { + warnlog("Stream recording error or ended"); + } + + try { + if (session.dbx && video.dropbox && video.dropbox[filename]) { + video.dropbox[filename](event.data); + } + if (video.gdrive && video.gdrive[filename]) { + video.gdrive[filename].addChunk(event.data); + } + } catch (e) { + errorlog(e); + } + } + } + + video.recorder.mediaRecorder.ondataavailable = handleDataAvailable; + + video.recorder.mediaRecorder.onerror = function (event) { + errorlog(event); + console.log("It's possible using &recordcodec=vp8 might resolve recording errors if caused by an incompatible hardware encoder or codec"); + video.recorder.stop(); + session.requestRateLimit(35, UUID); + if (!session.cleanOutput) { + setTimeout(function () { + warnUser("an error occured with the media recorder; stopping recording"); + }, 1); + } + }; + + video.recorder.mediaRecorder.onstop = function (event) { + log("mediaRecorder stopped"); + }; + + video.srcObject.onended = function (event) { + video.recorder.stop(); + //session.requestRateLimit(35, UUID); // changed DEc 05 2023 - not sure this makes sense. + if (!session.cleanOutput) { + setTimeout(function () { + warnUser("stream ended! stopping recording"); + }, 1); + } + }; + + setTimeout( + function (v) { + if (v && v.recorder) { + v.recorder.mediaRecorder.start(1000); + } + }, + 500, + video + ); // 100ms chunks + + return; +} +function updateGdriveButton(UUID, gdrive, screen = false) { + if (screen) { + var elements = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + UUID + '_screen"]'); + } else { + var elements = document.querySelectorAll('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + UUID + '"]'); + } + if (elements[0]) { + var progressPercentage = parseInt((1000 * gdrive.up) / gdrive.rec) / 10; + elements[0].innerText = progressPercentage + "%"; + + if (gdrive.state && gdrive.state == 2) { + elements[0].innerText = "GDrive " + elements[0].innerText + " (done)"; + } else { + elements[0].innerText += ", " + convertKilobytes(gdrive.rec) + " left"; + } + } + + if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") { + try { + window.dispatchEvent( + new CustomEvent("vdoninja:gdrive-progress", { + detail: { + UUID: UUID, + gdrive: gdrive, + screen: screen + } + }) + ); + } catch (e) { } + } +} + +function updateRemoteRecordButton(UUID, recorder, screen = false) { + if (screen) { + var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '_screen"]'); + } else { + var elements = document.querySelectorAll('[data-action-type="recorder-remote"][data--u-u-i-d="' + UUID + '"]'); + } + if (elements[0]) { + var time = parseInt(recorder) || 0; + if (time == -4) { + if (!session.cleanOutput) { + warnUser("A remote recording has stopped unexpectedly.\n\nDid a user cancel the file downlaod?"); + } + if (session.beepToNotify) { + playtone(); + } + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + elements[0].classList.remove("shake"); + elements[0].innerHTML = ' stopping...'; + setTimeout( + function (ele) { + ele.classList.add("shake"); + }, + 10, + elements[0] + ); + } else if (time == -3) { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + elements[0].disabled = true; + elements[0].innerHTML = ' Not Supported'; + if (!session.cleanOutput) { + setTimeout(function () { + warnUser("The remote browser does not support recording.\n\nPerhaps try local recording instead."); + }, 0); + } + } else if (time == -5) { + if (!session.cleanOutput) { + setTimeout(function () { + warnUser("The remote browser has only experimental support for media recording.\n\nAlso, when this download stops, the remote user may be asked to download the file for it to save."); + }, 0); + } + } else if (time == -2) { + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + elements[0].innerHTML = ' stopping...'; + } else if (time == -1) { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + elements[0].innerHTML = ' Record Remote'; + } else { + var minutes = Math.floor(time / 60); + var seconds = time - minutes * 60; + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + elements[0].innerHTML = ' ' + minutes + "m : " + zpadTime(seconds) + "s"; + } + } +} + +function updateLocalRecordButton(UUID, recorder) { + var elements = document.querySelectorAll('[data-action-type="recorder-local"][data--u-u-i-d="' + UUID + '"]'); + if (elements[0]) { + var time = parseInt(recorder) || 0; + + //target.innerHTML = ' ARMED'; + // + if (time == -3) { + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + elements[0].innerHTML = ' ARMED'; + elements[0].style.backgroundColor = "#BF3F3F"; + } else if (time == -2) { + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + elements[0].innerHTML = ' stopping...'; + elements[0].style.backgroundColor = ""; + } else if (time == -1) { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + elements[0].innerHTML = ' Record Local'; + elements[0].style.backgroundColor = ""; + } else { + var minutes = Math.floor(time / 60); + var seconds = time - minutes * 60; + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + elements[0].innerHTML = ' ' + minutes + "m : " + zpadTime(seconds) + "s"; + elements[0].style.backgroundColor = ""; + } + } +} + +async function recordLocalVideoToggle(startonly = false) { + if (!session.videoElement) { + return; + } + log("recordLocalVideoToggle()"); + + var ele = getById("recordLocalbutton"); + if (ele.dataset.state == "0") { + if (session.videoElement.recorder && session.videoElement.recorder.closing) { + warnlog("already closing"); + getById("recordLocalbutton").classList.remove("shake"); + getById("recordLocalbutton").classList.add("shake"); + setTimeout(function () { + getById("recordLocalbutton").classList.remove("shake"); + }, 1000); + + return false; + } + + ele.dataset.state = "1"; + ele.style.backgroundColor = "red"; + ele.innerHTML = ''; + if ("recording" in session.videoElement) { + errorlog("its already recording ??"); + } else { + var res = await recordLocalVideo("start"); + log(res); + } + + if (session.director) { + var elements = document.querySelectorAll('[data-action-type="recorder-local"][data-sid="' + session.streamID + '"]'); + if (elements[0]) { + elements[0].classList.add("pressed"); + elements[0].ariaPressed = "true"; + elements[0].innerHTML = ' Record'; + } + } + return true; + } else if (!startonly) { + if ("recording" in session.videoElement) { + var res = await recordLocalVideo("stop"); + log(res); + } + ele.dataset.state = "0"; + ele.style.backgroundColor = ""; + ele.innerHTML = ''; + + if (session.director) { + var elements = document.querySelectorAll('[data-action-type="recorder-local"][data-sid="' + session.streamID + '"]'); + if (elements[0]) { + elements[0].classList.remove("pressed"); + elements[0].ariaPressed = "false"; + elements[0].innerHTML = ' Record'; + } + } + return false; + } +} + +function cleanupSensorData(data) { + for (const key in data) { + let nonNullFound = false; + for (const subKey in data[key]) { + if (data[key][subKey] === null) { + delete data[key][subKey]; + } else if (subKey !== "t") { + nonNullFound = true; + } + } + if (!nonNullFound) { + delete data[key]; + } + } +} + +function setupSensorData(pollrate = 30) { + session.sensors = {}; + session.sensors.data = {}; + // session.sensorDataFilter = ["pos","lin","ori","mag","gyro","acc"]; + + const startSensor = (SensorType, sensorKey) => { + if (window[SensorType] && session.sensorDataFilter.includes(sensorKey)) { + try { + session.sensors.data[sensorKey] = {}; + let sensor = new window[SensorType]({ frequency: pollrate }); + sensor.addEventListener("reading", () => { + try { + session.sensors.data[sensorKey].x = sensor.x !== null ? parseFloat(sensor.x.toFixed(5)) : null; + session.sensors.data[sensorKey].y = sensor.y !== null ? parseFloat(sensor.y.toFixed(5)) : null; + session.sensors.data[sensorKey].z = sensor.z !== null ? parseFloat(sensor.z.toFixed(5)) : null; + } catch (e) { } + try { + session.sensors.data[sensorKey].t = parseInt(Math.round(sensor.timeStamp || 0)) || Date.now(); + } catch (e) { + errorlog(e); + } + }); + sensor.start(); + session.sensors[sensorKey] = sensor; + } catch (e) { + errorlog(e); + } + } + }; + + startSensor("Accelerometer", "acc"); + startSensor("Gyroscope", "gyro"); + startSensor("Magnetometer", "mag"); + startSensor("LinearAccelerationSensor", "lin"); + + if (session.sensorDataFilter.includes("ori")) { + try { + window.addEventListener("deviceorientation", e => { + if (e.alpha || e.beta || e.gamma || e.absolute) { + session.sensors.data.ori = { + a: e.alpha !== null ? parseFloat(e.alpha.toFixed(5)) : null, + b: e.beta !== null ? parseFloat(e.beta.toFixed(5)) : null, + g: e.gamma !== null ? parseFloat(e.gamma.toFixed(5)) : null, + d: e.absolute || null, + t: parseInt(Math.round(e.timeStamp || 0)) || Date.now() + }; + } + }); + } catch (e) { + errorlog("Device Orientation Error:", e); + } + } + + let isFirstUpdate = true; + if (navigator.geolocation && session.sensorDataFilter.includes("pos")) { + try { + navigator.geolocation.watchPosition( + pos => { + session.sensors.data.pos = { + speed: pos.coords.speed !== null ? parseFloat(pos.coords.speed.toFixed(3)) : null, + alt: pos.coords.altitude !== null ? parseFloat(pos.coords.altitude.toFixed(3)) : null, + acc: pos.coords.accuracy !== null ? parseFloat(pos.coords.accuracy.toFixed(3)) : null, + lat: pos.coords.latitude !== null ? parseFloat(pos.coords.latitude.toFixed(3)) : null, + lon: pos.coords.longitude !== null ? parseFloat(pos.coords.longitude.toFixed(3)) : null, + t: parseInt(Math.round(pos.timeStamp || 0)) || Date.now() + }; + + if (isFirstUpdate && (pos.coords.latitude || pos.coords.longitude)) { + isFirstUpdate = false; + console.log(session.sensors.data); + warnUser("🌎🌍🌏 Geo-location sharing is enabled.\n\nIf being tracked is unwanted, please disable the 'Location' permissions in your browser's site settings.", 10000); + } + }, + error => { + errorlog("Geolocation Error:", error); + if (error.code === error.PERMISSION_DENIED) { + warnUser("Geolocation permission was denied."); + } + }, + { + enableHighAccuracy: true, + timeout: 5000, + maximumAge: 0 + } + ); + } catch (e) { + errorlog("Device Orientation Error:", e); + } + } + + setInterval(function () { + if (session.sensors && session.sensors.data) { + cleanupSensorData(session.sensors.data); + session.sendMessage({ sensors: session.sensors.data }); + } + }, parseInt(1000 / pollrate)); +} + +function setupExternalSensorBridge() { + if (session.externalSensorBridgeAttached) { + return; + } + + session.externalSensorBridgeAttached = true; + log("External sensor bridge attached"); + + window.addEventListener("message", event => { + const payload = event.data; + if (!payload || !payload.sensors) { + return; + } + + if (session.externalSensorOrigin && event.origin && event.origin !== "null" && event.origin !== session.externalSensorOrigin) { + log("Sensor message rejected due to origin mismatch: " + event.origin); + return; + } + + session.sensors = session.sensors || {}; + session.sensors.data = session.sensors.data || {}; + + try { + Object.assign(session.sensors.data, payload.sensors); + } catch (e) {} + + try { + session.sendMessage({ sensors: payload.sensors }); + } catch (e) { + errorlog(e); + } + }); +} + +//// PCM 16 SAVING LOGIC +function PCM16(stream) { + if (!stream || !stream.getAudioTracks().length) { + errorlog("no audio track found"); + return null; + } + + var PCM = stream.getAudioTracks()[0].getSettings(); + + function audioBufferToWav(buffer, options = {}) { + const numChannels = buffer.numberOfChannels; + const sampleRate = buffer.sampleRate; + const format = options.float32 ? 3 : 1; + const bitDepth = format === 3 ? 32 : 16; + + let samples; + if (numChannels === 2) { + samples = interleave(buffer.getChannelData(0), buffer.getChannelData(1)); + } else { + samples = buffer.getChannelData(0); + } + + return encodeWAV(samples, format, sampleRate, numChannels, bitDepth); + } + + function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) { + const bytesPerSample = bitDepth / 8; + const blockAlign = numChannels * bytesPerSample; + const bufferLength = 44 + samples.length * bytesPerSample; + const buffer = new ArrayBuffer(bufferLength); + const dataView = new DataView(buffer); + + // referenced from: https://github.com/steveseguin/audiobuffer-to-wav (by Jam3 - MIT lic) + writeString(dataView, 0, "RIFF"); + dataView.setUint32(4, 36 + samples.length * bytesPerSample, true); + writeString(dataView, 8, "WAVE"); + writeString(dataView, 12, "fmt "); + dataView.setUint32(16, 16, true); + dataView.setUint16(20, format, true); + dataView.setUint16(22, numChannels, true); + dataView.setUint32(24, sampleRate, true); + dataView.setUint32(28, sampleRate * blockAlign, true); + dataView.setUint16(32, blockAlign, true); + dataView.setUint16(34, bitDepth, true); + writeString(dataView, 36, "data"); + dataView.setUint32(40, samples.length * bytesPerSample, true); + + if (format === 1) { + floatTo16BitPCM(dataView, 44, samples); + } else { + writeFloat32(dataView, 44, samples); + } + + return buffer; + } + function interleave(inputL, inputR) { + const length = inputL.length + inputR.length; + const result = new Float32Array(length); + + for (let index = 0, inputIndex = 0; index < length; index += 2, inputIndex++) { + result[index] = inputL[inputIndex]; + result[index + 1] = inputR[inputIndex]; + } + + return result; + } + function floatTo16BitPCM(output, offset, input) { + for (let i = 0; i < input.length; i++, offset += 2) { + const s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); + } + } + function writeFloat32(output, offset, input) { + for (let i = 0; i < input.length; i++, offset += 4) { + output.setFloat32(offset, input[i], true); + } + } + function writeString(dataView, offset, string) { + for (let i = 0; i < string.length; i++) { + dataView.setUint8(offset + i, string.charCodeAt(i)); + } + } + // end reference + + PCM.audioContext = new AudioContext({ sampleRate: PCM.sampleRate }); + PCM.source = PCM.audioContext.createMediaStreamSource(stream); + PCM.numberOfChannels = PCM.source.channelCount; + PCM.scriptNode = PCM.audioContext.createScriptProcessor(4096, PCM.numberOfChannels, PCM.numberOfChannels); // buffer size, input channels, output channels + + PCM.recording = false; + PCM.audioData = []; + for (let i = 0; i < PCM.numberOfChannels; i++) { + PCM.audioData.push([]); + } + PCM.scriptNode.onaudioprocess = audioProcessingEvent => { + if (!PCM.recording) return; + for (let channel = 0; channel < PCM.numberOfChannels; channel++) { + const inputData = audioProcessingEvent.inputBuffer.getChannelData(channel); + PCM.audioData[channel].push(new Float32Array(inputData)); + } + }; + PCM.source.connect(PCM.scriptNode); + PCM.scriptNode.connect(PCM.audioContext.destination); + + PCM.startRecording = function () { + PCM.audioData = []; + for (let i = 0; i < PCM.numberOfChannels; i++) { + PCM.audioData.push([]); + } + PCM.recording = true; + }; + PCM.stopRecording = function (filename = "filename") { + PCM.recording = false; + + const bufferLength = PCM.audioData[0].length * 4096; + const audioBuffer = PCM.audioContext.createBuffer(PCM.numberOfChannels, bufferLength, PCM.audioContext.sampleRate); + + for (let channel = 0; channel < PCM.numberOfChannels; channel++) { + const channelData = audioBuffer.getChannelData(channel); + PCM.audioData[channel].forEach((chunk, index) => { + channelData.set(chunk, index * 4096); + }); + } + const wavArrayBuffer = audioBufferToWav(audioBuffer); + const blob = new Blob([wavArrayBuffer], { type: "audio/wav" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = filename + ".wav"; + anchor.click(); + URL.revokeObjectURL(url); + }; + + return PCM; +} +//// END OF PCM 16 SAVING CODE + +async function recordLocalVideo(action = null, configureRecording = false, remote = false, altUUID = false) { + // event.currentTarget,this.parentNode.parentNode.dataset.UUID + + if (session.record === false) { + warnlog("recordings are disabled by decree of thy host magistrate"); + } + log("original", configureRecording); + if (typeof configureRecording !== "object") { + let bitrate = configureRecording !== false ? configureRecording : (session.recordLocal !== false ? session.recordLocal : session.recordDefault); + configureRecording = { + bitrate: bitrate, + usePCM: (bitrate === 0 || session.pcm) ? true : false, + audioOnly: (bitrate !== false && bitrate <= 0) ? true : false + }; + } + + if (remote) { + var video = remote; + if (remote.id === "videosource" || remote.id === "screensharesource") { + remote = false; + } + } else if (altUUID) { + var video = session.screenShareElement; + } else { + var video = session.videoElement; + } + + if (!video) { + warnlog("video not found"); + return; + } + + log(video.id); + + if ("recording" in video) { + if (action == "estop") { + video.recorder.eStop(); + warnlog("EMERGENCY Stopping RECORDING!"); + video.recorder.stop(); + return; + } else if (action == "stop") { + log("Stopping RECORDING!"); + video.recorder.stop(); + return; + } else if (action == "start") { + errorlog("ALREADY RECORDING!"); + if (remote) { + getById("recordLocalbutton").dataset.state = "1"; + getById("recordLocalbutton").style.backgroundColor = "red"; + getById("recordLocalbutton").innerHTML = ''; + } + return; + } else { + errorlog("STOPPING RECORDING by default toggle!"); + video.recorder.stop(); + return; + } + return; // this should never happen + } else if (action == "start") { + if (video && video.recorder && video.recorder.closing) { + errorlog("Ingore request. Haven't finished closing the previous recording."); + return; + } + + if (video.srcObject && video.srcObject.getTracks && !video.srcObject.getTracks().length) { + warnlog("No video or audio tracks to record"); + return; + } + + if (!MediaRecorder) { + var msg = {}; + msg.recorder = -3; + if (altUUID) { + msg.alt = true; + } + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + errorlog("no MediaRecorder"); + return; + } else if (SafariVersion || iPad || iOS) { + var msg = {}; + msg.recorder = -5; + if (altUUID) { + msg.alt = true; + } + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + log("SAFARI/IOS MODE ENABLED"); + } + video.recording = true; + if (remote) { + getById("recordLocalbutton").dataset.state = "1"; + getById("recordLocalbutton").style.backgroundColor = "red"; + getById("recordLocalbutton").innerHTML = ''; + } + } else if (action == "stop") { + warnlog("stop not sensible"); + return; + } else { + log("action is :" + action); + if (video && video.recorder && video.recorder.closing) { + errorlog("Ingore request. Haven't finished closing the previous recording."); + return; + } + + if (!remote) { + getById("recordLocalbutton").dataset.state = "1"; + getById("recordLocalbutton").style.backgroundColor = "red"; + getById("recordLocalbutton").innerHTML = ''; + } + video.recording = true; + } + + video.recorder = {}; + + if (!configureRecording.audioOnly && configureRecording.bitrate < 50) { + configureRecording.bitrate = 50; + } + + if (typeof video.srcObject === "undefined" || !video.srcObject) { + errorlog("video.srcObject undefined"); + return; + } + + log(configureRecording); + + var timestamp = Date.now(); + var filename = ""; + if (session.label || session.streamID) { + filename = session.label || session.streamID; + filename = filename.replace(/[\W]+/g, "_"); + filename = filename.substring(0, 200); + } + + filename += "_" + timestamp.toString(); + log("filename: " + filename); + + video.recorder.eStop = function () { + try { + video.recorder.writer.close(); + clearInterval(video.recorder.writer.interval); + } catch (e) { } + }; + + video.recorder.stop = function (restart = false, notify = false) { + if (session.dbx && video.dropbox && video.dropbox[filename]) { + video.dropbox[filename](false); + } + if (video.gdrive && video.gdrive[filename]) { + video.gdrive[filename].addChunk(false); + } + + try { + if (!remote) { + if (restart) { + if (getById("recordLocalbutton").dataset.state == 2) { + getById("recordLocalbutton").dataset.state = "0"; + getById("recordLocalbutton").style.backgroundColor = ""; + getById("recordLocalbutton").innerHTML = ''; + if (restart !== true) { + warnUser("Media Recording Stopped due to an error: " + restart); + } else { + warnUser("Media Recording Stopped due to an error."); + } + restart = false; + } else { + getById("recordLocalbutton").innerHTML = ''; + getById("recordLocalbutton").dataset.state = "2"; + } + } else { + getById("recordLocalbutton").dataset.state = "0"; + getById("recordLocalbutton").style.backgroundColor = ""; + getById("recordLocalbutton").innerHTML = ''; + if (notify) { + if (!session.cleanOutput) { + warnUser("A recording has stopped unexpectedly."); + } + if (session.beepToNotify) { + playtone(); + } + getById("recordLocalbutton").classList.remove("shake"); + setTimeout(function () { + getById("recordLocalbutton").classList.add("shake"); + }, 10); + } + } + } + } catch (e) { + errorlog(e); + } + + if (!video.recording) { + errorlog("ALREADY STOPPED"); + return; + } + + if (!video.recorder || video.recorder.closing) { + errorlog("it's still closing; can't start until its done"); + return; + } + video.recorder.closing = true; // start the closing process + + try { + if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) { + if (video.recorder.mediaRecorder.state !== "inactive") { + video.recorder.mediaRecorder.stop(); + } + } + } catch (e) { + errorlog(e); + try { + video.recorder.mediaRecorder.stop(); + } catch (e1) { + errorlog(e1); + } + } + + // video.recording = false; + + setTimeout( + (configureRecording, altUUID, video) => { + try { + video.recorder.writer.close(); + } catch (e) { + errorlog(e); + } + try { + clearInterval(video.recorder.writer.interval); + } catch (e) { + errorlog(e); + } + pokeIframeAPI("recording-stopped"); + if (!remote) { + try { + if (session.directorUUID) { + var msg = {}; + msg.recorder = -1; + if (altUUID) { + msg.alt = true; + } + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + } + } catch (e) { + errorlog(e); + } + } + + try { + if (video.recorder && video.recorder.mediaRecorder && video.recorder.mediaRecorder.stop) { + if (video.recorder.mediaRecorder.state !== "inactive") { + video.recorder.mediaRecorder.stop(); + } + } + } catch (e) { + errorlog(e); + try { + video.recorder.mediaRecorder.stop(); + } catch (e1) { + errorlog(e1); + } + } + try { + delete video.recorder; + delete video.recording; + } catch (e) { } + + if (!remote) { + if (restart) { + setTimeout( + function (configureRecording, altUUID) { + recordLocalVideo("start", configureRecording, false, altUUID); + }, + 0, + configureRecording, + altUUID + ); + } + } + }, + 500, + configureRecording, + altUUID, + video + ); + + if (!remote) { + try { + if (session.directorUUID) { + var msg = {}; + if (notify) { + msg.recorder = -4; // user aborted + } else { + msg.recorder = -2; + } + if (altUUID) { + msg.alt = true; + } + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + } + } catch (e) { + errorlog(e); + } + } + }; + + let options = {}; + let filext = ".webm"; + log("setting up options"); + + if (!configureRecording.audioOnly) { + log("videoKbps: " + configureRecording.bitrate); + var tryCodec = session.recordingVideoCodec || ""; // Simplified condition to assign tryCodec + + if (tryCodec && MediaRecorder.isTypeSupported("video/webm;codecs=" + tryCodec)) { + if (!session.cleanOutput) { + console.log("👍 The browser 'says' it supports " + tryCodec); + } + options.mimeType = "video/webm;codecs=" + tryCodec; + + if (configureRecording.usePCM) { + // Fixed the format of the MIME type string + var mimeTypeWithPCM = "video/x-matroska;codecs=" + tryCodec + ",pcm"; + if (MediaRecorder.isTypeSupported(mimeTypeWithPCM)) { + options.mimeType = mimeTypeWithPCM; + } else { + options.mimeType = "video/webm;codecs=pcm"; + } + } + } else { + // Simplified conditions for PCM support + if (tryCodec) { + warnlog("video/webm;codecs=" + tryCodec + " - is not supported"); + } + options.mimeType = configureRecording.usePCM && MediaRecorder.isTypeSupported("video/webm;codecs=pcm") ? "video/webm;codecs=pcm" : "video/webm"; + } + + // Simplified bitrate settings + options.videoBitsPerSecond = parseInt(configureRecording.bitrate * 1024); + if (configureRecording.bitrate < 1000) { + options.audioBitsPerSecond = parseInt(100 * 1024); + } else if (configureRecording.bitrate < 6000) { + options.audioBitsPerSecond = parseInt(130 * 1024); + } else if (configureRecording.bitrate < 20000) { + options.audioBitsPerSecond = parseInt(256 * 1024); + } else { + // If configureRecording.bitrate is >= 20000, use bitsPerSecond for total bitrate + options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024); + } + + if (iOS && options.mimeType) { + if (!MediaRecorder.isTypeSupported(options.mimeType)) { + options.mimeType = "video/mp4"; + filext = ".mp4"; + } + } + + video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); + try { + log(options); + video.recorder.mediaRecorder = new MediaRecorder(video.srcObject, options); + } catch (e) { + warnlog(e); + try { + errorlog("options failed"); + video.recorder.mediaRecorder = new MediaRecorder(video.srcObject); + } catch (e) { + errorlog(e); + errorlog("Failing the recording"); + var msg = {}; + msg.recorder = -3; + if (altUUID) { + msg.alt = true; + } + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + getById("recordLocalbutton").dataset.state = "0"; + getById("recordLocalbutton").style.backgroundColor = ""; + getById("recordLocalbutton").innerHTML = ''; + return; + } + } + + if (session.dbx) { + if (!video.dropbox) { + video.dropbox = {}; + } + + video.dropbox[filename] = await streamVideoToDropbox(filename.toString() + filext); // i don't want to upload to dropbox remote streams; just local + if (!video.dropbox[filename]) { + delete video.dropbox[filename]; + } + } + if (session.gdrive) { + if (!video.gdrive) { + video.gdrive = {}; + } + if (session.gdrive === true) { + video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext); + } else if (session.gdrive.sessionUri) { + video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext, session.gdrive.sessionUri); // filename isn't actually being used here + session.gdrive = false; + } else { + errorlog("gdrive partially setup?"); + video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext); + } + if (!video.gdrive[filename]) { + delete video.gdrive[filename]; + } + } + + log(video.recorder.mediaRecorder); + } else { + log("Audio only?"); + options.mimeType = "audio/webm"; + if (configureRecording.usePCM) { + if (MediaRecorder.isTypeSupported("audio/webm;codecs=pcm")) { + options.mimeType = "audio/webm;codecs=pcm"; + } + } else { + options.bitsPerSecond = parseInt(configureRecording.bitrate * 1024); + } + var stream = createMediaStream(); + var audioTrack = false; + video.srcObject.getAudioTracks().forEach(track => { + audioTrack = true; + stream.addTrack(track, video.srcObject); + }); + + if (!audioTrack) { + errorlog("Failing the recording; no audio track"); + try { + video.recorder.writer.close(); + } catch (e) { } + + try { + clearInterval(video.recorder.writer.interval); + } catch (e) { } + + try { + delete video.recorder; + delete video.recording; + } catch (e) { } + var msg = {}; + msg.recorder = -3; + if (altUUID) { + msg.alt = true; + } + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + getById("recordLocalbutton").dataset.state = "0"; + getById("recordLocalbutton").style.backgroundColor = ""; + getById("recordLocalbutton").innerHTML = ''; + return; + } else { + if (iOS && options.mimeType) { + if (!MediaRecorder.isTypeSupported(options.mimeType)) { + options.mimeType = "video/mp4"; + } + } + + try { + video.recorder.mediaRecorder = new MediaRecorder(stream, options); + } catch (e) { + warnlog(e); + try { + errorlog("options failed. failing safe.."); + video.recorder.mediaRecorder = new MediaRecorder(stream); + } catch (e) { + errorlog(e); + errorlog("Fail safe failed; closing the recording"); + try { + video.recorder.writer.close(); + } catch (e) { } + + try { + clearInterval(video.recorder.writer.interval); + } catch (e) { } + + try { + delete video.recorder; + delete video.recording; + } catch (e) { } + var msg = {}; + msg.recorder = -3; + if (altUUID) { + msg.alt = true; + } + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + getById("recordLocalbutton").dataset.state = "0"; + getById("recordLocalbutton").style.backgroundColor = ""; + getById("recordLocalbutton").innerHTML = ''; + return; + } + } + if (session.dbx) { + if (!video.dropbox) { + video.dropbox = {}; + } + video.dropbox[filename] = await streamVideoToDropbox(filename.toString() + filext); // i don't want to upload to dropbox remote streams; just local + if (!video.dropbox[filename]) { + delete video.dropbox[filename]; + } + } + if (session.gdrive) { + if (!video.gdrive) { + video.gdrive = {}; + } + if (session.gdrive === true) { + video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext); + } else if (session.gdrive.sessionUri) { + video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext, session.gdrive.sessionUri); // filename isn't actually being used here + session.gdrive = false; + } else { + video.gdrive[filename] = setupGoogleDriveUploader(filename.toString() + filext); + errorlog("Gdrive only partially setup"); + } + if (!video.gdrive[filename]) { + delete video.gdrive[filename]; + } + } + } + } + log(options); + + function createLock() { + let isLocked = false; + let queue = Promise.resolve(); + + return { + acquire: async () => { + const release = () => { isLocked = false; }; + while (isLocked) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + isLocked = true; + return release; + } + }; + } + + var chunkQueue = []; + async function handleDataAvailable(event, process = true) { + if (!video.recorder.writerLock) { + video.recorder.writerLock = createLock(); + } + + // Handle existing queue first + while (chunkQueue.length && process) { + const ret = await handleDataAvailable(chunkQueue.shift(), false); + if (ret === false) { + return; + } + } + + if (event.data && event.data.size > 0) { + try { + const release = await video.recorder.writerLock.acquire(); + try { + if (video && video.recorder && video.recorder.writer && video.recorder.writer._ownerWritableStream && video.recorder.writer._ownerWritableStream._state === "writable") { + await video.recorder.writer.write(event.data); + } else { + throw new Error("Writer not open"); + } + } catch (e) { + if (process === true) { + chunkQueue.push(event); + } else { + chunkQueue.unshift(event); + release(); + return false; + } + } finally { + release(); + } + + // Rest of the existing code for messaging and cloud uploads + if (session.directorList.length) { + if (video.recording) { + var msg = {}; + if (altUUID) { + msg.alt = true; + } + msg.recorder = parseInt((Date.now() - timestamp) / 1000) || 0; + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + } + } + + if (session.dbx && video.dropbox && video.dropbox[filename]) { + video.dropbox[filename](event.data); + } + if (video.gdrive && video.gdrive[filename]) { + video.gdrive[filename].addChunk(event.data); + } + } catch (e) { + errorlog(e); + } + } + } + + video.recorder.mediaRecorder.ondataavailable = (event) => { + handleDataAvailable(event).catch(e => errorlog(e)); + }; + + video.recorder.mediaRecorder.onerror = function (event) { + errorlog(event); + console.log("It's possible using &recordcodec=vp8 might resolve recording errors if caused by an incompatible hardware encoder or codec"); + if (event && event.error && event.error.name) { + video.recorder.stop(event.error.name); + } else { + video.recorder.stop(true); + } + }; + + video.srcObject.onended = function (event) { + video.recorder.stop(); + }; + + try { + video.recorder.iteration = 0; + + video.recorder.setupWriter = async function setupWriter(video) { + if (video.recorder.writer) { + try { + video.recorder.writer.close(); + await sleep(1000); + // we don't cancel the interval obviously + } catch (e) { + errorlog(e); + } + } + var { readable, writable } = new TransformStream({ + transform: (chunk, ctrl) => chunk.arrayBuffer().then(b => ctrl.enqueue(new Uint8Array(b))) + }); + var writer = await writable.getWriter(); + var addon = ""; + if (video.recorder.iteration != 0) { + addon = "_" + video.recorder.iteration; + } + video.recorder.iteration += 1; + readable.pipeTo(streamSaver.createWriteStream(filename.toString() + filext + addon, video.recorder.stop)); + video.recorder.writer = writer; + }; + + await video.recorder.setupWriter(video); + + if (session.recordingInterval) { + // minutes + function intervalClosure(video) { + var intervalId = setInterval( + function (video) { + try { + video.recorder.setupWriter(video); + } catch (e) { + clearInterval(intervalId); + } + }, + 1000 * 60 * session.recordingInterval, + video + ); + video.recorder.writer.interval = intervalId; + } + intervalClosure(video); + } + + video.recorder.mediaRecorder.start(1000); // 100ms chunks + + log("started recording"); + + pokeIframeAPI("recording-started"); + + getById("recordLocalbutton").dataset.state = "1"; + getById("recordLocalbutton").style.backgroundColor = "red"; + getById("recordLocalbutton").innerHTML = ''; + + if (session.directorList.length) { + var msg = {}; + if (altUUID) { + msg.alt = true; + } + msg.recorder = 0; + for (var i = 0; i < session.directorList.length; i++) { + msg.UUID = session.directorList[i]; + session.sendMessage(msg, msg.UUID); + } + } + } catch (e) { + errorlog(e); + } + return; +} + +async function recordWindowCapture(bitrate = 6000) { + // Streamlined window/tab recording for scenes + // Captures the current browser tab and records to disk + // + // Customizable via URL parameters: + // &recordwindow=BITRATE - recording bitrate in kbps (default: 6000) + // &pcm - use PCM audio (lossless, larger files) + // &screensharefps=FPS - capture framerate (default: 60) + // &screensharequality=X - resolution: 4k, 2k, 1080p, 720p, etc. + // &width=W&height=H - custom resolution + + if (session.recordWindowElement && session.recordWindowElement.recording) { + log("Window recording already in progress"); + return; + } + + try { + // Determine resolution from session parameters + var targetWidth = 1920; + var targetHeight = 1080; + + if (session.screensharequality) { + var q = parseInt(session.screensharequality); + if (q === -2) { // 4k + targetWidth = 3840; + targetHeight = 2160; + } else if (q === -3) { // 2k/1440p + targetWidth = 2560; + targetHeight = 1440; + } else if (q === 1) { // 720p + targetWidth = 1280; + targetHeight = 720; + } else if (q === 2) { // 360p + targetWidth = 640; + targetHeight = 360; + } + } + if (session.width) { + targetWidth = parseInt(session.width) || targetWidth; + } + if (session.height) { + targetHeight = parseInt(session.height) || targetHeight; + } + + // Determine framerate + var targetFps = 60; + if (session.screensharefps) { + targetFps = parseInt(session.screensharefps) || 60; + } + + var constraints = { + video: { + frameRate: { ideal: targetFps }, + width: { ideal: targetWidth }, + height: { ideal: targetHeight }, + cursor: "never" + }, + audio: true, + preferCurrentTab: true, + selfBrowserSurface: "include", + surfaceSwitching: "exclude" + }; + + if (session.displaySurface) { + constraints.video.displaySurface = session.displaySurface; + } + if (session.suppressLocalAudioPlayback) { + constraints.audio = { suppressLocalAudioPlayback: true }; + } + + log("Starting window capture: " + targetWidth + "x" + targetHeight + "@" + targetFps + "fps"); + var stream = await navigator.mediaDevices.getDisplayMedia(constraints); + + // Create a temp video element to hold the capture + var video = document.createElement("video"); + video.id = "recordWindowSource"; + video.srcObject = stream; + video.muted = true; + video.autoplay = true; + video.playsInline = true; + video.style.display = "none"; + document.body.appendChild(video); + + session.recordWindowElement = video; + + // Handle stream ending (user stops sharing) + stream.getVideoTracks()[0].onended = function() { + log("Window capture ended"); + if (video.recording) { + recordLocalVideo("stop", false, video); + } + video.remove(); + session.recordWindowElement = null; + // Reset button state if exists + var btn = document.getElementById("recordWindowButton"); + if (btn) { + btn.innerHTML = "● Start Recording"; + btn.title = "Record this scene to a local video file"; + btn.style.background = "#d00"; + btn.style.opacity = "1"; + btn.dataset.recording = "0"; + } + // Stop Go Live if active + if (session.goLivePC) { + try { + session.goLivePC.close(); + } catch(e) {} + session.goLivePC = null; + } + var liveBtn = document.getElementById("goLiveButton"); + if (liveBtn) { + liveBtn.innerHTML = "📡 Go Live"; + liveBtn.title = "Stream to Twitch via WHIP (requires stream key)"; + liveBtn.style.background = "#6441a5"; + liveBtn.dataset.live = "0"; + } + }; + + await video.play(); + + // Start recording using existing infrastructure + var usePCM = session.pcm || false; + var configureRecording = { + bitrate: bitrate, + usePCM: usePCM, + audioOnly: false + }; + + log("Starting window recording at " + bitrate + " kbps" + (usePCM ? " with PCM audio" : "")); + recordLocalVideo("start", configureRecording, video); + + } catch (e) { + errorlog("Window capture failed: " + e); + if (session.recordWindowElement) { + session.recordWindowElement.remove(); + session.recordWindowElement = null; + } + // Reset button state on error/cancel + var btn = document.getElementById("recordWindowButton"); + if (btn) { + btn.innerHTML = "● Start Recording"; + btn.style.background = "#d00"; + btn.style.opacity = "1"; + btn.dataset.recording = "0"; + } + } +} + +function localGlobalRecordStart() { + document.querySelectorAll("[data-action-type='recorder-local']").forEach(target => { + var UUID = target.dataset.UUID; + if (!UUID) { + return; + } + var video = session.rpcs[UUID].videoElement; + if (!video) { + return; + } + if (!video.stopWriter) { + recordVideo(target); // if not started, start + } + }); + recordLocalVideo("start"); // self +} +function localGlobalRecordStop() { + document.querySelectorAll("[data-action-type='recorder-local']").forEach(target => { + var UUID = target.dataset.UUID; + if (!UUID) { + return; + } + var video = session.rpcs[UUID].videoElement; + if (!video) { + return; + } + if (video.stopWriter) { + recordVideo(target); // if started, stop + } + }); + recordLocalVideo("stop"); // self +} +async function remoteGlobalRecordStart() { + window.focus(); + var bitrate = await promptAlt(miscTranslations["what-bitrate"], false, false, 6000); + document.querySelectorAll("[data-action-type='recorder-remote']").forEach(target => { + requestVideoRecord(target, true, bitrate); + }); +} +function remoteGlobalRecordStop() { + document.querySelectorAll("[data-action-type='recorder-remote']").forEach(target => { + if (target.classList.contains("pressed")) { + requestVideoRecord(target, false); + } + }); +} +session.onTrack = function (event, UUID) { + if (session.badStreamList.includes(session.rpcs[UUID].streamID)) { + errorlog("new connection is contained in badStreamList 2! This shouldn't happen"); + // we will have none of this. + return; + } + + var newTracks = []; + var newStream = false; + if (event.streams && event.streams[0]) { + newStream = event.streams[0]; + newTracks = newStream.getTracks(); + } else if (event.track) { + newTracks.push(event.track); + } else { + errorlog("Something went wrong with incoming track.."); + return; + } + + if (session.rpcs[UUID].streamSrc) { + var tracks = session.rpcs[UUID].streamSrc.getTracks(); + for (var i = 0; i < newTracks.length; i++) { + for (var j = 0; j < tracks.length; j++) { + if (newTracks[i].id == tracks[j].id && newTracks[i].kind == tracks[j].kind) { + // FIX: Only replace if old track is dead (ended) + // This prevents audio clicks during normal operation + if (tracks[j].readyState === "ended") { + try { + session.rpcs[UUID].streamSrc.removeTrack(tracks[j]); + log("Replaced dead " + tracks[j].kind + " track"); + } catch(e) { warnlog(e); } + } else { + // Old track is still live - skip duplicate as before + newTracks.splice(i, 1); + i--; + } + break; + } + } + } + } + + var screenshare = false; + var screenshareParentOverride = null; + if (session.rpcs[UUID].screenIndexes && session.rpcs[UUID].getReceivers && session.rpcs[UUID].screenIndexes.length) { + log("session.rpcs[UUID].screenIndexes: " + session.rpcs[UUID].screenIndexes); + var receievers = session.rpcs[UUID].getReceivers(); // excluded + for (var i = 0; i < receievers.length; i++) { + for (var j = 0; j < newTracks.length; j++) { + if (receievers[i].track && receievers[i].track.id == newTracks[j].id && receievers[i].track.kind == newTracks[j].kind) { + for (var k = 0; k < session.rpcs[UUID].screenIndexes.length; k++) { + if (session.rpcs[UUID].screenIndexes[k] == i) { + screenshare = true; + break; + } + } + } + if (screenshare) { + break; + } + } + if (screenshare) { + break; + } + } + } + if (typeof UUID === "string" && UUID.endsWith("_screen")) { + if (!screenshare) { + screenshare = true; + } + if (session.rpcs[UUID] && session.rpcs[UUID].realUUID) { + screenshareParentOverride = session.rpcs[UUID].realUUID; + } else { + screenshareParentOverride = UUID.slice(0, -7); + } + if (session.rpcs[UUID]) { + session.rpcs[UUID].screenShareState = true; + if (typeof session.rpcs[UUID].smallScreen === "undefined" || session.rpcs[UUID].smallScreen === null) { + session.rpcs[UUID].smallScreen = false; + } + } + } + + if (screenshare) { + const parentUUID = screenshareParentOverride || (typeof UUID === "string" && UUID.endsWith("_screen") ? UUID.slice(0, -7) : UUID); + if (parentUUID && session.rpcs[parentUUID]) { + session.rpcs[parentUUID].screenShareState = true; + } + if (parentUUID && session.rpcs[parentUUID + "_screen"]) { + session.rpcs[parentUUID + "_screen"].screenShareState = true; + } + } + + log("screenshare: " + screenshare); + log(session.rpcs[UUID].streamID); + + try { + var index = newTracks.length; + while (index--) { + if (newTracks[index].kind == "video") { + if (session.novideo !== false && !session.novideo.includes(session.rpcs[UUID].streamID)) { + if (!(screenshare && session.novideo.includes(session.rpcs[UUID].streamID + ":s"))) { + newTracks.splice(index, 1); + } + continue; + } else if (session.rpcs[UUID].settings && session.rpcs[UUID].settings.allowscreenvideo && screenshare) { + //newTracks.splice(index,1); + continue; + } else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.video) { + newTracks.splice(index, 1); + continue; + } + } else if (newTracks[index].kind == "audio") { + if (session.noaudio !== false && !session.noaudio.includes(session.rpcs[UUID].streamID)) { + if (!(screenshare && session.noaudio.includes(session.rpcs[UUID].streamID + ":s"))) { + newTracks.splice(index, 1); + } + continue; + } else if (session.excludeaudio && session.excludeaudio.includes(session.rpcs[UUID].streamID)) { + newTracks.splice(index, 1); + continue; + } else if (session.rpcs[UUID].settings && session.rpcs[UUID].settings.allowscreenaudio && screenshare) { + //newTracks.splice(index,1); + continue; + } else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.audio) { + newTracks.splice(index, 1); + continue; + } + } + } + } catch (e) { + errorlog(e); + } + + if (!newTracks.length) { + warnlog("NO NEW TRACKS?"); + return; + } + + if (session.encodedInsertableStreams && session.rpcs[UUID] && session.rpcs[UUID].getReceivers) { + var receievers = session.rpcs[UUID].getReceivers(); // excluded + for (var i = 0; i < receievers.length; i++) { + for (var j = 0; j < newTracks.length; j++) { + if (receievers[i].track && receievers[i].track.id == newTracks[j].id && receievers[i].track.kind == newTracks[j].kind) { + try { + setupReceiverTransform(receievers[i]); + } catch (e) { + errorlog(e); + } + } + } + } + } + + if (screenshare) { + var targetUUID = screenshareParentOverride || UUID; + if (session.rpcs[targetUUID]) { + session.setupScreenShareAddon(newTracks, targetUUID); + } else { + session.setupScreenShareAddon(newTracks, UUID); + } + return; + } + + //if (session.buffer!==false){ + playoutdelay(UUID); + //} + + session.directorSpeakerMute(); // apply any mute states to new tracks. + session.directorDisplayMute(); + + if (newStream) { + newStream.onremovetrack = function (e1) { + try { + warnlog("Track was removed"); + session.rpcs[UUID].streamSrc.getTracks().forEach(trk => { + if (trk.id == e1.track.id && trk.kind == e1.track.kind) { + session.rpcs[UUID].streamSrc.removeTrack(trk); + } + }); + if (e1.track.kind == "video") { + updateIncomingVideoElement(UUID, true, false); + } else { + updateIncomingVideoElement(UUID, false, true); + } + // updateIncomingVideoElement(UUID); // session.rpcs[UUID].videoElement.srcObject = session.rpcs[UUID].streamSrc; + setTimeout(function () { + updateMixer(); + }, 1); + } catch (e) { } + }; + + newStream.onerror = function (e1) { + errorlog(e1); + try { + warnlog("Track threw an error; going to reconnect it"); + session.rpcs[UUID].streamSrc.getTracks().forEach(trk => { + try { + if (trk.id == e1.track.id && trk.kind == e1.track.kind) { + session.rpcs[UUID].streamSrc.removeTrack(trk); + } + } catch (e) { } + }); + if (e1.track.kind == "video") { + updateIncomingVideoElement(UUID, true, false); + } else { + updateIncomingVideoElement(UUID, false, true); + } + setTimeout(function () { + updateMixer(); + }, 1); + } catch (e) { + errorlog(e); + } + }; + } + + createRichVideoElement(UUID); + + if (!session.rpcs[UUID].streamSrc) { + session.rpcs[UUID].streamSrc = createMediaStream(); + mediaSourceUpdated(UUID, session.rpcs[UUID].streamID); + } + + var videoAdded = false; + var audioAdded = false; + + newTracks.forEach(trk => { + if (trk.kind == "video") { + videoAdded = true; + } else if (trk.kind == "audio") { + audioAdded = true; + } + log("adding track"); + session.rpcs[UUID].streamSrc.addTrack(trk); + }); + + if (newTracks.length > session.rpcs[UUID].streamSrc.getTracks().length) { + errorlog("Not all the tracks were added to the local stream; are the tracks' IDs not unique?"); + console.log("streamSrc total tracks: " + session.rpcs[UUID].streamSrc.getTracks().length); + } + + if (isIFrame && session.sendframes) { + sendFrameHandler(newTracks, UUID); + } + + if (audioAdded && videoAdded) { + updateIncomingVideoElement(UUID); + } else if (videoAdded) { + updateIncomingVideoElement(UUID, true, false); + } else if (audioAdded) { + updateIncomingVideoElement(UUID, false, true); + if (!session.roomid && session.view && !session.permaid) { + setTimeout(function () { + updateMixer(); + }, 10); // video already has an auto-start, with aspect ratio size change. audio doesn't. + } + } + + if (session.twilio && audioAdded) { + session.twilio.updateMixer(UUID); + } + return session; +}; + +function sendFrameHandler(tracks, UUID = null) { + tracks.forEach(async trk => { + if (trk.kind !== "video") return; + + log("STARTING NEW SEND STREAM VIDEO TRACK"); + + const startImageStream = async () => { + + log("startImageStream"); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + const processor = new MediaStreamTrackProcessor(trk); + const reader = processor.readable.getReader(); + + while (true) { + try { + const { done, value: frame } = await reader.read(); + if (done) break; + try { + canvas.width = frame.displayWidth; + canvas.height = frame.displayHeight; + ctx.drawImage(frame, 0, 0); + const format = typeof session.sendframes === "string" ? session.sendframes : "webp"; + const imageData = canvas.toDataURL(`image/${format}`, 0.8); + + parent.postMessage({ + type: 'frame', + frame: imageData, + UUID, + streamID: (session.rpcs[UUID] ? session.rpcs[UUID].streamID : null), + trackID: trk.id, + kind: trk.kind, + format: format + }, session.sendframes); + } finally { + frame.close(); + } + } catch (e) { + console.error("Error processing image frame:", e); + break; + } + } + }; + + const startFrameStream = async () => { + log("startFrameStream"); + const processor = new MediaStreamTrackProcessor(trk); + const reader = processor.readable.getReader(); + + while (true) { + try { + const { done, value } = await reader.read(); + if (done) { + if (value) value.close(); + break; + } + + try { + parent.postMessage({ + frame: value, + UUID, + streamID: (session.rpcs[UUID] ? session.rpcs[UUID].streamID : null), + trackID: trk.id, + kind: trk.kind, + type: "frame" + }, session.sendframes, [value]); + } finally { + value.close(); + } + } catch (e) { + console.error("Error processing video frame:", e); + if (e.name === "DataCloneError") { + // Fall back to image stream if frame transfer fails + return startImageStream(); + } + break; + } + } + }; + + try { + if (typeof MediaStreamTrackProcessor === 'function') { + try { + new SharedArrayBuffer(1); + await startFrameStream(); + } catch (e) { + console.warn(e); + await startImageStream(); + } + } else { + await startImageStream(); + } + } catch (e) { + console.error("Stream processing failed:", e); + } + }); +} + +function updateIncomingVideoElement(UUID, video = true, audio = true) { + if (!session.rpcs[UUID].videoElement) { + return; + } + if (!session.rpcs[UUID].streamSrc) { + return; + } + + if (!session.rpcs[UUID].videoElement.srcObject) { + session.rpcs[UUID].videoElement.srcObject = createMediaStream(); + } + + if (video) { + var tracks = session.rpcs[UUID].videoElement.srcObject.getVideoTracks(); // add video track + + session.rpcs[UUID].streamSrc.getVideoTracks().forEach(trk => { + var added = false; + tracks.forEach(trk2 => { + if (trk.id == trk2.id && trk.kind == trk2.kind) { + added = true; + } + }); + if (!added) { + session.rpcs[UUID].videoElement.srcObject.getVideoTracks().forEach(trk2 => { + // make sure only one video track is added at a time. + log("removetrack"); + session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2); + }); + + if (trk.muted && trk.kind == "video" && session.director) { + trk.onunmute = function (e) { + if (!session.rpcs[UUID]) { + return; + } + this.onunmute = null; + warnlog("ON UN-MUTE"); + updateIncomingVideoElement(UUID, true, false); + }; + } else { + if (session.rpcs[UUID].videoElement.controls) { + session.rpcs[UUID].videoElement.controls = session.showControls || false; + if (session.showControls === null) { + setTimeout( + function (ele) { + if (ele) { + ele.controls = true; + } + }, + 500, + session.rpcs[UUID].videoElement + ); + } + } + session.rpcs[UUID].videoElement.srcObject.addTrack(trk); + mediaVideoTrackUpdated(UUID, session.rpcs[UUID].streamID); + } + } + }); + + if ((session.motionSwitch || session.motionRecord) && !session.rpcs[UUID].motionDetectionInterval) { + session.rpcs[UUID].motionDetectionInterval = setTimeout(function () { + setInterval(function () { + motionDetection(session.rpcs[UUID].videoElement, session.motionSwitch || session.motionRecord); + }, 400); + }, 2000); + } + } + if (audio) { + updateIncomingAudioElement(UUID); // do the same for audio now. + if (!video) { + if (session.rpcs[UUID] && session.rpcs[UUID].videoElement) { + // this bit of code fixes an issue where the volume button doesn't show, after adding an audio track for the first time + var pastMuteState = session.rpcs[UUID].videoElement.muted; + session.rpcs[UUID].videoElement.muted = !pastMuteState; + session.rpcs[UUID].videoElement.muted = pastMuteState; + } + } + } + if ((session.optimize === 0) && session.activatedStreams.size && session.rpcs[UUID] && session.rpcs[UUID].streamID) { + if (session.activatedStreams.has(session.rpcs[UUID].streamID)) { + if (session.activatedStreamsQueue[session.rpcs[UUID].streamID]) { + let msgs = session.activatedStreamsQueue[session.rpcs[UUID].streamID]; + delete session.activatedStreamsQueue[session.rpcs[UUID].streamID]; + msgs.forEach(msgWithTime => { + log(msgWithTime); + if (msgWithTime.time && (msgWithTime.time > Date.now() - 10000)) { + session.directorActions(msgWithTime.msg); + } + }); + } + } + } +} + +function updateIncomingAudioElement(UUID) { + // this can be called when turning on/off inbound audio processing. + if (!session.rpcs[UUID] || !session.rpcs[UUID].videoElement || !session.rpcs[UUID].streamSrc) { + return; + } + + if (!session.rpcs[UUID].videoElement.srcObject) { + session.rpcs[UUID].videoElement.srcObject = createMediaStream(); + } + + log("updateIncomingAudioElement: " + UUID); + if (session.audioEffects === true || session.pushLoudness || (session.rpcs[UUID].isolatedChannel !== undefined)) { + var tracks = session.rpcs[UUID].streamSrc.getAudioTracks(); + if (tracks.length) { + var track = tracks[0]; + track = addAudioPipeline(UUID, track); + log(track); + var added = false; + var tracks2 = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); + log(tracks2); + tracks2.forEach(trk2 => { + if (trk2.label && trk2.label == "MediaStreamAudioDestinationNode") { + // an old morphed node; delete it. + session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2); + } else if (track.id == trk2.id && track.kind == trk2.kind) { + // maybe it didn't morph; already added either way + added = true; + } else if (tracks[0].id == trk2.id && tracks[0].kind == trk2.kind && track.id != tracks[0].id) { + // remove original audio track that is now morphed + session.rpcs[UUID].videoElement.srcObject.removeTrack(trk2); + } + }); + if (!added) { + session.rpcs[UUID].videoElement.srcObject.addTrack(track); + mediaAudioTrackUpdated(UUID, session.rpcs[UUID].streamID); + } + } else { + session.rpcs[UUID].videoElement.srcObject.getAudioTracks().forEach(trk => { + // make sure to remove all tracks. + session.rpcs[UUID].videoElement.srcObject.remove(trk); + }); + } + } else { + var expected = []; + tracks = session.rpcs[UUID].videoElement.srcObject.getAudioTracks(); // add audio tracks + session.rpcs[UUID].streamSrc.getAudioTracks().forEach(trk => { + var added = false; + tracks.forEach(trk2 => { + if (trk.id == trk2.id && trk.kind == trk2.kind) { + added = true; + expected.push(trk2); // + } + }); + if (!added) { + session.rpcs[UUID].videoElement.srcObject.addTrack(trk); + mediaAudioTrackUpdated(UUID, session.rpcs[UUID].streamID); + } + }); + tracks.forEach(trk => { + var added = false; + expected.forEach(trk2 => { + if (trk.id == trk2.id && trk.kind == trk2.kind) { + added = true; + } + }); + if (!added) { + // not expected. so lets delete. + warnlog("this shouldn't happen that often, audio track orphaned. removing it"); + session.rpcs[UUID].videoElement.srcObject.removeTrack(trk); + } + }); + } + + if (session.mixMinus) { + stream = mixMinusAudio(UUID); // only works with p2p; no chunked mode. + } +} + +function cycleStyleOptions() { + session.style += 1; + if (session.style > 6) { + session.style = 1; + } else if (session.style == 4) { + session.style = 5; + } + + for (var UUID in session.rpcs) { + if (session.rpcs[UUID].canvas) { + try { + if (session.rpcs[UUID].canvas) { + session.rpcs[UUID].canvas.remove(); + } + } catch (e) { } + session.rpcs[UUID].canvas = null; + } + updateIncomingAudioElement(UUID); + } + updateMixer(); +} + +function addAudioPipeline(UUID, track) { + // INBOUND AUDIO EFFECTS ; audio tracks only + try { + if (session.disableViewerWebAudioPipeline) { + log("ignoring addAudioPipeline - disableViewerWebAudioPipeline is enabled (noap)"); + return track; + } + + log("Triggered webaudio effects path"); + + for (var tid in session.rpcs[UUID].inboundAudioPipeline) { + delete session.rpcs[UUID].inboundAudioPipeline[tid]; // get rid of old nodes. + } + var trackid = track.id; // this is an audio track, or should be. + + session.rpcs[UUID].inboundAudioPipeline[trackid] = {}; + + session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream = createMediaStream(); + session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream.addTrack(track); + + if (ChromiumVersion && session.audioEffects) { + // I'm going to deprecate this. + session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio = createAudioElement(); // TODO: I don't know if this mutedAudio thing matters any more, in recent versions of Chrome, since it won't play even if muted. + session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = true; + session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.playsinline = true; // ## Added Oct 9th 2022. Not sure it's does anything, but might help with iPhones? + session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.srcObject = session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream; // needs to be added as an streamed element to be usable, even if its hidden + session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = true; + //session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.volume = 0.01; + session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio + .play() + .then(_ => { + //session.rpcs[UUID].inboundAudioPipeline[trackid].mutedAudio.muted = false; + log("playing 1"); + }) + .catch(warnlog); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaStreamTrackSource + var source = session.audioCtx.createMediaStreamSource(session.rpcs[UUID].inboundAudioPipeline[trackid].mediaStream); + + ////////////////// + + var screwedUp = false; + session.rpcs[UUID].inboundAudioPipeline[trackid].destination = false; + + if (session.rpcs[UUID].isolatedChannel !== undefined) { + log("Isolating channel: " + session.rpcs[UUID].isolatedChannel); + session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); + source = isolateChannel(source, session.rpcs[UUID].isolatedChannel); + screwedUp = true; + } + + if (session.sync !== false) { + log("adding a delay node to audio"); + source = addDelayNode(source, UUID, trackid); + screwedUp = true; + } + if (session.style === 2) { + log("adding a fftwave node to audio"); + try { + if (session.rpcs[UUID].inboundAudioPipeline[trackid]) { + // clear audioMeterGuest, if active. + clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval); + } + } catch (e) { } + source = fftWaveform(source, UUID, trackid); + } else if (session.style === 3 || session.meterStyle) { + log("adding a loudness meter node to audio"); + source = audioMeterGuest(source, UUID, trackid); + } else if (session.audioMeterGuest) { + log("adding a loudness meter node to audio"); + source = audioMeterGuest(source, UUID, trackid); + } else if (session.activeSpeaker) { + log("adding a loudness meter node to audio"); + source = audioMeterGuest(source, UUID, trackid); + } else if (session.quietOthers) { + log("adding a loudness meter node to audio"); + source = audioMeterGuest(source, UUID, trackid); + } else if (session.pushLoudness) { + source = audioMeterGuest(source, UUID, trackid); + } else { + try { + if (session.rpcs[UUID].inboundAudioPipeline[trackid]) { + // nothign active, so clear + clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval); + } + } catch (e) { } + } + + if (session.playChannel) { + session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); + source = selectChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.playChannel); + screwedUp = true; + } else if (session.rpcs[UUID].channelOffset !== false) { + log("custom offset set"); + session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); + source = offsetChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.rpcs[UUID].channelOffset, session.rpcs[UUID].channelWidth); + screwedUp = true; + } else if (session.offsetChannel !== false) { + // proably better to do this last. + log("adding offset channels"); + session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); + source = offsetChannel(session.rpcs[UUID].inboundAudioPipeline[trackid].destination, source, session.offsetChannel, session.channelWidth); + screwedUp = true; + } else if (session.panning !== false) { + // proably better to do this last. + log("adding offset channels"); + session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); + source = stereoPanning(source, UUID, trackid, session.panning); + screwedUp = true; + } else if (session.rpcs[UUID].videoElement && session.rpcs[UUID].videoElement.manualSink) { + screwedUp = true; // added June-3-22 to allow for custom outputs to different audio output destinations. + } + + if (screwedUp) { + warnlog("screwedUp mode activated. dun dun"); + if (session.rpcs[UUID].inboundAudioPipeline[trackid].destination === false) { + session.rpcs[UUID].inboundAudioPipeline[trackid].destination = session.audioCtx.createMediaStreamDestination(); + } + source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].destination); + + try { + if (session.firstPlayTriggered && session.audioCtx.state == "suspended") { + log("trying to resume.."); + session.audioCtx.resume(); + } + } catch (e) { + warnlog("session.audioCtx.resume(); failed"); + } + + return session.rpcs[UUID].inboundAudioPipeline[trackid].destination.stream.getAudioTracks()[0]; + } + + try { + if (session.firstPlayTriggered && session.audioCtx.state == "suspended") { + session.audioCtx.resume(); + } + } catch (e) { + warnlog("session.audioCtx.resume(); failed 2"); + } + + return track; + } catch (e) { + errorlog(e); + } + return track; +} + +function processMiniInfoUpdate(miniInfo, UUID) { + if ("qlr" in miniInfo) { + session.rpcs[UUID].stats.info.quality_limitation_reason = miniInfo.qlr; + } + + if ("con" in miniInfo) { + session.rpcs[UUID].stats.info.conn_type = miniInfo.con; + } + + if ("cpu" in miniInfo) { + session.rpcs[UUID].stats.info.cpuLimited = miniInfo.cpu; + if (session.rpcs[UUID].signalMeter) { + if (miniInfo.cpu) { + session.rpcs[UUID].signalMeter.dataset.cpu = "1"; + } else if ("cpu" in miniInfo) { + session.rpcs[UUID].signalMeter.dataset.cpu = "0"; + } + } + } + + if ("hw_enc" in miniInfo) { + session.rpcs[UUID].stats.info.hardware_video_encoder = miniInfo.hw_enc; + } + + if ("bat" in miniInfo) { + if (typeof miniInfo.bat == "number") { + session.rpcs[UUID].stats.info.power_level = miniInfo.bat * 100; + } else { + session.rpcs[UUID].stats.info.power_level = null; + } + } + if ("chrg" in miniInfo) { + session.rpcs[UUID].stats.info.plugged_in = miniInfo.chrg; + } + + if ("out" in miniInfo && "c" in miniInfo.out) { + session.rpcs[UUID].stats.info.total_outbound_p2p_connections = miniInfo.out.c; + if (session.showConnections && session.rpcs[UUID].connectionDetails) { + session.rpcs[UUID].connectionDetails.innerText = "🔗" + session.rpcs[UUID].stats.info.total_outbound_p2p_connections; + session.rpcs[UUID].connectionDetails.dataset.value = session.rpcs[UUID].stats.info.total_outbound_p2p_connections; + } + } + + if (session.rpcs[UUID].batteryMeter) { + batteryMeterInfoUpdate(UUID); + } +} + +function batteryMeterInfoUpdate(UUID) { + if (session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.power_level !== null) { + var level = session.rpcs[UUID].batteryMeter.querySelector(".battery-level"); + if (level) { + var value = session.rpcs[UUID].stats.info.power_level; + if (value > 100) { + value = 100; + } + if (value < 0) { + value = 0; + } + level.style.height = parseInt(value) + "%"; + if (value < 15) { + session.rpcs[UUID].batteryMeter.classList.remove("warn"); + session.rpcs[UUID].batteryMeter.classList.add("alert"); + } else if (value < 25) { + session.rpcs[UUID].batteryMeter.classList.remove("alert"); + session.rpcs[UUID].batteryMeter.classList.add("warn"); + } else { + session.rpcs[UUID].batteryMeter.classList.remove("alert"); + session.rpcs[UUID].batteryMeter.classList.remove("warn"); + } + if (value < 100) { + session.rpcs[UUID].batteryMeter.classList.remove("hidden"); + } + //session.rpcs[UUID].batteryMeter.title = value+"% battery remaining"; + session.rpcs[UUID].batteryMeter.title = parseInt(value) + "% battery remaining"; + } + } + + if (session.rpcs[UUID].stats.info && "plugged_in" in session.rpcs[UUID].stats.info && session.rpcs[UUID].stats.info.plugged_in === false) { + session.rpcs[UUID].batteryMeter.dataset.plugged = "0"; + session.rpcs[UUID].batteryMeter.classList.remove("hidden"); + } else { + session.rpcs[UUID].batteryMeter.dataset.plugged = "1"; + // add on + session.rpcs[UUID].batteryMeter.title = parseInt(value) + "% charging"; + session.rpcs[UUID].batteryMeter.classList.add("hidden"); + } +} + +function setupGuestLabelControl(UUID) { + var labelID = getById("label_" + UUID); + if (labelID) { + labelID.classList.add("contolboxLabel"); + labelID.dataset.UUID = UUID; + + if (session.rpcs[UUID].label) { + labelID.innerText = session.rpcs[UUID].label; // Replace underscores with a Space when publishing to HTML. No Double spaces. + labelID.classList.remove("addALabel"); + } else if (session.directorUUID === UUID) { + miniTranslate(labelID, "main-director"); + labelID.classList.remove("addALabel"); + } else if (session.directorList.indexOf(UUID) >= 0) { + miniTranslate(labelID, "co-director"); + labelID.classList.remove("addALabel"); + } else { + miniTranslate(labelID, "add-a-label"); + labelID.classList.add("addALabel"); + } + labelID.onclick = async function (ee) { + var oldlabel = ee.target.innerText; + if (session.rpcs[ee.target.dataset.UUID].label === false) { + oldlabel = ""; + } + window.focus(); + var newlabel = await promptAlt(getTranslation("new-display-name"), false, false, oldlabel); + if (newlabel !== null) { + newlabel = newlabel.trim(); + if (newlabel == "") { + newlabel = false; + if (session.directorUUID === UUID) { + miniTranslate(ee.target, "main-director"); + //ee.target.innerHTML = getTranslation("main-director"); + ee.target.classList.remove("addALabel"); + } else if (session.directorList.indexOf(UUID) >= 0) { + miniTranslate(ee.target, "co-director"); + //ee.target.innerHTML = getTranslation("co-director"); + ee.target.classList.remove("addALabel"); + } else { + miniTranslate(ee.target, "add-a-label"); + //ee.target.innerHTML = getTranslation("add-a-label"); + ee.target.classList.add("addALabel"); + } + } else { + ee.target.innerText = newlabel; + ee.target.classList.remove("addALabel"); + } + var data = {}; + data.UUID = ee.target.dataset.UUID; + data.changeLabel = true; + data.value = newlabel; + session.sendRequest(data, data.UUID); + + // Update local label immediately and sync to co-directors + try { + session.rpcs[UUID].label = newlabel; + session.rpcs[UUID].labelSetByDirector = true; // Prevent info message from overwriting + // Push label update to co-directors via directorState sync + if (session.director && session.rpcs[UUID].streamID) { + var labelPayload = { directorState: {} }; + labelPayload.directorState[session.rpcs[UUID].streamID] = getDetailedState(session.rpcs[UUID].streamID); + session.pushDirectorStateUpdate(labelPayload, "label-change"); + } + } catch (e) { errorlog(e); } + + pokeAPI("details", getDetailedState(session.rpcs[UUID].streamID)); + } + + }; + } + updateAriaLabel(UUID); +} + +function updateAriaLabel(UUID = false) { + if (!(UUID in session.rpcs)) { + return; + } + + var element = document.getElementById("container_" + UUID); + if (!element) { + return; + } + var sid = ""; + if (session.rpcs[UUID].streamID) { + sid = ": " + session.rpcs[UUID].streamID; + } + + if (session.rpcs[UUID].label) { + element.setAttribute("aria-label", session.rpcs[UUID].label); + } else if (session.directorUUID === UUID) { + element.setAttribute("aria-label", (getTranslation("main-director") || "Main Director") + sid); + } else if (session.directorList.indexOf(UUID) >= 0) { + element.setAttribute("aria-label", (getTranslation("co-director") || "Co Director") + sid); + } else if (sid) { + element.setAttribute("aria-label", "ID" + sid); + } else { + element.setAttribute("aria-label", getTranslation("undefined") || "Undefined"); + } + + element.setAttribute("role", "region"); +} + +function updateLabelDirectors(UUID) { + var elements = getById("label_" + UUID); + if (elements) { + if (session.rpcs[UUID].label) { + elements.innerText = session.rpcs[UUID].label; + elements.classList.remove("addALabel"); + } else if (session.directorUUID === UUID) { + miniTranslate(elements, "main-director"); + elements.classList.remove("addALabel"); + } else if (session.directorList.indexOf(UUID) >= 0) { + miniTranslate(elements, "co-director"); + elements.classList.remove("addALabel"); + } else { + miniTranslate(elements, "add-a-label"); + elements.classList.add("addALabel"); + } + } + updateAriaLabel(UUID); +} + +function updateLabelDirectors2(UUID) { + var elements = getById("label_" + UUID); + if (elements) { + if (session.directorUUID === UUID) { + miniTranslate(elements, "main-director"); + elements.classList.remove("addALabel"); + } else if (session.directorList.indexOf(UUID) >= 0) { + miniTranslate(elements, "co-director"); + elements.classList.remove("addALabel"); + } else { + miniTranslate(elements, "add-a-label"); + elements.classList.add("addALabel"); + } + } + updateAriaLabel(UUID); +} + +function directorCoDirectorColoring(UUID) { + if (UUID === session.directorUUID) { + try { + session.rpcs[UUID].stats.info.director = true; + getById("container_" + UUID).classList.add("directorBox"); + } catch (e) { } + } else if (session.directorList.indexOf(UUID) >= 0) { + try { + session.rpcs[UUID].stats.info.coDirector = true; + addDirectorBlue(UUID); + } catch (e) { } + } +} + +function addDirectorBlue(UUID) { + try { getById("container_" + UUID).classList.add("directorBlue"); log("[ui] addDirectorBlue for UUID=" + UUID); } catch (e) { errorlog(e); } +} + +function soloLinkGeneratorInit(UUID) { + document.querySelectorAll("container_" + UUID).forEach(ele => { + ele.querySelectorAll("[data-sololink]").forEach(ele2 => { + // value='" + soloLink + "' href='" + soloLink + "'/>" + soloLink + " + var soloLink = soloLinkGenerator(session.rpcs[UUID].streamID, false); + ele2.value = soloLink; + ele2.href = soloLink; + ele2.innerText = soloLink; + }); + }); +} + +function initRecordingImpossible(UUID) { + var ele = document.querySelectorAll('[data-action-type="mute-guest"][data--u-u-i-d="' + UUID + '"]'); + if (ele) { + ele.disabled = true; + ele.title = getTranslation("Audio processing is disabled with this guest. Can't mute or change volume"); + } + var ele = document.querySelectorAll('[data-action-type="volume"][data--u-u-i-d="' + UUID + '"]'); + if (ele) { + ele.disabled = true; + ele.title = title = getTranslation("Audio processing is disabled with this guest. Can't mute or change volume"); + ele.style.opacity = 0.2; + } +} + +function initGroupButtons(UUID) { + var elements = document.querySelectorAll('[data-action-type="toggle-group"][data--u-u-i-d="' + UUID + '"]'); + for (var i = 0; i < elements.length; i++) { + elements[i].classList.remove("pressed"); + elements[i].ariaPressed = "false"; + for (var g = 0; g < session.rpcs[UUID].group.length; g++) { + if (elements[i].dataset.group === session.rpcs[UUID].group[g]) { + elements[i].classList.add("pressed"); + elements[i].ariaPressed = "true"; + } + } + } +} + +function changeGroupDirector(ele, state = null) { + var group = ele.dataset.group; + + var index = session.group.indexOf(group); + + var change = false; + + if (state === true) { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + if (index === -1) { + session.group.push(group); + change = true; + } + } else if (state === false) { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + if (index > -1) { + session.group.splice(index, 1); + change = true; + } + } else if (ele.classList.contains("pressed")) { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + if (index > -1) { + session.group.splice(index, 1); + change = true; + } + } else { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + if (index === -1) { + session.group.push(group); + change = true; + } + } + + if (session.group.length || session.allowNoGroup) { + session.sendMessage({ group: session.group.join(",") }); + } else { + session.sendMessage({ group: false }); + } + if (change) { + updateMixer(); + pokeIframeAPI("group-set-updated", session.group); + } + + if (session.group.indexOf(group) === -1) { + return false; + } else { + return true; + } +} + +function changeGroupDirectorAPI(group, state = null, update = true) { + log("changeGroupDirectorAPI()"); + group = sanitizeLabel(group); + + if (document.getElementById("container_director")) { + var ele = getById("container_director").querySelector('[data-action-type="toggle-group"][data-group="' + group + '"]'); + if (ele) { + if (update) { + ele.click(); + } else if (state === true) { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } + if (session.group.indexOf(group) === -1) { + return false; + } else { + return true; + } + } + } + + var index = session.group.indexOf(group); + + var eleGroup = getById("groups"); + eleGroup.classList.remove("hidden"); + var ele = eleGroup.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"'); + + if (eleGroup.showDirector) { + if (!ele) { + ele = htmlToElement('"); + + var added = false; + eleGroup.querySelectorAll("[data-group]").forEach(ele2 => { + log(ele2); + if (!added && ele2.dataset.group > group + "") { + ele2.parentNode.insertBefore(ele, ele2); + added = true; + } + }); + if (!added) { + eleGroup.appendChild(ele); + } + } + } else if (!ele) { + ele = document.createElement("div"); + ele.dataset.actionType = "toggle-group"; + ele.dataset.group = group; + ele.classList.add("float"); + ele.style.display = "inline-block"; + ele.role = "button"; + ele.innerHTML = '
    ' + group; + eleGroup.appendChild(ele); + ele.onclick = function () { + changeGroupDirectorAPI(this.dataset.group); + }; + } + var changed = false; + + if (state === true) { + if (eleGroup.showDirector) { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } else { + ele.classList.add("green"); + ele.ariaPressed = "true"; + } + if (index === -1) { + session.group.push(group); + changed = true; + } + } else if (state === false) { + if (eleGroup.showDirector) { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } else { + ele.classList.remove("green"); + ele.ariaPressed = "false"; + } + if (index > -1) { + session.group.splice(index, 1); + changed = true; + } + } else if (ele.classList.contains("green")) { + if (eleGroup.showDirector) { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } else { + ele.classList.remove("green"); + ele.ariaPressed = "false"; + } + if (index > -1) { + session.group.splice(index, 1); + changed = true; + } + } else { + if (eleGroup.showDirector) { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } else { + ele.classList.add("green"); + ele.ariaPressed = "true"; + } + if (index === -1) { + session.group.push(group); + changed = true; + } + } + if (update) { + if (session.group.length || session.allowNoGroup) { + session.sendMessage({ group: session.group.join(",") }); + } else { + session.sendMessage({ group: false }); + } + } + if (changed) { + updateMixer(); + pokeIframeAPI("group-set-updated", session.group); + } + + if (state !== null) { + return true; + } else if (session.group.indexOf(group) === -1) { + return false; + } else { + return true; + } +} + +function changeGroupViewDirectorAPI(group, state = null) { + log("changeGroupViewDirectorAPI()"); + group = sanitizeLabel(group); + + var index = session.groupView.indexOf(group); + + var changed = false; + + if (state === true) { + if (index === -1) { + session.groupView.push(group); + changed = true; + } + } else if (state === false) { + if (index > -1) { + session.groupView.splice(index, 1); + changed = true; + } + } else { + if (index > -1) { + session.groupView.splice(index, 1); + } else { + session.groupView.push(group); + } + changed = true; + } + + if (changed) { + updateMixer(); + pokeIframeAPI("group-view-set-updated", session.groupView); + } + + if (state !== null) { + return true; + } else if (session.groupView.indexOf(group) === -1) { + return false; + } else { + return true; + } +} + +function changeGroup(ele, state = null) { + var group = ele.dataset.group; + + var index = session.rpcs[ele.dataset.UUID].group.indexOf(group); + + var changed = false; + + if (state === true) { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + if (index === -1) { + session.rpcs[ele.dataset.UUID].group.push(group); + changed = true; + } + } else if (state === false) { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + if (index > -1) { + session.rpcs[ele.dataset.UUID].group.splice(index, 1); + changed = true; + } + } else if (ele.classList.contains("pressed")) { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + if (index > -1) { + session.rpcs[ele.dataset.UUID].group.splice(index, 1); + changed = true; + } + } else { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + if (index === -1) { + session.rpcs[ele.dataset.UUID].group.push(group); + changed = true; + } + } + if (session.rpcs[ele.dataset.UUID].group.length) { + session.sendRequest({ group: session.rpcs[ele.dataset.UUID].group.join(",") }, ele.dataset.UUID); + } else { + session.sendRequest({ group: false }, ele.dataset.UUID); + } + syncDirectorState(ele); + + if (changed) { + updateMixer(); + } + + if (session.rpcs[ele.dataset.UUID].group.indexOf(group) === -1) { + return false; + } else { + return true; + } +} + +function changeChannelOffset(UUID, channel) { + var ele = document.querySelectorAll('[data-action-type="add-channel"][data--u-u-i-d="' + UUID + '"]'); + for (var i = 0; i < ele.length; i++) { + if (channel === i) { + if (ele[i].classList.contains("pressed")) { + ele[i].classList.remove("pressed"); + ele[i].ariaPressed = "false"; + channel = false; + } else { + ele[i].classList.add("pressed"); + ele[i].ariaPressed = "true"; + } + } else { + ele[i].classList.remove("pressed"); + ele[i].ariaPressed = "false"; + } + } + session.rpcs[UUID].channelOffset = channel; + + updateIncomingVideoElement(UUID, false, true); + if (channel === false) { + return false; + } else { + return true; + } +} + +function toggleMonoStereoMic(ele) { + if (ele.checked) { + session.audioInputChannels = 1; + + getById("micStereoMonoInput").checked = true; + getById("micStereoMonoInput3").checked = true; + } else if (urlParams.has("channelcount") || urlParams.has("ac") || urlParams.has("inputchannels")) { + // not ideal having this here + session.audioInputChannels = urlParams.get("channelcount") || urlParams.get("ac") || urlParams.get("inputchannels") || 0; + session.audioInputChannels = parseInt(session.audioInputChannels); + if (!session.audioInputChannels) { + session.audioInputChannels = false; + } + getById("micStereoMonoInput").checked = false; + getById("micStereoMonoInput3").checked = false; + } else { + session.audioInputChannels = false; + getById("micStereoMonoInput").checked = false; + getById("micStereoMonoInput3").checked = false; + } + try { + activatedPreview = false; + if (ele.id == "micStereoMonoInput3") { + grabAudio("#audioSource3"); + } else { + grabAudio("#audioSource"); + } + } catch (e) { + errorlog(e); + } +} + +function selectChannel(destination, source, channel) { + session.audioCtx.destination.channelCountMode = "explicit"; + session.audioCtx.destination.channelInterpretation = "discrete"; + destination.channelCountMode = "explicit"; + destination.channelInterpretation = "discrete"; + + try { + destination.channelCount = 1; + } catch (e) { + errorlog("Max channels: " + destination.channelCount); + } + + var splitter = session.audioCtx.createChannelSplitter(6); + var merger = session.audioCtx.createChannelMerger(1); // mono + + source.connect(splitter); + splitter.connect(merger, channel - 1, 0); + + return merger; +} + +function offsetChannel(destination, source, offset, width = false) { + session.audioCtx.destination.channelCountMode = "explicit"; + session.audioCtx.destination.channelInterpretation = "discrete"; + destination.channelCountMode = "explicit"; + destination.channelInterpretation = "discrete"; + + try { + destination.channelCount = session.audioChannels; + } catch (e) { + errorlog("Max channels: " + destination.channelCount); + } + + if (width) { + var splitter = session.audioCtx.createChannelSplitter(width); + var merger = session.audioCtx.createChannelMerger(width + offset); + } else { + var splitter = session.audioCtx.createChannelSplitter(2); + var merger = session.audioCtx.createChannelMerger(2 + offset); + } + + source.connect(splitter); + splitter.connect(merger, 0, offset); + + if (session.stereo && session.stereo != 3) { + splitter.connect(merger, 1, 1 + offset); + } + return merger; +} + +function addReverb(source, UUID, trackid, value) { + // not yet actually working. requires a buffer; bleh! + if (value === true) { + value = Math.random() * (Math.random() * 2 - 1); + errorlog(value); + } else if (value === false) { + return source; + } else { + value = parseFloat(value / 90) - 1 || 0; + if (value < -1) { + value = -1; + } + if (value > 1) { + value = 1; + } + } + //// some reverb logic goes here... + ///var reverbNode = session.audioCtx.createStereoPanner(); + ///session.rpcs[UUID].inboundAudioPipeline[trackid].reverbNode = reverbNode; + //// + + source.connect(reverbNode); + return reverbNode; +} + +function stereoPanning(source, UUID, trackid, value) { + // Normalize value to [-1, 1] where 0=center + if (parseInt(value) === -1) { + value = Math.random() * (Math.random() * 2 - 1); + warnlog(value); + } else if (value === false) { + return source; + } else if (value === true) { + value = 90; + } else { + // input 0..180 => -1..1 + value = parseFloat(value / 90) - 1 || 0; + } + if (value < -1) value = -1; + if (value > 1) value = 1; + + // Pre-pan gain trim to avoid clipping + var gainNode = session.audioCtx.createGain(); + session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode = gainNode; + gainNode.gain.value = 1 - Math.abs(value) / 2; + source.connect(gainNode); + + // Create panner with Safari fallback + var panNode; + try { + if (session.audioCtx.createStereoPanner) { + panNode = session.audioCtx.createStereoPanner(); + session.rpcs[UUID].inboundAudioPipeline[trackid].panType = "stereo"; + panNode.pan.value = value; + } else { + panNode = session.audioCtx.createPanner(); + panNode.panningModel = "equalpower"; + panNode.distanceModel = "inverse"; + var x = value; + var z = 1 - Math.abs(value); + try { + if (typeof panNode.positionX !== "undefined") { + panNode.positionX.value = x; + panNode.positionY.value = 0; + panNode.positionZ.value = z; + } else if (panNode.setPosition) { + panNode.setPosition(x, 0, z); + } + } catch (e) { } + session.rpcs[UUID].inboundAudioPipeline[trackid].panType = "panner"; + } + } catch (e) { + warnlog("Stereo panning node creation failed; bypassing"); + return gainNode; + } + + session.rpcs[UUID].inboundAudioPipeline[trackid].panNode = panNode; + gainNode.connect(panNode); + return panNode; +} + +function adjustPan(UUID, value) { + if (value === true) { + value = Math.random() * (Math.random() * 2 - 1); + } else if (value === false) { + value = 0; + } else { + value = parseFloat(value / 90) - 1 || 0; + if (value < -1) { + value = -1; + } + if (value > 1) { + value = 1; + } + } + + for (var trackid in session.rpcs[UUID].inboundAudioPipeline) { + if ("panNode" in session.rpcs[UUID].inboundAudioPipeline[trackid]) { + try { + if (session.rpcs[UUID].inboundAudioPipeline[trackid].panType === "stereo" && session.rpcs[UUID].inboundAudioPipeline[trackid].panNode.pan) { + session.rpcs[UUID].inboundAudioPipeline[trackid].panNode.pan.setValueAtTime(value, session.audioCtx.currentTime); + } else { + // Fallback panner + var x = value; + var z = 1 - Math.abs(value); + var pn = session.rpcs[UUID].inboundAudioPipeline[trackid].panNode; + if (typeof pn.positionX !== "undefined") { + pn.positionX.setValueAtTime(x, session.audioCtx.currentTime); + pn.positionY.setValueAtTime(0, session.audioCtx.currentTime); + pn.positionZ.setValueAtTime(z, session.audioCtx.currentTime); + } else if (pn.setPosition) { + pn.setPosition(x, 0, z); + } + } + } catch (e) { warnlog(e); } + } + if ("gainPanNode" in session.rpcs[UUID].inboundAudioPipeline[trackid] && session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode.gain) { + try { session.rpcs[UUID].inboundAudioPipeline[trackid].gainPanNode.gain.setValueAtTime(1 - Math.abs(value) / 2, session.audioCtx.currentTime); } catch (e) { } + } + } +} + +function addDelayNode(source, UUID, trackid) { + // append the delay Node to the track??? WOULD THIS WORK? + + var delay = parseFloat(session.sync) || 0; + if (delay < 0) { + delay = 0; + } + + if ((session.audioBuffer !== false) && session.audioBuffer >= 0) { + delay += parseFloat(session.audioBuffer); + } else if (session.buffer && session.buffer > 0) { + delay += parseFloat(session.buffer); + } + + delay = delay / 1000; + + session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode = session.audioCtx.createDelay(delay + 5); // 5 seconds additionally added for the purpose of flexibility + + session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode.delayTime.value = delay; // delayTime takes it in seconds. + source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode); + log("added new delay node"); + return session.rpcs[UUID].inboundAudioPipeline[trackid].delayNode; +} + +function createStyleCanvas(UUID) { + // append the delay Node to the track??? WOULD THIS WORK? + if (!session.rpcs[UUID].canvas) { + // just make sure that if using &effects or something, to null the canvas after use, else this won't trigger. + session.rpcs[UUID].canvas = document.createElement("canvas"); + session.rpcs[UUID].canvas.dataset.UUID = UUID; + if (session.rpcs[UUID].streamID) { + session.rpcs[UUID].canvas.dataset.sid = session.rpcs[UUID].streamID; + } + session.rpcs[UUID].canvas.style.pointerEvents = "auto"; + session.rpcs[UUID].canvasCtx = session.rpcs[UUID].canvas.getContext("2d", { alpha: session.alpha }); + // + session.rpcs[UUID].canvas.addEventListener("click", function (e) { + // show stats of video if double clicked + log("clicked"); + try { + var uid = e.currentTarget.dataset.UUID; + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + if (session.statsMenu !== false) { + if ("stats" in session.rpcs[uid]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, uid); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, uid); + } + } + e.stopPropagation(); + return false; + } else if ("prePausedBandwidth" in session.rpcs[uid]) { + unPauseVideo(e.currentTarget); + } + } catch (e) { + errorlog(e); + } + }); + + if (session.statsMenu) { + if ("stats" in session.rpcs[UUID]) { + var [menu, innerMenu] = statsMenuCreator(); + printViewStats(innerMenu, UUID); + menu.interval = setInterval(printViewStats, session.statsInterval, innerMenu, UUID); + } + } + + if (session.aspectRatio) { + if (session.aspectRatio == 1) { + session.rpcs[UUID].canvas.width = "720"; + session.rpcs[UUID].canvas.height = "1280"; + } else if (session.aspectRatio == 2) { + session.rpcs[UUID].canvas.width = "960"; + session.rpcs[UUID].canvas.height = "960"; + } else if (session.aspectRatio == 3) { + session.rpcs[UUID].canvas.width = "1280"; + session.rpcs[UUID].canvas.height = "960"; + } + } else { + session.rpcs[UUID].canvas.width = "1280"; + session.rpcs[UUID].canvas.height = "720"; + } + + updateMixer(); + return true; + } else { + return false; + } +} + +function applyStyleEffect(UUID) { + if (!session.rpcs[UUID].canvas || !session.rpcs[UUID].canvasCtx) { + return; + } + + /* session.rpcs[UUID].canvasContainer = document.createElement("div"); + session.rpcs[UUID].canvasContainer.appendChild(session.rpcs[UUID].canvas); + session.rpcs[UUID].canvas.style = "width:100%;height:100%;display:block;"; + session.rpcs[UUID].canvasContainer.appendChild(session.rpcs[UUID].videoElement); */ + + if (session.style == 3) { + // black + session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0, 0, 0)"; + session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height); + } else if (session.style == 4) { + session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0, 0, 0)"; + session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height); + } else if (session.style == 5) { + var r = Math.random() * 255; + var g = Math.random() * 255; + var b = Math.random() * 255; + session.rpcs[UUID].canvasCtx.fillStyle = "rgb(" + r + ", " + g + ", " + b + ")"; + session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height); + } else if (session.style == 6) { + session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0,0,0)"; + session.rpcs[UUID].canvasCtx.fillRect(0, 0, session.rpcs[UUID].canvas.width, session.rpcs[UUID].canvas.height); + + var r = Math.random() * 150 + 50; + var g = Math.random() * 150 + 50; + var b = Math.random() * 150 + 50; + session.rpcs[UUID].canvasCtx.fillStyle = "rgb(" + r + ", " + g + ", " + b + ")"; + session.rpcs[UUID].canvasCtx.beginPath(); + session.rpcs[UUID].canvasCtx.arc(parseInt(session.rpcs[UUID].canvas.width / 2), parseInt(session.rpcs[UUID].canvas.height / 2), parseInt(session.rpcs[UUID].canvas.height / 4), 0, 2 * Math.PI, false); + session.rpcs[UUID].canvasCtx.fill(); + + if (session.rpcs[UUID].label) { + session.rpcs[UUID].canvasCtx.fillStyle = "rgb(0,0,0)"; + session.rpcs[UUID].canvasCtx.textAlign = "center"; + session.rpcs[UUID].canvasCtx.font = parseInt(session.rpcs[UUID].canvas.height / 2.11) + "px Arial"; + session.rpcs[UUID].canvasCtx.fillText(session.rpcs[UUID].label[0].toUpperCase(), parseInt(session.rpcs[UUID].canvas.width / 2), parseInt((session.rpcs[UUID].canvas.height * 2) / 3)); + } else { + var tmp = getComputedStyle(document.querySelector(":root")).getPropertyValue("--video-background-image").split('"'); + if (tmp.length === 3) { + var img = new Image(); + img.onload = function () { + session.rpcs[UUID].canvasCtx.fillStyle = "rgb(25,0,0)"; + session.rpcs[UUID].canvasCtx.drawImage(img, parseInt(session.rpcs[UUID].canvas.width / 2 - 110), parseInt(session.rpcs[UUID].canvas.height / 2 - 110), 220, 220); + }; + img.src = tmp[1]; + } + } + } +} + +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function fftWaveform(source, UUID, trackid) { + // append the delay Node to the track??? WOULD THIS WORK? + // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser = session.audioCtx.createAnalyser(); + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.fftSize = 512; + var bufferLength = session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.frequencyBinCount; + var dataArray = new Uint8Array(bufferLength); + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteTimeDomainData(dataArray); + // analyser.getByteTimeDomainData(dataArray); + source.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser); + + createStyleCanvas(UUID); + clearInterval(session.rpcs[UUID].canvasIntervalAction); + var canvasIntervalAction = setInterval( + function (uuid) { + if (session.style !== 2) { + clearInterval(canvasIntervalAction); // this is FFT only, so okay to kill. + return; + } + + try { + session.rpcs[uuid].inboundAudioPipeline[trackid].analyser.getByteTimeDomainData(dataArray); + session.rpcs[uuid].canvasCtx.fillStyle = "rgba(0, 0, 0, 0.1)"; + session.rpcs[uuid].canvasCtx.fillRect(0, 0, session.rpcs[uuid].canvas.width, session.rpcs[uuid].canvas.height); + session.rpcs[uuid].canvasCtx.lineWidth = 10; + session.rpcs[uuid].canvasCtx.strokeStyle = "rgb(111, 255, 111)"; + + var sliceWidth = (session.rpcs[uuid].canvas.width * 1.0) / bufferLength; + + var loudness = dataArray; + var Squares = loudness.map(val => (val - 128.0) * (val - 128.0)); + var Sum = Squares.reduce((acum, val) => acum + val); + var Mean = Sum / loudness.length; + loudness = Math.sqrt(Mean) * 10; + session.rpcs[uuid].stats.Audio_Loudness = parseInt(loudness); + + if (session.pushLoudness == true) { + var loudnessObj = {}; + loudnessObj[session.rpcs[uuid].streamID] = session.rpcs[uuid].stats.Audio_Loudness; + + if (isIFrame) { + parent.postMessage({ loudness: loudnessObj, action: "loudness", value: loudness, UUID: uuid }, session.iframetarget); + } + } + + if (loudness < 2) { + return; + } + + //log(bufferLength); + session.rpcs[uuid].canvasCtx.beginPath(); + var m = session.rpcs[uuid].canvas.height / 256.0; + session.rpcs[uuid].canvasCtx.moveTo(0, dataArray[0] * m); + var x = 0; + for (var i = 1; i < bufferLength; i++) { + var y = dataArray[i] * m; + session.rpcs[uuid].canvasCtx.lineTo(x, y); + x += sliceWidth; + } + session.rpcs[uuid].canvasCtx.lineTo(session.rpcs[uuid].canvas.width, session.rpcs[uuid].canvas.height / 2); + session.rpcs[uuid].canvasCtx.stroke(); + } catch (e) { + warnlog(e); + warnlog("Did the remote source disconnect?"); + clearInterval(canvasIntervalAction); + warnlog(session.rpcs[uuid]); + } + }, + 50, + UUID + ); + session.rpcs[UUID].canvasIntervalAction = canvasIntervalAction; + return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser; +} + +function audioMeterGuest(mediaStreamSource, UUID, trackid) { + log("audioMeterGuest started"); + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser = session.audioCtx.createAnalyser(); + mediaStreamSource.connect(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser); + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.fftSize = 256; + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.smoothingTimeConstant = 0.05; + + var bufferLength = session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.frequencyBinCount; + var dataArray = new Uint8Array(bufferLength); + + function updateLevels() { + try { + if (!session.rpcs[UUID]) { + return; + } + + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.getByteFrequencyData(dataArray); + var total = 0; + for (var i = 0; i < dataArray.length; i++) { + total += dataArray[i]; + } + total = parseInt(total / 150); + session.rpcs[UUID].stats.Audio_Loudness = total; + + if (session.pushLoudness == true) { + var loudnessObj = {}; + loudnessObj[session.rpcs[UUID].streamID] = session.rpcs[UUID].stats.Audio_Loudness; + + if (isIFrame) { + parent.postMessage({ loudness: loudnessObj, action: "loudness", value: session.rpcs[UUID].stats.Audio_Loudness, UUID: UUID }, session.iframetarget); + } + } + + try { + clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval); + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () { + updateLevels(); + }, 100); + } catch (e) { + log("closing old inaudio pipeline"); + } + + if (session.style == 3 || session.meterStyle) { + // overrides style + if (session.rpcs[UUID].videoElement) { + if (total > 40) { + session.rpcs[UUID].videoElement.dataset.speaking = "2"; + } else if (total > 10) { + session.rpcs[UUID].videoElement.dataset.speaking = "1"; + } else { + session.rpcs[UUID].videoElement.dataset.speaking = "0"; + } + + if (session.meterStyle == 4) { + session.rpcs[UUID].videoElement.dataset.loudness = total; + return; // this is cause we are using the data-loudness + } + } else if (session.meterStyle == 4) { + return; + } + } else if (session.scene !== false) { + // if a scene, cancel + return; + } else if (session.audioMeterGuest === false) { + // don't show if we just want the volume levels + return; + } + + if (session.rpcs[UUID].voiceMeter) { + session.rpcs[UUID].voiceMeter.dataset.level = total; + if (session.meterStyle == 1) { + var perct = Math.min(total, 100); + + session.rpcs[UUID].voiceMeter.style.height = perct + "%"; + if (total > 80) { + var R = parseInt((255 * perct) / 100) + .toString(16) + .padStart(2, "0"); + var G = parseInt(255 - (255 * perct) / 100) + .toString(16) + .padStart(2, "0"); + session.rpcs[UUID].voiceMeter.style.backgroundColor = "#" + R + G + "00"; + } else { + session.rpcs[UUID].voiceMeter.style.backgroundColor = "#00FF00"; + } + } else { + if (total > 15) { + session.rpcs[UUID].voiceMeter.style.opacity = 100; // temporary + } else { + session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary + } + } + } else { + session.rpcs[UUID].voiceMeter = document.createElement("div"); + session.rpcs[UUID].voiceMeter.id = "voiceMeter_" + UUID; + session.rpcs[UUID].voiceMeter.dataset.level = total; + if (session.meterStyle == 1) { + session.rpcs[UUID].voiceMeter.classList.add("video-meter2"); + } else { + if (total > 15) { + session.rpcs[UUID].voiceMeter.style.opacity = 100; // temporary + } else { + session.rpcs[UUID].voiceMeter.style.opacity = 0; // temporary + } + if (session.meterStyle == 2) { + session.rpcs[UUID].voiceMeter.classList.add("video-meter-2"); + } else { + session.rpcs[UUID].voiceMeter.classList.add("video-meter"); + } + } + updateMixer(); + } + } catch (e) { + warnlog(e); + // fail as an exception; this is a control close. + return; + } + } + clearTimeout(session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval); + session.rpcs[UUID].inboundAudioPipeline[trackid].analyser.interval = setTimeout(function () { + updateLevels(); + }, 100); + return session.rpcs[UUID].inboundAudioPipeline[trackid].analyser; +} + +function effectsDynamicallyUpdate(event, ele) { + log("effectsDynamicallyUpdate"); + let lastEffectValue = session.effect; + session.effect = ele.options[ele.selectedIndex].value; + + getById("selectImageContent").style.display = "none"; + getById("selectImageContent3").style.display = "none"; + getById("selectImageOverlay").style.display = "none"; + getById("selectImageOverlay3").style.display = "none"; + getById("selectEffectAmount").style.display = "none"; + getById("selectEffectAmount3").style.display = "none"; + + if (session.effect === "1") { + updateRenderOutpipe(); + return; + } + + if (session.effect === "7") { + // Show zoom amount sliders + getById("selectEffectAmount").style.display = "block"; + getById("selectEffectAmount3").style.display = "block"; + + // Show both sets of position controls + getById("zoomPositionControls").style.display = "block"; + getById("zoomPositionControls3").style.display = "block"; + + if (session.effectValue_default) { + session.effectValue = session.effectValue_default; + } else { + session.effectValue = 1; + } + + // Set up zoom amount sliders + const zoomInputs = ["selectEffectAmountInput", "selectEffectAmountInput3"]; + zoomInputs.forEach(id => { + const input = getById(id); + if (input) { + input.min = 1; + input.max = 4; + input.step = 0.1; + input.value = session.effectValue; + } + }); + + // Initialize all position sliders + const sliderPairs = [ + ["zoomPositionX1", "zoomPositionX"], + ["zoomPositionY1", "zoomPositionY"] + ]; + + sliderPairs.forEach(pair => { + pair.forEach(id => { + const slider = getById(id); + if (slider) { + if (id.includes('X')) { + slider.value = xPosition; + } else { + slider.value = yPosition; + } + } + }); + }); + } else { + // Hide all position controls + getById("zoomPositionControls").style.display = "none"; + getById("zoomPositionControls3").style.display = "none"; + } + + if (["8", "overlay"].includes(session.effect)) { + // like zoom but none + if (session.effect === "overlay") { + loadOverlayImages(); + } + updateRenderOutpipe(); + return; + } + + if (session.effect == "3a") { + session.effect = "3"; + session.effectValue = 5; + } + if (session.effectValue_default === false && session.effect == "3") { + session.effectValue = 2; + } else { + session.effectValue = session.effectValue_default; + } + + if (session.effect == "0" || !session.effect) { + updateRenderOutpipe(); + return; + } else if (session.effect === "3" || session.effect === "4") { + if (!["3", "4", "5"].includes(lastEffectValue)) { + attemptTFLiteJsFileLoad(); + if (!session.tfliteModule.looping) { + updateRenderOutpipe(); + } + } + if (session.effect === "3" && session.effectValue_default == false) { + getById("selectEffectAmount").style.display = "block"; + getById("selectEffectAmount3").style.display = "block"; + + getById("selectEffectAmountInput").min = 0; + getById("selectEffectAmountInput").max = 20; + getById("selectEffectAmountInput").step = 1; + getById("selectEffectAmountInput3").min = 0; + getById("selectEffectAmountInput3").max = 20; + getById("selectEffectAmountInput3").step = 1; + + getById("selectEffectAmountInput").value = session.effectValue; + getById("selectEffectAmountInput3").value = session.effectValue; + } + } else if (session.effect === "5") { + if (!["3", "4", "5"].includes(lastEffectValue)) { + attemptTFLiteJsFileLoad(); + if (!session.tfliteModule.looping) { + updateRenderOutpipe(); + } + } + loadContentEffectsImages(); + } else if ((session.effect === "14" || session.effect === "15") && session.effectValue_default == false) { + getById("selectEffectAmount").style.display = "block"; + getById("selectEffectAmount3").style.display = "block"; + + getById("selectEffectAmountInput").min = 1; + getById("selectEffectAmountInput").max = 50; + getById("selectEffectAmountInput").step = 1; + getById("selectEffectAmountInput3").min = 1; + getById("selectEffectAmountInput3").max = 50; + getById("selectEffectAmountInput3").step = 1; + + getById("selectEffectAmountInput").value = parseInt(session.effectValue) || 25; + getById("selectEffectAmountInput3").value = parseInt(session.effectValue) || 25; + + loadContentEffectsImages(); + } else if (session.effect === "6") { + if (!gpgpuSupport) { + if (!session.cleanOutput) { + warnUser("Hardware acceleration isn't detected.

    This effect will not work", 4000, false); + return; + } + } else if (gpgpuSupport == "Google SwiftShader") { + if (!session.cleanOutput) { + warnUser("Hardware acceleration isn't detected.

    Please enable it for this effect to work correctly.

    Settings -> Advanced -> System -> Use hardware-accleration", false, false); + } + return; + } + loadTensorflowJS(); + updateRenderOutpipe(); + //mainMeshMask(); + } else { + //loadEffect(session.effect); + updateRenderOutpipe(); + } + + if (session.permaid === false && session.roomid === false && session.view === false && session.director === false) { + updateURL("effects"); + } +} + +function loadContentEffectsImages() { + if (!["5", "15"].includes(session.effect)) { + return; + } // only load for certain effects + + if (session.defaultBackgroundImages) { + try { + session.defaultBackgroundImages.reverse(); + } catch (e) { + errorlog("Could not process image list"); + session.defaultBackgroundImages = false; + session.selectedImage_contents = getById("selectImage_contents"); + return; + } + session.defaultBackgroundImages.forEach(imgSrc => { + try { + var img = document.createElement("img"); + img.onerror = function () { + this.style.display = "none"; + }; // hide images that fail to load + img.crossOrigin = "Anonymous"; + img.src = imgSrc; + img.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;"; + img.onclick = function (event) { + changeEffectsImage(event, this); + }; + getById("selectImage_contents").prepend(img); + } catch (e) { } + }); + session.defaultBackgroundImages = false; + session.selectedImage_contents = getById("selectImage_contents"); + } else if (!session.selectedImage_contents) { + session.selectedImage_contents = getById("selectImage_contents"); + } + if (document.getElementById("selectImageContent")) { + document.getElementById("selectImageContent").style.display = "block"; + document.getElementById("selectImageContent").appendChild(session.selectedImage_contents); + session.selectedImage_contents.classList.remove("hidden"); + } else if (document.getElementById("selectImageContent3")) { + document.getElementById("selectImageContent3").style.display = "block"; + document.getElementById("selectImageContent3").appendChild(session.selectedImage_contents); + session.selectedImage_contents.classList.remove("hidden"); + } +} + +async function changeOverlayImage(ev, ele) { + if (ele.files && ele.files[0]) { + if (session.foregroundImg) { + session.foregroundImg.classList.remove("selectedContentEffectsImage"); + } + session.foregroundImg = document.createElement("img"); + session.foregroundImg.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;"; + session.foregroundImg.onclick = function (event) { + changeEffectsImage(event, this); + }; + ele.parentNode.parentNode.insertBefore(session.foregroundImg, ele.parentNode); + session.foregroundImg.onload = () => { + URL.revokeObjectURL(session.foregroundImg.src); // no longer needed, free memory + }; + session.foregroundImg.src = URL.createObjectURL(ele.files[0]); // set src to blob url + session.foregroundImg.classList.add("selectedContentEffectsImage"); + } else if (ele.tagName.toLowerCase() == "img") { + if (session.foregroundImg) { + session.foregroundImg.classList.remove("selectedContentEffectsImage"); + } + session.foregroundImg = ele; + session.foregroundImg.classList.add("selectedContentEffectsImage"); + } +} +function loadOverlayImages() { + if (session.defaultForegroundImages) { + try { + session.defaultForegroundImages.reverse(); + } catch (e) { + errorlog("Could not process image list"); + session.defaultForegroundImages = false; + session.selectImageOverlay_contents = getById("selectImageOverlay_contents"); + return; + } + session.defaultForegroundImages.forEach(imgSrc => { + try { + var img = document.createElement("img"); + img.onerror = function () { + this.style.display = "none"; + }; // hide images that fail to load + img.crossOrigin = "Anonymous"; + img.src = imgSrc; + img.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;"; + img.onclick = function (event) { + changeOverlayImage(event, this); + }; + getById("selectImageOverlay_contents").prepend(img); + } catch (e) { } + }); + session.defaultForegroundImages = false; + session.selectImageOverlay_contents = getById("selectImageOverlay_contents"); + } else if (!session.selectImageOverlay_contents) { + session.selectImageOverlay_contents = getById("selectImageOverlay_contents"); + } + if (document.getElementById("selectImageOverlay")) { + document.getElementById("selectImageOverlay").style.display = "block"; + document.getElementById("selectImageOverlay").appendChild(session.selectImageOverlay_contents); + session.selectImageOverlay_contents.classList.remove("hidden"); + } else if (document.getElementById("selectImageOverlay3")) { + document.getElementById("selectImageOverlay3").style.display = "block"; + document.getElementById("selectImageOverlay3").appendChild(session.selectImageOverlay_contents); + session.selectImageOverlay_contents.classList.remove("hidden"); + } +} + +var effectsLoaded = {}; +var JEELIZFACEFILTER = null; +async function loadEffect(effect) { + warnlog("effect:" + effect); + var filename = effect.replace(/\W/g, ""); + if (effectsLoaded[filename]) { + effectsLoaded[filename](); + return; + } else { + effectsLoaded[filename] = function () { }; + } + warnlog("Loading Effect: " + effect); + var script = document.createElement("script"); + script.onload = async function () { + log("LOADED EFFECT"); + effectsLoaded[filename] = await effectsEngine(filename); + log("effectsEngine();"); + if (gpgpuSupport == "Google SwiftShader") { + if (!session.cleanOutput) { + warnUser("Hardware acceleration isn't detected.

    Please enable it for better performance.

    Settings -> Advanced -> System -> Use hardware-accleration", false, false); + } + } + effectsLoaded[filename](); + }; + script.src = "./filters/" + filename + ".js?" + parseInt(1000 * Math.random()); + document.head.appendChild(script); + warnUser("Loading custom effects model...", 1000); +} + +async function loadScript(url, callback = false) { + var res = null; + var rej = null; + var promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + + var check = document.querySelector("script[src='" + url + "']"); + if (check) { + if (callback) { + callback(); + } + } else { + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = url; + script.onload = function () { + res(); + if (callback) { + callback(); + } + }; + document.head.appendChild(script); + } + return await promise; +} + +var tokenClient = false; +function YoutubeChatInterface(remote = false) { + // this lets us query Youtube for chat messages, but its quota limited :( + if (!tokenClient) { + tokenClient = true; + } else { + return; + } + + var gisInited = false; + var gapiInited = false; + var busy = 0; + + function handleAuthClick() { + tokenClient.callback = async resp => { + if (resp.error) { + errorlog(resp.error); + } + closeModal(); + var auths = gapi.client.getToken(); + if (auths) { + setStorage("YoutubeAuth", JSON.stringify(auths), auths.expires_in || 3600); + } + listBroadcasts(); + }; + var saved = getStorage("YoutubeAuth"); + + if (saved) { + gapi.client.setToken(JSON.parse(saved)); + listBroadcasts(); + } else if (gapi.client.getToken() === null) { + if (remote) { + tokenClient.requestAccessToken({ prompt: "consent" }); + } else { + warnUser("", false, false); + } + } else { + if (remote) { + tokenClient.requestAccessToken({ prompt: "" }); + } else { + warnUser("", false, false); + } + } + } + + function maybeEnableButtons() { + if (gapiInited && gisInited) { + handleAuthClick(); + } + } + + async function initializeGapiClient() { + await gapi.client.init({ + apiKey: session.youtubeKey.split(",")[1], + discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest"] + }); + gapiInited = true; + maybeEnableButtons(); + } + + function handleSignoutClick() { + let token = gapi.client.getToken(); + if (token !== null) { + google.accounts.oauth2.revoke(token.access_token); + gapi.client.setToken(""); + } + } + + async function listBroadcasts() { + try { + var response = await gapi.client.youtube.liveBroadcasts.list({ + broadcastStatus: "active" + }); + } catch (err) { + errorlog(err); + return; + } + + let broadcasts = response.result.items; + if (!broadcasts || broadcasts.length == 0) { + return; + } + broadcasts.forEach(broadcast => { + setTimeout( + function (liveChatId) { + listMessages(liveChatId); + busy += 1; + }, + 1000, + broadcast.snippet.liveChatId + ); + }); + } + + async function listMessages(liveChatId, pageToken = false) { + try { + if (pageToken) { + var response = await gapi.client.youtube.liveChatMessages.list({ + liveChatId: liveChatId, + part: ["id", "snippet", "authorDetails"], + pageToken: pageToken + }); + } else { + var response = await gapi.client.youtube.liveChatMessages.list({ + liveChatId: liveChatId, + part: ["id", "snippet", "authorDetails"] + }); + } + + var messages = response.result.items; + messages.forEach(msg => { + pokeIframeAPI("YoutubeChat", msg); + }); + + var polling = response.result.pollingIntervalMillis; + var pageToken = response.result.nextPageToken; + + if (busy > 1) { + // popular eh? Lets quickly check for more. + } else if (busy > 0) { + // a message ! hurrah + if (polling < 2000) { + polling = 2000; + } // Was it just luck? + } else if (polling < 5000) { + polling = 5000; // let's not spam the api, cause we know there isn't anything waiting.. + } + busy = 0; // reset + setTimeout( + function (liveChatId, pageToken) { + listMessages(liveChatId, pageToken); + }, + polling, + liveChatId, + pageToken + ); + } catch (err) { + return; + } + } + + function gisLoaded() { + tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: session.youtubeKey.split(",")[0], + scope: "https://www.googleapis.com/auth/youtube", + callback: "" + }); + gisInited = true; + maybeEnableButtons(); + } + function gapiLoaded() { + gapi.load("client", initializeGapiClient); + } + + loadScript("https://apis.google.com/js/api.js", gapiLoaded); + loadScript("https://accounts.google.com/gsi/client", gisLoaded); +} + +function loadTensorflowJS() { + if (session.TFJSModel != null) { + return; + } + log("loadTensorflowJS()"); + session.TFJSModel = true; + var script = document.createElement("script"); + var script2 = document.createElement("script"); + var script3 = document.createElement("script"); + var script4 = document.createElement("script"); + script.onload = function () { + document.head.appendChild(script2); + }; + script2.onload = function () { + document.head.appendChild(script3); + }; + script3.onload = function () { + document.head.appendChild(script4); + }; + script4.onload = function () { + async function loadModel() { + session.TFJSModel = await faceLandmarksDetection.load(faceLandmarksDetection.SupportedPackages.mediapipeFacemesh); + closeModal(); + warnUser("Almost done loading model...", 3000); + } + loadModel(); + }; + script.src = "./thirdparty/tfjs/tf-core.js"; + script2.src = "./thirdparty/tfjs/tf-converter.js"; + script3.src = "./thirdparty/tfjs/tf-backend-webgl.js"; + script4.src = "./thirdparty/tfjs/face-landmarks-detection.js"; + warnUser("Downloading a big effects model... may take a minute", 15000); + + script.type = "text/javascript"; + script2.type = "text/javascript"; + script3.type = "text/javascript"; + script4.type = "text/javascript"; + document.head.appendChild(script); +} + +var TFLITELOADING = false; +function attemptTFLiteJsFileLoad() { + if (session.tfliteModule !== false) { + return true; + } + warnUser("Loading effects model..."); + TFLITELOADING = true; + session.tfliteModule = {}; + + if (!document.getElementById("tflitesimdjs")) { + var tmpScript = document.createElement("script"); + tmpScript.onload = loadTFLiteModel; + tmpScript.type = "text/javascript"; + tmpScript.src = "./thirdparty/tflite/tflite-simd.js?ver=2"; + tmpScript.id = "tflitesimdjs"; + document.head.appendChild(tmpScript); + } + + return false; +} +async function changeEffectsImage(ev, ele) { + if (ele.files && ele.files[0]) { + if (session.effectsImage) { + session.effectsImage.classList.remove("selectedContentEffectsImage"); + } + session.effectsImage = document.createElement("img"); + session.effectsImage.style = "max-width:130px;max-height:73.5px;display:inline-block;margin:10px;cursor:pointer;"; + session.effectsImage.onclick = function (event) { + changeEffectsImage(event, this); + }; + ele.parentNode.parentNode.insertBefore(session.effectsImage, ele.parentNode); + session.effectsImage.onload = () => { + URL.revokeObjectURL(session.effectsImage.src); // no longer needed, free memory + }; + session.effectsImage.src = URL.createObjectURL(ele.files[0]); // set src to blob url + session.effectsImage.classList.add("selectedContentEffectsImage"); + } else if (ele.tagName.toLowerCase() == "img") { + if (session.effectsImage) { + session.effectsImage.classList.remove("selectedContentEffectsImage"); + } + session.effectsImage = ele; + session.effectsImage.classList.add("selectedContentEffectsImage"); + } +} + +async function changeEffectAmount(ev, ele) { + session.effectValue = ele.value; + if (ele.id === "selectEffectAmountInput") { + getById("selectEffectAmountInput3").value = ele.value; + } + log("session.effectValue: " + session.effectValue); +} +async function loadTFLiteModel() { + try { + if (session.tfliteModule && session.effectsImage) { + var img = session.effectsImage; + session.tfliteModule = await createTFLiteSIMDModule(); + session.effectsImage = img; + } else { + session.tfliteModule = {}; + session.tfliteModule = await createTFLiteSIMDModule(); + } + if (!session.tfliteModule.simd) { + var elements = document.querySelectorAll("[data-warnSimdNotice]"); + for (let i = 0; i < elements.length; i++) { + elements[i].style.display = "inline-block"; + } + } + } catch (e) { + warnlog("TF-LITE FAILED TO LOAD"); + closeModal(); + return; + } + const modelResponse = await fetch("./thirdparty/tflite/segm_full_v679.tflite"); + session.tfliteModule.model = await modelResponse.arrayBuffer(); + + session.tfliteModule.HEAPU8.set(new Uint8Array(session.tfliteModule.model), session.tfliteModule._getModelBufferMemoryOffset()); + session.tfliteModule._loadModel(session.tfliteModule.model.byteLength); + session.tfliteModule.activelyProcessing = false; + TFLITELOADING = false; + closeModal(); + if (LaunchTFWorkerCallback) { + TFLiteWorker(); + } +} +function smdInfo() { + warnUser("For improved performance, use Chrome v87 or newer with SIMD support enabled.
    Enable SIMD here: chrome://flags/#enable-webassembly-simd", false, false); +} + +async function startPublishing() { + if (query("#publishOutURL input[type='text']").dataset.twitch == "true") { + session.whipOutput = "https://g.webrtc.live-video.net:4443/v2/offer"; + } else { + session.whipOutput = query("#publishOutURL input[type='text']").value || session.whipOutput || null; + } + + if (!session.whipOutput) { + warnUser("Please first provided an output destination", 2500); + return; + } + + if (!session.whipOutputToken) { + session.whipOutputToken = query("#publishOutToken input[type='password']").value || false; + } + + if (!session.whipOutputToken && query("#publishOutURL input[type='text']").dataset.twitch == "true") { + warnUser("Please enter a Twitch stream token first", 2000); + return; + } + getById("publishSettings").classList.add("hidden"); + + if (!getById("whipoutvbrcbr").classList.contains("hidden")) { + if (getById("whipoutvbrcbr").value === "cbr") { + session.cbr = 1; + } else { + session.cbr = 0; + } + } + if (!getById("whipoutdenoise").classList.contains("hidden")) { + if (getById("whipoutdenoise").value === "1") { + session.noiseSuppression = true; + } else { + session.noiseSuppression = false; + } + } + if (!getById("whipoutisolation").classList.contains("hidden")) { + if (getById("whipoutisolation").value === "1") { + session.voiceIsolation = true; + } else { + session.voiceIsolation = false; + } + } + + if (!getById("whipoutautogain").classList.contains("hidden")) { + if (getById("whipoutautogain").value === "1") { + session.autoGainControl = true; + } else { + session.autoGainControl = false; + } + } + if (!getById("whipoutstereo").classList.contains("hidden")) { + if (getById("whipoutstereo").value === "1") { + session.stereo = 1; + } else { + session.stereo = 0; + } + } + if (!getById("whipoutbitrateGroupFlag").classList.contains("hidden")) { + session.whipOutVideoBitrate = parseInt(getById("whipoutbitrateGroupFlag").value); + } + if (!getById("whipoutaudiobitrate").classList.contains("hidden")) { + session.whipOutAudioBitrate = parseInt(getById("whipoutaudiobitrate").value); + } + + var ret = await publishScreen(); + if (ret) { + getById("publishSettings").classList.add("hidden"); + resizeWindow(1280, 720); + document.title = "PUBLISHING🔴" + document.title; + } else { + getById("publishSettings").classList.remove("hidden"); + } + return ret; +} + +async function startRecording() { + session.recordLocal = session.recordLocal || 6000; + + var ret = await publishScreen(); + if (ret) { + getById("publishSettings").classList.add("hidden"); + resizeWindow(1280, 720); + document.title = "RECORDING🔴" + document.title; + recordLocalVideoToggle(); + } else { + getById("publishSettings").classList.remove("hidden"); + } +} + +function twitchSelect(ele) { + if (ele.checked) { + //query("#publishOutURL input[type='text']").value = + query("#publishOutURL input[type='text']").disabled = true; + query("#publishOutURL input[type='text']").classList.add("disable"); + query("#publishOutURL input[type='text']").dataset.twitch = "true"; + + query("#publishOutToken input[type='password']").placeholder = "Twitch stream token here"; + } else { + query("#publishOutURL input[type='text']").disabled = null; + query("#publishOutURL input[type='text']").classList.remove("disable"); + delete getById("publishOutURL").disabled; + query("#publishOutURL input[type='text']").dataset.twitch = "false"; + query("#publishOutToken input[type='password']").placeholder = "WHIP auth token here"; + } +} + +function resizeWindow(width, height) { + if (window.outerWidth) { + window.resizeTo(width + (window.outerWidth - window.innerWidth), height + (window.outerHeight - window.innerHeight)); + } else { + window.resizeTo(500, 500); + window.resizeTo(width + (500 - document.body.offsetWidth), height + (500 - document.body.offsetHeight)); + } + + setInterval(function () { + if (window.innerWidth / window.innerHeight > 17 / 9 && window.innerWidth / window.innerHeight < 15 / 9) { + return; + } + if (window.outerWidth) { + window.resizeTo(width + (window.outerWidth - window.innerWidth), height + (window.outerHeight - window.innerHeight)); + } else { + window.resizeTo(500, 500); + window.resizeTo(width + (500 - document.body.offsetWidth), height + (500 - document.body.offsetHeight)); + } + }, 5000); +} + +// compress SDP +/* +function compressSDP(sdp) { + // Extract critical values + const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1]; + const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1]; + + // Extract fingerprint (removing colons) + const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/); + const fingerprint = fingerprintMatch[1].replace(/:/g, ''); + + // Extract ICE candidates if they exist + const candidates = []; + const candidateRegex = /a=candidate:([^\r\n]+)/g; + let candidateMatch; + while ((candidateMatch = candidateRegex.exec(sdp)) !== null) { + const parts = candidateMatch[1].split(' '); + + // Skip IPv6 candidates + if (parts[4].includes(':')) continue; + + // Only keep foundation, component, protocol, priority, ip, port, type and related values + const foundation = parts[0]; + const component = parts[1]; + const protocol = parts[2].toLowerCase() === 'udp' ? 'u' : 't'; + const priority = parseInt(parts[3]); + const ip = parts[4]; + const port = parseInt(parts[5]); + const type = parts[7]; + + // Encode type: host=h, srflx=s, relay=r + let typeCode; + switch(type) { + case 'host': typeCode = 'h'; break; + case 'srflx': typeCode = 's'; break; + case 'relay': typeCode = 'r'; break; + default: typeCode = 'x'; + } + + // Compact representation of candidate + // Format: foundation,component,protocol,priority(shortened),ip,port,type + candidates.push( + `${foundation},${component},${protocol},${priorityToCompact(priority)},${ip},${port},${typeCode}` + ); + } + + // Convert fingerprint from hex to a more compact representation + const compactFingerprint = hexToCompact(fingerprint); + + // Build the compressed string + // Format: C(version)|ufrag|pwd|fingerprint|[candidates] + let result = `C1|${iceUfrag}|${icePwd}|${compactFingerprint}`; + + // Add candidates if they exist + if (candidates.length > 0) { + result += `|${candidates.join('/')}`; + } + + return result; +} +function playCompressedSDP(sdp){ + sdp = decompressSDP(sdp) + let msg = {}; + msg.description = {sdp, type:"offer"}; + msg.UUID = session.generateStreamID(15); + session.processDescription2(msg); +} + +function decompressSDP(compressed) { + // Parse compressed string + // Format: C(version)|ufrag|pwd|fingerprint|[candidates] + const parts = compressed.split('|'); + + // Version check + if (parts[0] !== 'C1') { + throw new Error('Unsupported compression version'); + } + + const iceUfrag = parts[1]; + const icePwd = parts[2]; + const fingerprint = compactToHex(parts[3]); + + // Format fingerprint with colons + const formattedFingerprint = fingerprint.match(/.{2}/g).join(':'); + + // Build the base SDP + let sdp = [ + 'v=0', + 'o=- 1 1 IN IP4 127.0.0.1', + 's=-', + 't=0 0', + 'a=group:BUNDLE 0', + 'a=extmap-allow-mixed', + 'a=msid-semantic: WMS', + 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel', + 'c=IN IP4 0.0.0.0', + `a=ice-ufrag:${iceUfrag}`, + `a=ice-pwd:${icePwd}`, + 'a=ice-options:trickle', + `a=fingerprint:sha-256 ${formattedFingerprint}`, + 'a=setup:actpass', + 'a=mid:0', + 'a=sctp-port:5000', + 'a=max-message-size:262144' + ].join('\r\n'); + + // Add candidates if they exist + if (parts.length > 4 && parts[4]) { + const candidates = parts[4].split('/'); + + for (const candidate of candidates) { + const candParts = candidate.split(','); + + // Parse the candidate parts + const foundation = candParts[0]; + const component = candParts[1]; + const protocol = candParts[2] === 'u' ? 'UDP' : 'TCP'; + const priority = compactToPriority(candParts[3]); + const ip = candParts[4]; + const port = candParts[5]; + + // Decode type + let type; + switch(candParts[6]) { + case 'h': type = 'host'; break; + case 's': type = 'srflx'; break; + case 'r': type = 'relay'; break; + default: type = 'unknown'; + } + + // Build the candidate line + sdp += `\r\na=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`; + } + } + + return sdp; +} + +function priorityToCompact(priority) { + // Simplified priority encoding - actual implementation would use a more sophisticated approach + // For typical values, this could be a mapping to shorter codes + return priority.toString(36); +} + +function compactToPriority(compact) { + return parseInt(compact, 36); +} + +function hexToCompact(hex) { + // Convert hex pairs to numbers, then to a more compressed alphabet + // This uses a custom encoding that maps to URL-safe characters + const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"; + let result = ''; + + // Process 3 bytes (6 hex chars) at a time to produce 4 output chars + for (let i = 0; i < hex.length; i += 6) { + const chunk = hex.substr(i, 6); + if (chunk.length < 6) { + // Handle the last partial chunk + const value = parseInt(chunk, 16); + result += chars[value % 64]; + if (chunk.length > 2) { + result += chars[Math.floor(value / 64) % 64]; + } + } else { + // Process full 6-hex-char chunk + const value = parseInt(chunk, 16); + result += chars[value & 0x3F]; + result += chars[(value >> 6) & 0x3F]; + result += chars[(value >> 12) & 0x3F]; + result += chars[(value >> 18) & 0x3F]; + } + } + + return result; +} + +function compactToHex(compact) { + const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"; + let result = ''; + + // Process 4 input chars at a time to produce 6 hex chars + for (let i = 0; i < compact.length; i += 4) { + let value = 0; + + for (let j = 0; j < 4 && i + j < compact.length; j++) { + const char = compact[i + j]; + const charValue = chars.indexOf(char); + if (charValue === -1) { + throw new Error(`Invalid character in compact string: ${char}`); + } + value |= charValue << (j * 6); + } + + // Convert to hex + const hex = value.toString(16).padStart(6, '0'); + result += hex; + } + + // Ensure result is the right length for a SHA-256 hash (64 hex chars) + return result.padEnd(64, '0'); +} + +function compressSDPForQR(sdp) { + // Extract critical values + const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1]; + const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1]; + + // Extract fingerprint (removing colons) + const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/); + const fingerprint = fingerprintMatch[1].replace(/:/g, ''); + + // Extract ICE candidates if they exist + const candidates = []; + const candidateRegex = /a=candidate:([^\r\n]+)/g; + let candidateMatch; + while ((candidateMatch = candidateRegex.exec(sdp)) !== null) { + const parts = candidateMatch[1].split(' '); + + // Skip IPv6 candidates + if (parts[4].includes(':')) continue; + + // Compact candidate representation + const foundation = parts[0]; + const component = parts[1]; + const protocol = parts[2].toLowerCase() === 'udp' ? '1' : '2'; + const priority = parseInt(parts[3]); + const ip = compressIP(parts[4]); + const port = parseInt(parts[5]); + + // Encode type: host=1, srflx=2, relay=3 + let typeCode; + switch(parts[7]) { + case 'host': typeCode = '1'; break; + case 'srflx': typeCode = '2'; break; + case 'relay': typeCode = '3'; break; + default: typeCode = '0'; + } + + // Ultra-compact representation, optimized for QR + candidates.push(`${foundation}${component}${protocol}${compressNumber(priority)}${ip}${port}${typeCode}`); + } + + // Compact encoding of fingerprint, optimized for QR code alphanumeric mode + const qrFingerprint = fingerprintToQR(fingerprint); + + // Use very compact delimiter (single character) + let result = `Q${iceUfrag}~${icePwd}~${qrFingerprint}`; + + // Add candidates with minimal separator + if (candidates.length > 0) { + result += `~${candidates.join(',')}`; + } + + return result; +} + +function decompressSDPFromQR(compressed) { + // Parse compressed string + // Format: Q[iceUfrag]~[icePwd]~[fingerprint]~[candidates] + if (!compressed.startsWith('Q')) { + throw new Error('Unsupported QR compression format'); + } + + const parts = compressed.substring(1).split('~'); + const iceUfrag = parts[0]; + const icePwd = parts[1]; + const fingerprint = qrToFingerprint(parts[2]); + + // Format fingerprint with colons + const formattedFingerprint = fingerprint.match(/.{2}/g).join(':'); + + // Build the base SDP + let sdp = [ + 'v=0', + 'o=- 1 1 IN IP4 127.0.0.1', + 's=-', + 't=0 0', + 'a=group:BUNDLE 0', + 'a=extmap-allow-mixed', + 'a=msid-semantic: WMS', + 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel', + 'c=IN IP4 0.0.0.0', + `a=ice-ufrag:${iceUfrag}`, + `a=ice-pwd:${icePwd}`, + 'a=ice-options:trickle', + `a=fingerprint:sha-256 ${formattedFingerprint}`, + 'a=setup:actpass', + 'a=mid:0', + 'a=sctp-port:5000', + 'a=max-message-size:262144' + ].join('\r\n'); + + // Add candidates if they exist + if (parts.length > 3 && parts[3]) { + const candidates = parts[3].split(','); + + for (const candidate of candidates) { + // Extract the packed candidate information + let i = 0; + + // Extract foundation (variable length, numeric) + let j = i; + while (j < candidate.length && /^\d$/.test(candidate[j])) j++; + const foundation = candidate.substring(i, j); + i = j; + + // Extract component (1 digit) + const component = candidate.substring(i, i + 1); + i += 1; + + // Extract protocol (1 digit code) + const protocolCode = candidate.substring(i, i + 1); + const protocol = protocolCode === '1' ? 'UDP' : 'TCP'; + i += 1; + + // Extract priority (variable length, encoded) + j = i; + while (j < candidate.length && /^[0-9A-Z]$/.test(candidate[j])) j++; + const priorityEncoded = candidate.substring(i, j); + const priority = decompressNumber(priorityEncoded); + i = j; + + // Extract IP (encoded) + j = i; + while (j < candidate.length && !/^\d$/.test(candidate[j])) j++; + const ipEncoded = candidate.substring(i, j); + const ip = decompressIP(ipEncoded); + i = j; + + // Extract port (variable length, numeric) + j = i; + while (j < candidate.length && /^\d$/.test(candidate[j])) j++; + const port = candidate.substring(i, j); + i = j; + + // Extract type (1 digit code) + const typeCode = candidate.substring(i, i + 1); + let type; + switch(typeCode) { + case '1': type = 'host'; break; + case '2': type = 'srflx'; break; + case '3': type = 'relay'; break; + default: type = 'unknown'; + } + + // Build the candidate line + sdp += `\r\na=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`; + } + } + + return sdp; +} + +function compressIP(ip) { + // For common local IPs, use a single character code + if (ip === '127.0.0.1') return 'L'; + if (ip === '0.0.0.0') return 'Z'; + if (ip.startsWith('192.168.')) return 'P' + ip.split('.').slice(2).join(''); + if (ip.startsWith('10.')) return 'T' + ip.split('.').slice(1).join(''); + if (ip.startsWith('172.')) return 'S' + ip.split('.').slice(1).join(''); + + // For other IPs, convert to a base36 representation + const parts = ip.split('.'); + let num = 0; + for (let i = 0; i < 4; i++) { + num = num * 256 + parseInt(parts[i]); + } + return num.toString(36).toUpperCase(); +} + +function decompressIP(compressed) { + if (compressed === 'L') return '127.0.0.1'; + if (compressed === 'Z') return '0.0.0.0'; + if (compressed.startsWith('P')) { + const suffix = compressed.substring(1); + if (suffix.length === 0) return '192.168.0.0'; + if (suffix.length === 1) return `192.168.${suffix}.0`; + return `192.168.${suffix.substring(0, 1)}.${suffix.substring(1)}`; + } + if (compressed.startsWith('T')) { + const suffix = compressed.substring(1); + if (suffix.length === 0) return '10.0.0.0'; + if (suffix.length === 1) return `10.${suffix}.0.0`; + if (suffix.length === 2) return `10.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`; + return `10.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`; + } + if (compressed.startsWith('S')) { + const suffix = compressed.substring(1); + if (suffix.length === 0) return '172.0.0.0'; + if (suffix.length === 1) return `172.${suffix}.0.0`; + if (suffix.length === 2) return `172.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`; + return `172.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`; + } + + // Default case: convert from base36 + const num = parseInt(compressed, 36); + return [ + (num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + num & 0xFF + ].join('.'); +} + +function compressNumber(num) { + // Convert to base36 for compactness, prefer uppercase for QR + return num.toString(36).toUpperCase(); +} + +function decompressNumber(compressed) { + return parseInt(compressed, 36); +} + +function fingerprintToQR(hex) { + // Convert 64-char hex to a more compact representation + // Using base36 for better QR code efficiency (alphanumeric mode) + let result = ''; + + // Process 4 hex chars (16 bits) at a time to produce 3 base36 chars + for (let i = 0; i < hex.length; i += 4) { + const chunk = hex.substr(i, 4); + if (chunk.length < 4) { + // Handle the last partial chunk + const value = parseInt(chunk, 16); + result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3); + } else { + // Process full chunk + const value = parseInt(chunk, 16); + result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3); + } + } + + return result; +} + +function qrToFingerprint(qrFp) { + let result = ''; + + // Process 3 base36 chars at a time to produce 4 hex chars + for (let i = 0; i < qrFp.length; i += 3) { + const chunk = qrFp.substr(i, 3); + const value = parseInt(chunk, 36); + result += value.toString(16).padStart(4, '0').substring(0, 4); + } + + // Ensure result has correct length for SHA-256 (64 hex chars) + return result.padEnd(64, '0'); +} + +function sdpToQRCode(sdp, element) { + const compressed = compressSDPForQR(sdp); + + // Use the provided qrcodejs library + new QRCode(element, { + text: compressed, + width: 128, + height: 128, + colorDark: "#000000", + colorLight: "#ffffff", + correctLevel: QRCode.CorrectLevel.M // Use medium error correction + }); + + return compressed; // Return compressed string for reference +} + +function qrToSDP(qrData) { + return decompressSDPFromQR(qrData); +} + */ +/// + +function configureWhipOutSDP(description) { + // THIS IS FOR WHIP-OUTPUT; it has + + var configs = {}; + + if (SafariVersion && SafariVersion <= 13 && (iOS || iPad)) { + return description; // skip. Not going to try to tinker with older iOS SDPs + } else if (session.stereo == 3 || session.stereo == 5 || session.stereo == 6 || session.stereo == 1) { + // stereo out + configs = { stereo: 1 }; + log("stereo enabled"); + } else if (iOS || iPad) { + // iOS doesn't have multichannel, so why even bother + configs = {}; + } else if (session.stereo == 4) { + configs = { stereo: 2 }; + log("stereo enabled"); + } else { + configs = { stereo: 0 }; + } + + if (session.whipOutAudioCodec === "pcm") { + if (session.audioInputChannels && session.audioInputChannels == 1) { + description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, false); // mono + } else if (session.stereo) { + description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, true); // mono + } else { + description.sdp = CodecsHandler.modifyDescPCM(description.sdp, session.micSampleRate || 48000, false); + } + } else { + if (session.whipOutAudioCodec) { + description.sdp = CodecsHandler.preferAudioCodec(description.sdp, session.whipOutAudioCodec, session.predAudio, session.pfecAudio); // "red" codec + } + + if (session.noFEC !== null) { + configs.useinbandfec = session.noFEC ? 0 : 1; + } + if (session.maxptime !== false) { + configs.maxptime = session.maxptime; + } + if (session.minptime !== false) { + configs.minptime = session.minptime; + } + if (session.ptime !== false) { + configs.ptime = session.ptime; + } + if (session.dtx !== false) { + configs.dtx = session.dtx; // "usedtx", if no loud audio, stops sending audio for 400ms. default. + } + + if (session.whipOutAudioBitrate) { + configs.maxaveragebitrate = session.whipOutAudioBitrate * 1024; + configs.cbr = session.cbr; + } + + if (Object.keys(configs).length) { + log("Processing sdp of type: " + description.type + " ..."); + log(configs); + description.sdp = CodecsHandler.setOpusAttributes(description.sdp, configs, true); + } + } + + if (iOS || iPad) { + // solves issues with iOS rotation not being correct + if (session.removeOrientationFlag && description.sdp.includes("a=extmap:3 urn:3gpp:video-orientation\r\n")) { + description.sdp = description.sdp.replace("a=extmap:3 urn:3gpp:video-orientation\r\n", ""); + } + } + + if (session.screenShareState && typeof session.whipOutScreenShareCodec === "object") { + session.whipOutScreenShareCodec.reverse().forEach(codec => { + description.sdp = CodecsHandler.preferCodec(description.sdp, codec, session.videoErrorCorrection); + + if (session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) { + description.sdp = CodecsHandler.setVideoBitrates( + description.sdp, + { + min: parseInt((session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) / 10) || 1, + max: session.whipOutScreenShareBitrate || session.whipOutVideoBitrate || 1 + }, + codec + ); + } + }); + } else if (session.screenShareState && session.whipOutScreenShareCodec) { + description.sdp = CodecsHandler.preferCodec(description.sdp, session.whipOutScreenShareCodec, session.videoErrorCorrection); + + if (session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) { + description.sdp = CodecsHandler.setVideoBitrates( + description.sdp, + { + min: parseInt((session.whipOutScreenShareBitrate || session.whipOutVideoBitrate) / 10) || 1, + max: session.whipOutScreenShareBitrate || session.whipOutVideoBitrate || 1 + }, + session.whipOutScreenShareCodec + ); + } + } else if (typeof session.whipOutCodec === "object") { + session.whipOutCodec.reverse().forEach(codec => { + description.sdp = CodecsHandler.preferCodec(description.sdp, codec, session.videoErrorCorrection); + + if (session.whipOutVideoBitrate) { + description.sdp = CodecsHandler.setVideoBitrates( + description.sdp, + { + min: parseInt(session.whipOutVideoBitrate / 10) || 1, + max: session.whipOutVideoBitrate || 1 + }, + codec + ); + } + }); + } else if (session.whipOutCodec) { + description.sdp = CodecsHandler.preferCodec(description.sdp, session.whipOutCodec, session.videoErrorCorrection); + if (session.whipOutVideoBitrate) { + description.sdp = CodecsHandler.setVideoBitrates( + description.sdp, + { + min: parseInt(session.whipOutVideoBitrate / 10) || 1, + max: session.whipOutVideoBitrate || 1 + }, + session.whipOutCodec + ); + } + } else { + log("preferring h264 in sdp"); + description.sdp = CodecsHandler.preferCodec(description.sdp, "h264", session.videoErrorCorrection); // h264 default. openh264? well, this was breaking with Pion, so, meh. whatever h264 + if (session.whipOutVideoBitrate) { + description.sdp = CodecsHandler.setVideoBitrates( + description.sdp, + { + min: parseInt(session.whipOutVideoBitrate / 10) || 1, + max: session.whipOutVideoBitrate || 1 + }, + "h264" + ); + } + } + + var bitrate = 2500; + if (session.whipOutVideoBitrate !== false) { + bitrate = session.whipOutVideoBitrate; + } + if (session.screenShareState && session.whipOutScreenShareBitrate !== false) { + bitrate = session.whipOutScreenShareBitrate; + } + + session.whipOut.savedBitrate = bitrate; // actual target + session.whipOut.setBitrate = bitrate; // max + + return description; +} +const scalabilityModes = ["L1T1", "L1T2", "L1T3", "L2T1", "L2T2", "L2T3", "L3T1", "L3T2", "L3T3", "L2T1h", "L2T2h", "L2T3h", "S2T1", "S2T2", "S2T3", "S2T1h", "S2T2h", "S2T3h", "S3T1", "S3T2", "S3T3", "S3T1h", "S3T2h", "S3T3h", "L2T2_KEY", "L2T3_KEY", "L3T2_KEY", "L3T3_KEY"]; + +function completeLocationURL(originalURL, locationURL) { + if (!originalURL) { + return locationURL; + } else if (!locationURL) { + return originalURL; + } + const parsedOriginalURL = new URL(originalURL); + + // Check if the location URL is already absolute + if (locationURL.startsWith("http://") || locationURL.startsWith("https://")) { + return locationURL; + } else { + // Check if the original URL's pathname ends with a slash or not + let basePath = parsedOriginalURL.origin + parsedOriginalURL.pathname; + if (!basePath.endsWith("/")) { + // If the pathname does not end with a slash, remove the last segment + basePath = basePath.substring(0, basePath.lastIndexOf("/") + 1); + } + + // Resolve "." and ".." in the relative URL against the base path + const fullPath = new URL(locationURL, basePath).href; + + return fullPath; + } +} + +function getWhipOutCanvasTrack(baseRTC = session.whipOut) { + if (!baseRTC) { + errorlog("Meshcast/WHIP not connected; cant' create canvas for it"); + } + if (!baseRTC.canvas) { + baseRTC.canvas = document.createElement("canvas"); + baseRTC.canvas.width = 320; + baseRTC.canvas.height = 180; + } + if (!baseRTC.ctx) { + baseRTC.ctx = baseRTC.canvas.getContext("2d", { alpha: false }); + baseRTC.ctx.fillStyle = "#000"; + baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height); + } + + if (!baseRTC.canvasStream) { + (function loop() { + baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height); + setTimeout(loop, 250); // drawing at 30fps + })(); + try { + baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height); + baseRTC.canvasStream = baseRTC.canvas.captureStream(4); + + baseRTC.ctx.fillRect(0, 0, baseRTC.canvas.width, baseRTC.canvas.height); + } catch (e) { + errorlog("Error creating whip output placeholder track"); + } + } + var tracks = baseRTC.canvasStream.getVideoTracks(); + if (tracks.length) { + return tracks[0] + } + errorlog("Meschast canvas not working"); + return false; +} + +function whipOut() { + log("whipOut"); + if (session.whipPublishPrimary === false) { + log("whipOut skipped: primary WHIP disabled"); + return false; + } + + if (!session.videoElement || !session.videoElement.srcObject) { + log("no videoElement yet created; can't do whip out until then"); + return false; + } + + for (const UUID in session.pcs) { + if (!session.pcs.hasOwnProperty(UUID)) { + continue; + } + if (session.pcs[UUID] && session.pcs[UUID].whipout === true) { + session.pcs[UUID].whipout = null; + } + } + + var candidates = []; + var codec = false; + var keyframe = false; + async function whipConnect() { + try { + if (!session.configuration) { + await chooseBestTURN(); + } + + if (session.encodedInsertableStreams) { + // most servers won't support this + session.configuration.encodedInsertableStreams = true; + } + + if (session.bundlePolicy) { + session.configuration.bundlePolicy = session.bundlePolicy; + } + var config = { ...session.configuration }; + log(config); + + // do anything whip specific here + + session.whipOut = new RTCPeerConnection(config); + session.whipOut.stats = {}; + session.whipOut.maxBandwidth = null; // based on max available bitrate + session.whipOut.scale = false; + session.whipOut.offerToReceiveAudio = false; + session.whipOut.offerToReceiveVideo = false; + session.whipOut.keyframeTimeout = null; + } catch (err) { + errorlog(err); + if (!session.cleanOutput) { + warnUser("An RTC error occured"); + } + } + + try { + /// audio tracks + + var tracks = false; + if (session.videoElement && session.videoElement.srcObject) { + tracks = session.videoElement.srcObject.getAudioTracks(); + } + var streamsource = false; + + if (!tracks || !tracks.length) { + if (!session.audioCtx) { + session.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + + // Create an oscillator at an audible frequency + const oscillator = session.audioCtx.createOscillator(); + oscillator.frequency.value = 440; // Standard A note + + // Create a gain to make it nearly silent + const gainNode = session.audioCtx.createGain(); + gainNode.gain.value = 0.00001; + + const destination = session.audioCtx.createMediaStreamDestination(); + + oscillator.connect(gainNode); + gainNode.connect(destination); + + oscillator.start(); + + streamsource = destination.stream; + destination.stream.getAudioTracks().forEach(trk => { + tracks = trk; + + trk.enabled = true; // Force the track to appear active + + const constraints = { // keep it quite and not denoised out back to zero + channelCount: { ideal: 1 }, + autoGainControl: { ideal: false }, + echoCancellation: { ideal: false }, + noiseSuppression: { ideal: false } + }; + + try { + trk.applyConstraints(constraints); + } catch (e) { + console.warn("Could not apply audio constraints", e); + } + }); + } else { + tracks = tracks[0]; + streamsource = session.videoElement.srcObject; + } + + if (session.audioContentHint && tracks.kind === "audio") { + try { + tracks.contentHint = session.audioContentHint; + } catch (e) { + errorlog(e); + } + } + + if (tracks) { + try { + session.whipOut.addTransceiver(tracks, { + streams: [streamsource], + direction: "sendonly" + }); + } catch (e) { + errorlog(e); + session.whipOut.addTrack(tracks); + } + } + } catch (e) { + errorlog(e); + } + + try { + //// video tracks /// + + var tracks = false; + if (session.videoElement && session.videoElement.srcObject) { + tracks = session.videoElement.srcObject.getVideoTracks(); + } + + if (!tracks || !tracks.length) { + var track = getWhipOutCanvasTrack(session.whipOut); + } else { + var track = tracks[0]; + } + + if (track) { // it's actually just "track" now. + if (session.screenShareState && session.screenshareContentHint && track.kind === "video") { + try { + track.contentHint = session.screenshareContentHint; + } catch (e) { + errorlog(e); + } + } else if (session.contentHint && track.kind === "video") { + try { + track.contentHint = session.contentHint; + } catch (e) { + errorlog(e); + } + } + + try { + var transceiverSetup = { + streams: [session.videoElement.srcObject], + direction: "sendonly" + }; + if (session.scalabilityMode) { + // might be a good time to validate the scalabilityMode at this point; check to see if requested codec is available,etc. + try { + transceiverSetup.sendEncodings = [{ scalabilityMode: session.scalabilityMode }]; + } catch (e) { + errorlog("Invalid scalability mode provided"); + } + if (session.whipOutCodec && session.whipOutCodec.length) { + //var svcCodecPref = []; + //for (var i = 0;i= 0) { + session.whipOut.location = this.getResponseHeader("location") || ""; + session.whipOut.location = completeLocationURL(session.whipOutput, session.whipOut.location); + } else if (!session.whipOut.location && session.whipOutput) { + session.whipOut.location = session.whipOutput; + session.whipOut.location = completeLocationURL(session.whipOutput, session.whipOut.location); + } + } catch (e) { + errorlog(e); + } + try { + log(this.getAllResponseHeaders()); if (this.getAllResponseHeaders().toLowerCase().indexOf("whep") >= 0) { WHELPlaybackURL = this.getResponseHeader("whep") || this.getResponseHeader("WHEP") || false; - } else { - console.log("Note: No WHEP key/value was found in the WHIP header response or it was not exposed.\n\nProviding the WHEP URL for this WHIP output via the WHEP header key will allow p2p access to the WHEP stream for others conneted to this peer."); - } - if (!WHELPlaybackURL && session.whipOutput) { - var targetDomain = session.whipOutput.split("/"); - try { - if (targetDomain.length > 2 && targetDomain[2].endsWith(".cloudflarestream.com") && targetDomain[3].length == 65) { + } else { + console.log("Note: No WHEP key/value was found in the WHIP header response or it was not exposed.\n\nProviding the WHEP URL for this WHIP output via the WHEP header key will allow p2p access to the WHEP stream for others conneted to this peer."); + } + if (!WHELPlaybackURL && session.whipOutput) { + var targetDomain = session.whipOutput.split("/"); + try { + if (targetDomain.length > 2 && targetDomain[2].endsWith(".cloudflarestream.com") && targetDomain[3].length == 65) { WHELPlaybackURL = "https://" + targetDomain[2] + "/" + targetDomain[3].slice(33, 65) + "/webRTC/play"; session.whipOut.stats.whipHost = "Cloudflare"; } else if (/^https?:\/\/(?:[\w-]+\.)*meshcast\.io(?:\/|$)/i.test(session.whipOutput)) { // this should be only if meshcast doens't return a whep URL. we guess as a fallback. @@ -55080,145 +55545,145 @@ function whipOut() { session.whipOut.stats.whipHost = "Meshcast2"; } - } catch (e) { - errorlog(e); - } - } else if (WHELPlaybackURL && !(WHELPlaybackURL.startsWith("http://") || WHELPlaybackURL.startsWith("https://"))) { - var targetDomain = session.whipOutput.split("/"); - if (targetDomain.length > 2) { - if (WHELPlaybackURL.startsWith("/")) { - WHELPlaybackURL = targetDomain[0] + "//" + targetDomain[2] + WHELPlaybackURL; - } else { - WHELPlaybackURL = targetDomain[0] + "//" + targetDomain[2] + "/" + WHELPlaybackURL; - } - } - } else if (WHELPlaybackURL && /^https?:\/\/(?:[\w-]+\.)*meshcast\.io(?:\/|$)/i.test(WHELPlaybackURL)) { // this should be only if meshcast doens't return a whep URL. we guess as a fallback. - try { - session.whipOut.stats.whipHost = "Meshcast"; - session.whipOut.stats.watch_URL = "https://meshcast.io/view.html?geo=" + WHELPlaybackURL.split("https://")[1].split(".")[0] + "&id=" + WHELPlaybackURL.split("meshcast.io/")[1].split("/whip")[0]; - } catch (e) { - errorlog(e); - } - } - log("WHELPlaybackURL: " + WHELPlaybackURL); - session.whipOut.stats.whep_URL = WHELPlaybackURL; - } catch (e) { - errorlog(e); - } - - if (WHELPlaybackURL) { - if (WHELPlaybackURL.includes("|")) { - var tmp = WHELPlaybackURL.split("|"); - session.whipoutSettings = { type: "whep", url: tmp[0].trim(), token: tmp[1].trim(), started: false }; - } else { - session.whipoutSettings = { type: "whep", url: WHELPlaybackURL, started: false }; - } - } - - if (contentType.startsWith("application/sdp") || isSDP(this.responseText)) { - var jsep = {}; - jsep.sdp = this.responseText; - jsep.type = "answer"; - try { - jsep = configureWhipOutSDP(jsep); - } catch (e) { - errorlog(e); - } - warnlog("Processing answer:"); - warnlog(jsep); - if (session.whipOut && session.whipOut.location) { - sessionStorage.setItem("deleteWhipOnLoad", JSON.stringify({ location: session.whipOut.location, whipOutputToken: session.whipOutputToken })); - session.whipOut.deleteme = function () { - let xhttp = new XMLHttpRequest(); - if (session.whipOutputToken) { - xhttp.setRequestHeader("Authorization", "Bearer " + session.whipOutputToken); - } - xhttp.onload = function () { - sessionStorage.removeItem("deleteWhipOnLoad"); - }; - xhttp.onerror = function () { - sessionStorage.removeItem("deleteWhipOnLoad"); - }; - xhttp.open("DELETE", session.whipOut.location, true); - xhttp.send(); - delete session.whipOut.deleteme; - }; - } - if (session.localNetworkOnly) { - jsep.sdp = filterSDPLAN(jsep.sdp); - } - if (session.stunOnly) { // or whatever flag you want to use - jsep.sdp = filterStunOnly(jsep.sdp); - } - session.whipOut - .setRemoteDescription(jsep) - .then(async function () { - warnlog("SHOULD BE CONNECTED?"); - //var content = ""; - //while (candidates.length) { - // var candidate = candidates.pop(); - // content += candidate.candidate; - //} - //warnlog("Content: " + content.length); - //if (content){ - // warnlog("SENDING TRICKLE"); //.. I should, but I'm not, since most sites don't support it still. - if (session.whipoutSettings) { - session.whipoutSettings.started = Date.now(); - } - if (session.whipOutKeyframe) { - clearTimeout(session.whipOut.keyframeTimeout); - session.whipOut.keyframeTimeout = setInterval(function () { - GOP(); - }, session.whipOutKeyframe); // ensure GOP no longer than 6s - } - session.whipOutSetScale(); - await sleep(1000); // give whip server a moment to setup I guess. - if (session.whipoutSettings) { - for (var UUID in session.pcs) { - if (session.pcs[UUID].whipout === null) { - var data = {}; - data.whepSettings = session.whipoutSettings; - if (session.sendMessage(data, UUID)) { - session.pcs[UUID].whipout = true; - } - } - } - } - //} - }) - .catch(async function (e) { - errorlog(e); - errorlog("Recieved an invalid SDP answer response from the WHIP endpoint. While things may still work, it won't work as intended."); - if (WHELPlaybackURL) { - log("Since we have the WHEP URL, we will assume its desired to forward it on, even if it may not work."); - if (session.whipoutSettings) { - session.whipoutSettings.started = Date.now(); - } - try { - if (session.whipOutKeyframe) { - clearTimeout(session.whipOut.keyframeTimeout); - session.whipOut.keyframeTimeout = setInterval(function () { - GOP(); - }, session.whipOutKeyframe); // ensure GOP no longer than 6s - } - session.whipOutSetScale(); - } catch (e) { - errorlog(e); - } - await sleep(1000); // give whip server a moment to setup I guess. - if (session.whipoutSettings) { - for (var UUID in session.pcs) { - if (session.pcs[UUID].whipout === null) { - var data = {}; - data.whepSettings = session.whipoutSettings; - if (session.sendMessage(data, UUID)) { - session.pcs[UUID].whipout = true; - } - } - } - } - } - }); + } catch (e) { + errorlog(e); + } + } else if (WHELPlaybackURL && !(WHELPlaybackURL.startsWith("http://") || WHELPlaybackURL.startsWith("https://"))) { + var targetDomain = session.whipOutput.split("/"); + if (targetDomain.length > 2) { + if (WHELPlaybackURL.startsWith("/")) { + WHELPlaybackURL = targetDomain[0] + "//" + targetDomain[2] + WHELPlaybackURL; + } else { + WHELPlaybackURL = targetDomain[0] + "//" + targetDomain[2] + "/" + WHELPlaybackURL; + } + } + } else if (WHELPlaybackURL && /^https?:\/\/(?:[\w-]+\.)*meshcast\.io(?:\/|$)/i.test(WHELPlaybackURL)) { // this should be only if meshcast doens't return a whep URL. we guess as a fallback. + try { + session.whipOut.stats.whipHost = "Meshcast"; + session.whipOut.stats.watch_URL = "https://meshcast.io/view.html?geo=" + WHELPlaybackURL.split("https://")[1].split(".")[0] + "&id=" + WHELPlaybackURL.split("meshcast.io/")[1].split("/whip")[0]; + } catch (e) { + errorlog(e); + } + } + log("WHELPlaybackURL: " + WHELPlaybackURL); + session.whipOut.stats.whep_URL = WHELPlaybackURL; + } catch (e) { + errorlog(e); + } + + if (WHELPlaybackURL) { + if (WHELPlaybackURL.includes("|")) { + var tmp = WHELPlaybackURL.split("|"); + session.whipoutSettings = { type: "whep", url: tmp[0].trim(), token: tmp[1].trim(), started: false }; + } else { + session.whipoutSettings = { type: "whep", url: WHELPlaybackURL, started: false }; + } + } + + if (contentType.startsWith("application/sdp") || isSDP(this.responseText)) { + var jsep = {}; + jsep.sdp = this.responseText; + jsep.type = "answer"; + try { + jsep = configureWhipOutSDP(jsep); + } catch (e) { + errorlog(e); + } + warnlog("Processing answer:"); + warnlog(jsep); + if (session.whipOut && session.whipOut.location) { + sessionStorage.setItem("deleteWhipOnLoad", JSON.stringify({ location: session.whipOut.location, whipOutputToken: session.whipOutputToken })); + session.whipOut.deleteme = function () { + let xhttp = new XMLHttpRequest(); + if (session.whipOutputToken) { + xhttp.setRequestHeader("Authorization", "Bearer " + session.whipOutputToken); + } + xhttp.onload = function () { + sessionStorage.removeItem("deleteWhipOnLoad"); + }; + xhttp.onerror = function () { + sessionStorage.removeItem("deleteWhipOnLoad"); + }; + xhttp.open("DELETE", session.whipOut.location, true); + xhttp.send(); + delete session.whipOut.deleteme; + }; + } + if (session.localNetworkOnly) { + jsep.sdp = filterSDPLAN(jsep.sdp); + } + if (session.stunOnly) { // or whatever flag you want to use + jsep.sdp = filterStunOnly(jsep.sdp); + } + session.whipOut + .setRemoteDescription(jsep) + .then(async function () { + warnlog("SHOULD BE CONNECTED?"); + //var content = ""; + //while (candidates.length) { + // var candidate = candidates.pop(); + // content += candidate.candidate; + //} + //warnlog("Content: " + content.length); + //if (content){ + // warnlog("SENDING TRICKLE"); //.. I should, but I'm not, since most sites don't support it still. + if (session.whipoutSettings) { + session.whipoutSettings.started = Date.now(); + } + if (session.whipOutKeyframe) { + clearTimeout(session.whipOut.keyframeTimeout); + session.whipOut.keyframeTimeout = setInterval(function () { + GOP(); + }, session.whipOutKeyframe); // ensure GOP no longer than 6s + } + session.whipOutSetScale(); + await sleep(1000); // give whip server a moment to setup I guess. + if (session.whipoutSettings) { + for (var UUID in session.pcs) { + if (session.pcs[UUID].whipout === null) { + var data = {}; + data.whepSettings = session.whipoutSettings; + if (session.sendMessage(data, UUID)) { + session.pcs[UUID].whipout = true; + } + } + } + } + //} + }) + .catch(async function (e) { + errorlog(e); + errorlog("Recieved an invalid SDP answer response from the WHIP endpoint. While things may still work, it won't work as intended."); + if (WHELPlaybackURL) { + log("Since we have the WHEP URL, we will assume its desired to forward it on, even if it may not work."); + if (session.whipoutSettings) { + session.whipoutSettings.started = Date.now(); + } + try { + if (session.whipOutKeyframe) { + clearTimeout(session.whipOut.keyframeTimeout); + session.whipOut.keyframeTimeout = setInterval(function () { + GOP(); + }, session.whipOutKeyframe); // ensure GOP no longer than 6s + } + session.whipOutSetScale(); + } catch (e) { + errorlog(e); + } + await sleep(1000); // give whip server a moment to setup I guess. + if (session.whipoutSettings) { + for (var UUID in session.pcs) { + if (session.pcs[UUID].whipout === null) { + var data = {}; + data.whepSettings = session.whipoutSettings; + if (session.sendMessage(data, UUID)) { + session.pcs[UUID].whipout = true; + } + } + } + } + } + }); } else if (contentType == "application/error") { if (this.responseText == 432) { warnUser("Whip out error: 432"); @@ -55331,517 +55796,517 @@ function whipOut() { } }; - xhttp.send(data); - } catch (e) { - errorlog(e); - } - } - function GOP() { - log("Sending keyframe"); - try { - if (!session.whipOut) { - return; - } - - var senders = session.whipOut.getSenders(); - var sender = false; - senders.forEach(senderVideo => { - if (senderVideo.track && senderVideo.track.id && senderVideo.track.kind == "video") { - sender = senderVideo; - } - }); - - if (!sender) { - log("GOP(): can't change bitrate; no video sender found"); - return false; - } - - var settings = {}; - settings.scaleResolutionDownBy = 10; // 50% of default max - - setEncodings( - sender, - settings, - function (sendr) { - var settings = {}; - - var chromeVersion = getChromiumVersion(); - if (chromeVersion > 80) { - // just because - settings.scaleResolutionDownBy = null; - } else { - settings.scaleResolutionDownBy = 1.0; - } - - setEncodings(sendr, settings, function () { - //log("scaleResolutionDownBy set 3b!"); - }); - }, - sender - ); - - return true; - } catch (e) { - errorlog(e); - } - } - - // WHIP auto-reconnection with exponential backoff - var whipReconnecting = false; - var whipReconnectAttempts = 0; - - function retryWhipConnection() { - if (!session.whipOutput) { - log("No WHIP output configured, stopping retry"); - return; - } - - if (whipReconnecting) { - return; - } - - // Check if already connected - if (session.whipOut && - (session.whipOut.connectionState === 'connected' || - session.whipOut.iceConnectionState === 'connected' || - session.whipOut.iceConnectionState === 'completed')) { - log("WHIP connection is already established. No need to reconnect."); - whipReconnecting = false; - whipReconnectAttempts = 0; - return; - } - - whipReconnecting = true; - - const maxRetries = 5; - const initialDelay = 2000; - const maxDelay = 20000; - - let currentRetry = whipReconnectAttempts; - let currentDelay = Math.min(initialDelay * Math.pow(2, currentRetry), maxDelay); - - function attemptReconnect() { - if (!session.whipOutput) { - log("WHIP output removed, stopping retry"); - whipReconnecting = false; - return; - } - - // Check if connection recovered - if (session.whipOut && - (session.whipOut.connectionState === 'connected' || - session.whipOut.iceConnectionState === 'connected' || - session.whipOut.iceConnectionState === 'completed')) { - log("WHIP connection recovered. Stopping retry."); - whipReconnecting = false; - whipReconnectAttempts = 0; - return; - } - - log("Attempting WHIP reconnection (attempt " + (currentRetry + 1) + "/" + maxRetries + ")"); - - // Close existing connection - if (session.whipOut) { - try { - session.whipOut.close(); - } catch (e) { - warnlog(e); - } - session.whipOut = null; - } - - // Reset publishing state - publishing = false; - candidates = []; - - // Attempt reconnection - reuses session.whipOutput and session.whipOutputToken - try { - whipConnect(); - // Give it time to connect before checking - var checkAttempts = 0; - var maxCheckAttempts = 6; // Up to 30 seconds total (6 x 5s) - function checkConnectionState() { - checkAttempts++; - if (session.whipOut && - (session.whipOut.connectionState === 'connected' || - session.whipOut.iceConnectionState === 'connected' || - session.whipOut.iceConnectionState === 'completed')) { - log("WHIP reconnection successful"); - whipReconnecting = false; - whipReconnectAttempts = 0; - } else if (session.whipOut && - (session.whipOut.connectionState === 'connecting' || - session.whipOut.iceConnectionState === 'checking' || - session.whipOut.iceConnectionState === 'new')) { - // Still connecting - wait longer before declaring failure - if (checkAttempts < maxCheckAttempts) { - log("WHIP still connecting, waiting... (" + checkAttempts + "/" + maxCheckAttempts + ")"); - setTimeout(checkConnectionState, 5000); - } else { - log("WHIP connection timeout after " + (checkAttempts * 5) + "s"); - scheduleNextRetry(); - } - } else { - scheduleNextRetry(); - } - } - function scheduleNextRetry() { - currentRetry++; - whipReconnectAttempts = currentRetry; - if (currentRetry < maxRetries) { - currentDelay = Math.min(currentDelay * 2, maxDelay); - log("WHIP reconnection not yet established. Retrying in " + currentDelay + "ms"); - setTimeout(attemptReconnect, currentDelay); - } else { - log("WHIP max retries reached, stopping reconnection attempts"); - whipReconnecting = false; - } - } - setTimeout(checkConnectionState, 5000); // First check after 5 seconds - } catch (e) { - errorlog(e); - currentRetry++; - whipReconnectAttempts = currentRetry; - if (currentRetry < maxRetries) { - currentDelay = Math.min(currentDelay * 2, maxDelay); - setTimeout(attemptReconnect, currentDelay); - } else { - whipReconnecting = false; - } - } - } - - // Start first attempt after initial delay - log("WHIP connection lost. Will retry in " + currentDelay + "ms"); - setTimeout(attemptReconnect, currentDelay); - } - - // Expose for manual reconnection from UI - session.restartWhipConnection = function() { - log("Manual WHIP restart requested"); - whipReconnecting = false; - whipReconnectAttempts = 0; - if (session.whipOut) { - try { - session.whipOut.close(); - } catch (e) { - warnlog(e); - } - session.whipOut = null; - } - publishing = false; - candidates = []; - whipConnect(); - }; - - // Track reconnect attempts for mesh debug visibility - session.getWhipReconnectAttempts = function() { - return whipReconnectAttempts; - }; - - whipConnect(); -} - -function cleanupStereoSettings(sdp) { - if (typeof sdp !== "string" || sdp === "") { - return sdp; - } - let lines = sdp.split("\n"); - lines = lines.map(line => { - if (line.startsWith("a=fmtp:") && (line.includes("sprop-stereo=0;") || line.includes("stereo=0;"))) { - line = line.replace("sprop-stereo=0;", "").replace("stereo=0;", ""); - line = line.replace(";;", ";").replace(/;$/, ""); - } - return line; - }); - return lines.join("\n"); -} - -function ensureViewerRpcDefaults(UUID) { - try { - if (!session || !session.rpcs || !session.rpcs[UUID]) { - return; - } - const rpc = session.rpcs[UUID]; - if (!rpc.stats) { - rpc.stats = {}; - } - if (typeof rpc.allowGraphs === "undefined") { - rpc.allowGraphs = false; - } - if (typeof rpc.allowDrawing === "undefined") { - rpc.allowDrawing = false; - } - if (!rpc.inboundAudioPipeline) { - rpc.inboundAudioPipeline = {}; - } - if (typeof rpc.channelOffset === "undefined") { - rpc.channelOffset = false; - } - if (typeof rpc.channelWidth === "undefined") { - rpc.channelWidth = false; - } - if (typeof rpc.settings === "undefined") { - rpc.settings = false; - } - if (typeof rpc.defaultSpeaker === "undefined") { - rpc.defaultSpeaker = false; - } - if (typeof rpc.lockedVideoBitrate === "undefined") { - rpc.lockedVideoBitrate = false; - } - if (typeof rpc.lockedAudioBitrate === "undefined") { - rpc.lockedAudioBitrate = false; - } - if (typeof rpc.manualBandwidth === "undefined") { - rpc.manualBandwidth = false; - } - if (typeof rpc.motionDetectionInterval === "undefined") { - rpc.motionDetectionInterval = false; - } - if (typeof rpc.buffer === "undefined") { - rpc.buffer = false; - } - if (typeof rpc.getStatsTimeout === "undefined") { - rpc.getStatsTimeout = null; - } - } catch (e) { - errorlog(e); - } -} - -function broadcastWhepSettings(kind = "primary") { - if (session.noMeshcast) { - return false; - } - let settings = null; - let property = "whipout"; - let allowProperty = null; - if (kind === "screen") { - if (!session.screenShareState) { - return false; - } - settings = session.whipoutScreenSettings; - property = "whipScreen"; - allowProperty = "screenWhepAllowed"; - } else { - settings = session.whipoutSettings; - } - if (!settings || !settings.url) { - return false; - } - const startedMarker = typeof settings.started === "number" && settings.started > 0 ? settings.started : false; - if (kind === "screen" && !startedMarker) { - return false; - } - const marker = startedMarker || true; - let sent = false; - for (const UUID in session.pcs) { - if (!session.pcs.hasOwnProperty(UUID)) { - continue; - } - const peer = session.pcs[UUID]; - if (!peer) { - continue; - } - if (peer[property] === false) { - continue; - } - if (startedMarker && peer[property] === startedMarker) { - continue; - } - if (!startedMarker && peer[property] === marker) { - continue; - } - if (!startedMarker && peer[property] === true) { - continue; - } - if (allowProperty && peer[allowProperty] === false) { - continue; - } - const data = {}; - const payload = Object.assign({}, settings); - if (kind === "screen") { - data.whepScreenSettings = payload; - } else { - data.whepSettings = payload; - } - if (session.sendMessage(data, UUID)) { - peer[property] = marker; - sent = true; - } - } - return sent; -} - -async function whipOutScreen() { - log("whipOutScreen"); - if (!session.whipPublishScreen || !session.whipOutputScreen) { - log("whipOutScreen skipped: screen WHIP disabled"); - return false; - } - if (!session.screenShareState || !session.screenStream) { - log("whipOutScreen waiting: no active screen stream"); - return false; - } - - try { - if (!session.configuration) { - await chooseBestTURN(); - } - } catch (e) { - errorlog(e); - } - - for (const UUID in session.pcs) { - if (!session.pcs.hasOwnProperty(UUID)) { - continue; - } - if (session.pcs[UUID]) { - if (session.pcs[UUID].whipScreen !== false) { - session.pcs[UUID].whipScreen = null; - } - } - } - - const config = { ...session.configuration }; - if (session.encodedInsertableStreams) { - config.encodedInsertableStreams = true; - } - if (session.bundlePolicy) { - config.bundlePolicy = session.bundlePolicy; - } - - try { - if (session.whipOutScreen && session.whipOutScreen.close) { - try { - session.whipOutScreen.getSenders().forEach(sender => { - try { - if (sender.replaceTrack) { - const result = sender.replaceTrack(null); - if (result && typeof result.catch === "function") { - result.catch(() => { }); - } - } - } catch (e) { } - }); - } catch (e) { } - session.whipOutScreen.close(); - } - } catch (e) { - errorlog(e); - } - - let pc; - try { - pc = new RTCPeerConnection(config); - } catch (err) { - errorlog(err); - return false; - } - - session.whipOutScreen = pc; - pc.stats = {}; - pc.maxBandwidth = null; - pc.scale = false; - pc.offerToReceiveAudio = false; - pc.offerToReceiveVideo = false; - - const candidates = []; - let iceGatheringResolve; - const iceGatheringPromise = new Promise(resolve => { - iceGatheringResolve = resolve; - }); - - pc.onicecandidate = event => { - if (event.candidate) { - candidates.push(event.candidate); - } else if (iceGatheringResolve) { - iceGatheringResolve(); - } - }; - pc.onicegatheringstatechange = () => { - if (pc.iceGatheringState === "complete" && iceGatheringResolve) { - iceGatheringResolve(); - } - }; - pc.oniceconnectionstatechange = () => { - if (pc.iceConnectionState === "failed" || pc.iceConnectionState === "disconnected" || pc.iceConnectionState === "closed") { - warnlog("Screen WHIP ICE state: " + pc.iceConnectionState); - } - }; - - const stream = session.screenStream; - stream.getTracks().forEach(track => { - try { - pc.addTransceiver(track, { direction: "sendonly", streams: [stream] }); - } catch (e) { - try { - pc.addTrack(track, stream); - } catch (err) { - errorlog(err); - } - } - }); - - let offer; - try { - offer = await pc.createOffer(); - } catch (e) { - errorlog(e); - pc.close(); - session.whipOutScreen = null; - return false; - } - - try { - offer = configureWhipOutSDP(offer); - } catch (e) { - errorlog(e); - } - - try { - await pc.setLocalDescription(offer); - } catch (e) { - errorlog(e); - pc.close(); - session.whipOutScreen = null; - return false; - } - - try { - if (session.whipWait) { - let timedOut = false; - const timer = sleep(session.whipWait).then(() => { - timedOut = true; - if (iceGatheringResolve) { - iceGatheringResolve(); - } - }); - await Promise.race([iceGatheringPromise, timer]); - if (timedOut) { - warnlog("Screen WHIP ICE gathering timed out after " + session.whipWait + "ms"); - } - } else { - await iceGatheringPromise; - } - } catch (e) { - errorlog(e); - } - - let localSDP = pc.localDescription ? pc.localDescription.sdp : null; - if (!localSDP) { - pc.close(); - session.whipOutScreen = null; - return false; - } - var filteredDesc = filterDescriptionIpv6(pc.localDescription); - localSDP = filteredDesc.sdp; - localSDP = cleanupStereoSettings(localSDP); - - function sendOfferToEndpoint(sdpPayload) { - return new Promise((resolve, reject) => { - const xhttp = new XMLHttpRequest(); + xhttp.send(data); + } catch (e) { + errorlog(e); + } + } + function GOP() { + log("Sending keyframe"); + try { + if (!session.whipOut) { + return; + } + + var senders = session.whipOut.getSenders(); + var sender = false; + senders.forEach(senderVideo => { + if (senderVideo.track && senderVideo.track.id && senderVideo.track.kind == "video") { + sender = senderVideo; + } + }); + + if (!sender) { + log("GOP(): can't change bitrate; no video sender found"); + return false; + } + + var settings = {}; + settings.scaleResolutionDownBy = 10; // 50% of default max + + setEncodings( + sender, + settings, + function (sendr) { + var settings = {}; + + var chromeVersion = getChromiumVersion(); + if (chromeVersion > 80) { + // just because + settings.scaleResolutionDownBy = null; + } else { + settings.scaleResolutionDownBy = 1.0; + } + + setEncodings(sendr, settings, function () { + //log("scaleResolutionDownBy set 3b!"); + }); + }, + sender + ); + + return true; + } catch (e) { + errorlog(e); + } + } + + // WHIP auto-reconnection with exponential backoff + var whipReconnecting = false; + var whipReconnectAttempts = 0; + + function retryWhipConnection() { + if (!session.whipOutput) { + log("No WHIP output configured, stopping retry"); + return; + } + + if (whipReconnecting) { + return; + } + + // Check if already connected + if (session.whipOut && + (session.whipOut.connectionState === 'connected' || + session.whipOut.iceConnectionState === 'connected' || + session.whipOut.iceConnectionState === 'completed')) { + log("WHIP connection is already established. No need to reconnect."); + whipReconnecting = false; + whipReconnectAttempts = 0; + return; + } + + whipReconnecting = true; + + const maxRetries = 5; + const initialDelay = 2000; + const maxDelay = 20000; + + let currentRetry = whipReconnectAttempts; + let currentDelay = Math.min(initialDelay * Math.pow(2, currentRetry), maxDelay); + + function attemptReconnect() { + if (!session.whipOutput) { + log("WHIP output removed, stopping retry"); + whipReconnecting = false; + return; + } + + // Check if connection recovered + if (session.whipOut && + (session.whipOut.connectionState === 'connected' || + session.whipOut.iceConnectionState === 'connected' || + session.whipOut.iceConnectionState === 'completed')) { + log("WHIP connection recovered. Stopping retry."); + whipReconnecting = false; + whipReconnectAttempts = 0; + return; + } + + log("Attempting WHIP reconnection (attempt " + (currentRetry + 1) + "/" + maxRetries + ")"); + + // Close existing connection + if (session.whipOut) { + try { + session.whipOut.close(); + } catch (e) { + warnlog(e); + } + session.whipOut = null; + } + + // Reset publishing state + publishing = false; + candidates = []; + + // Attempt reconnection - reuses session.whipOutput and session.whipOutputToken + try { + whipConnect(); + // Give it time to connect before checking + var checkAttempts = 0; + var maxCheckAttempts = 6; // Up to 30 seconds total (6 x 5s) + function checkConnectionState() { + checkAttempts++; + if (session.whipOut && + (session.whipOut.connectionState === 'connected' || + session.whipOut.iceConnectionState === 'connected' || + session.whipOut.iceConnectionState === 'completed')) { + log("WHIP reconnection successful"); + whipReconnecting = false; + whipReconnectAttempts = 0; + } else if (session.whipOut && + (session.whipOut.connectionState === 'connecting' || + session.whipOut.iceConnectionState === 'checking' || + session.whipOut.iceConnectionState === 'new')) { + // Still connecting - wait longer before declaring failure + if (checkAttempts < maxCheckAttempts) { + log("WHIP still connecting, waiting... (" + checkAttempts + "/" + maxCheckAttempts + ")"); + setTimeout(checkConnectionState, 5000); + } else { + log("WHIP connection timeout after " + (checkAttempts * 5) + "s"); + scheduleNextRetry(); + } + } else { + scheduleNextRetry(); + } + } + function scheduleNextRetry() { + currentRetry++; + whipReconnectAttempts = currentRetry; + if (currentRetry < maxRetries) { + currentDelay = Math.min(currentDelay * 2, maxDelay); + log("WHIP reconnection not yet established. Retrying in " + currentDelay + "ms"); + setTimeout(attemptReconnect, currentDelay); + } else { + log("WHIP max retries reached, stopping reconnection attempts"); + whipReconnecting = false; + } + } + setTimeout(checkConnectionState, 5000); // First check after 5 seconds + } catch (e) { + errorlog(e); + currentRetry++; + whipReconnectAttempts = currentRetry; + if (currentRetry < maxRetries) { + currentDelay = Math.min(currentDelay * 2, maxDelay); + setTimeout(attemptReconnect, currentDelay); + } else { + whipReconnecting = false; + } + } + } + + // Start first attempt after initial delay + log("WHIP connection lost. Will retry in " + currentDelay + "ms"); + setTimeout(attemptReconnect, currentDelay); + } + + // Expose for manual reconnection from UI + session.restartWhipConnection = function() { + log("Manual WHIP restart requested"); + whipReconnecting = false; + whipReconnectAttempts = 0; + if (session.whipOut) { + try { + session.whipOut.close(); + } catch (e) { + warnlog(e); + } + session.whipOut = null; + } + publishing = false; + candidates = []; + whipConnect(); + }; + + // Track reconnect attempts for mesh debug visibility + session.getWhipReconnectAttempts = function() { + return whipReconnectAttempts; + }; + + whipConnect(); +} + +function cleanupStereoSettings(sdp) { + if (typeof sdp !== "string" || sdp === "") { + return sdp; + } + let lines = sdp.split("\n"); + lines = lines.map(line => { + if (line.startsWith("a=fmtp:") && (line.includes("sprop-stereo=0;") || line.includes("stereo=0;"))) { + line = line.replace("sprop-stereo=0;", "").replace("stereo=0;", ""); + line = line.replace(";;", ";").replace(/;$/, ""); + } + return line; + }); + return lines.join("\n"); +} + +function ensureViewerRpcDefaults(UUID) { + try { + if (!session || !session.rpcs || !session.rpcs[UUID]) { + return; + } + const rpc = session.rpcs[UUID]; + if (!rpc.stats) { + rpc.stats = {}; + } + if (typeof rpc.allowGraphs === "undefined") { + rpc.allowGraphs = false; + } + if (typeof rpc.allowDrawing === "undefined") { + rpc.allowDrawing = false; + } + if (!rpc.inboundAudioPipeline) { + rpc.inboundAudioPipeline = {}; + } + if (typeof rpc.channelOffset === "undefined") { + rpc.channelOffset = false; + } + if (typeof rpc.channelWidth === "undefined") { + rpc.channelWidth = false; + } + if (typeof rpc.settings === "undefined") { + rpc.settings = false; + } + if (typeof rpc.defaultSpeaker === "undefined") { + rpc.defaultSpeaker = false; + } + if (typeof rpc.lockedVideoBitrate === "undefined") { + rpc.lockedVideoBitrate = false; + } + if (typeof rpc.lockedAudioBitrate === "undefined") { + rpc.lockedAudioBitrate = false; + } + if (typeof rpc.manualBandwidth === "undefined") { + rpc.manualBandwidth = false; + } + if (typeof rpc.motionDetectionInterval === "undefined") { + rpc.motionDetectionInterval = false; + } + if (typeof rpc.buffer === "undefined") { + rpc.buffer = false; + } + if (typeof rpc.getStatsTimeout === "undefined") { + rpc.getStatsTimeout = null; + } + } catch (e) { + errorlog(e); + } +} + +function broadcastWhepSettings(kind = "primary") { + if (session.noMeshcast) { + return false; + } + let settings = null; + let property = "whipout"; + let allowProperty = null; + if (kind === "screen") { + if (!session.screenShareState) { + return false; + } + settings = session.whipoutScreenSettings; + property = "whipScreen"; + allowProperty = "screenWhepAllowed"; + } else { + settings = session.whipoutSettings; + } + if (!settings || !settings.url) { + return false; + } + const startedMarker = typeof settings.started === "number" && settings.started > 0 ? settings.started : false; + if (kind === "screen" && !startedMarker) { + return false; + } + const marker = startedMarker || true; + let sent = false; + for (const UUID in session.pcs) { + if (!session.pcs.hasOwnProperty(UUID)) { + continue; + } + const peer = session.pcs[UUID]; + if (!peer) { + continue; + } + if (peer[property] === false) { + continue; + } + if (startedMarker && peer[property] === startedMarker) { + continue; + } + if (!startedMarker && peer[property] === marker) { + continue; + } + if (!startedMarker && peer[property] === true) { + continue; + } + if (allowProperty && peer[allowProperty] === false) { + continue; + } + const data = {}; + const payload = Object.assign({}, settings); + if (kind === "screen") { + data.whepScreenSettings = payload; + } else { + data.whepSettings = payload; + } + if (session.sendMessage(data, UUID)) { + peer[property] = marker; + sent = true; + } + } + return sent; +} + +async function whipOutScreen() { + log("whipOutScreen"); + if (!session.whipPublishScreen || !session.whipOutputScreen) { + log("whipOutScreen skipped: screen WHIP disabled"); + return false; + } + if (!session.screenShareState || !session.screenStream) { + log("whipOutScreen waiting: no active screen stream"); + return false; + } + + try { + if (!session.configuration) { + await chooseBestTURN(); + } + } catch (e) { + errorlog(e); + } + + for (const UUID in session.pcs) { + if (!session.pcs.hasOwnProperty(UUID)) { + continue; + } + if (session.pcs[UUID]) { + if (session.pcs[UUID].whipScreen !== false) { + session.pcs[UUID].whipScreen = null; + } + } + } + + const config = { ...session.configuration }; + if (session.encodedInsertableStreams) { + config.encodedInsertableStreams = true; + } + if (session.bundlePolicy) { + config.bundlePolicy = session.bundlePolicy; + } + + try { + if (session.whipOutScreen && session.whipOutScreen.close) { + try { + session.whipOutScreen.getSenders().forEach(sender => { + try { + if (sender.replaceTrack) { + const result = sender.replaceTrack(null); + if (result && typeof result.catch === "function") { + result.catch(() => { }); + } + } + } catch (e) { } + }); + } catch (e) { } + session.whipOutScreen.close(); + } + } catch (e) { + errorlog(e); + } + + let pc; + try { + pc = new RTCPeerConnection(config); + } catch (err) { + errorlog(err); + return false; + } + + session.whipOutScreen = pc; + pc.stats = {}; + pc.maxBandwidth = null; + pc.scale = false; + pc.offerToReceiveAudio = false; + pc.offerToReceiveVideo = false; + + const candidates = []; + let iceGatheringResolve; + const iceGatheringPromise = new Promise(resolve => { + iceGatheringResolve = resolve; + }); + + pc.onicecandidate = event => { + if (event.candidate) { + candidates.push(event.candidate); + } else if (iceGatheringResolve) { + iceGatheringResolve(); + } + }; + pc.onicegatheringstatechange = () => { + if (pc.iceGatheringState === "complete" && iceGatheringResolve) { + iceGatheringResolve(); + } + }; + pc.oniceconnectionstatechange = () => { + if (pc.iceConnectionState === "failed" || pc.iceConnectionState === "disconnected" || pc.iceConnectionState === "closed") { + warnlog("Screen WHIP ICE state: " + pc.iceConnectionState); + } + }; + + const stream = session.screenStream; + stream.getTracks().forEach(track => { + try { + pc.addTransceiver(track, { direction: "sendonly", streams: [stream] }); + } catch (e) { + try { + pc.addTrack(track, stream); + } catch (err) { + errorlog(err); + } + } + }); + + let offer; + try { + offer = await pc.createOffer(); + } catch (e) { + errorlog(e); + pc.close(); + session.whipOutScreen = null; + return false; + } + + try { + offer = configureWhipOutSDP(offer); + } catch (e) { + errorlog(e); + } + + try { + await pc.setLocalDescription(offer); + } catch (e) { + errorlog(e); + pc.close(); + session.whipOutScreen = null; + return false; + } + + try { + if (session.whipWait) { + let timedOut = false; + const timer = sleep(session.whipWait).then(() => { + timedOut = true; + if (iceGatheringResolve) { + iceGatheringResolve(); + } + }); + await Promise.race([iceGatheringPromise, timer]); + if (timedOut) { + warnlog("Screen WHIP ICE gathering timed out after " + session.whipWait + "ms"); + } + } else { + await iceGatheringPromise; + } + } catch (e) { + errorlog(e); + } + + let localSDP = pc.localDescription ? pc.localDescription.sdp : null; + if (!localSDP) { + pc.close(); + session.whipOutScreen = null; + return false; + } + var filteredDesc = filterDescriptionIpv6(pc.localDescription); + localSDP = filteredDesc.sdp; + localSDP = cleanupStereoSettings(localSDP); + + function sendOfferToEndpoint(sdpPayload) { + return new Promise((resolve, reject) => { + const xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function () { if (this.readyState === 4) { if (this.status === 200 || this.status === 201 || this.status === 204) { @@ -55865,31 +56330,31 @@ async function whipOutScreen() { } }; - xhttp.onerror = reject; - try { - xhttp.open("POST", session.whipOutputScreen, true); - } catch (e) { - reject(e); - return; - } - xhttp.setRequestHeader("Content-Type", "application/sdp"); - if (session.whipOutputToken) { - xhttp.setRequestHeader("Authorization", "Bearer " + session.whipOutputToken); - } - xhttp.send(sdpPayload); - }); - } - - let response; - try { - response = await sendOfferToEndpoint(localSDP); - } catch (err) { - errorlog(err); - pc.close(); - session.whipOutScreen = null; - return false; - } - + xhttp.onerror = reject; + try { + xhttp.open("POST", session.whipOutputScreen, true); + } catch (e) { + reject(e); + return; + } + xhttp.setRequestHeader("Content-Type", "application/sdp"); + if (session.whipOutputToken) { + xhttp.setRequestHeader("Authorization", "Bearer " + session.whipOutputToken); + } + xhttp.send(sdpPayload); + }); + } + + let response; + try { + response = await sendOfferToEndpoint(localSDP); + } catch (err) { + errorlog(err); + pc.close(); + session.whipOutScreen = null; + return false; + } + const { status, contentType, linkHeader, locationHeader, whepHeader, headers, body } = response; if (locationHeader) { @@ -55990,5351 +56455,5351 @@ async function whipOutScreen() { whepUrl = session.whipOutputScreen.replace("/whip", "/whep"); } - - if (contentType && contentType.indexOf("application/sdp") === 0 && body) { - try { - const answer = { type: "answer", sdp: body }; - await pc.setRemoteDescription(answer); - } catch (e) { - errorlog(e); - } - } - - if (!session.whipoutScreenSettings) { - session.whipoutScreenSettings = { type: "whep", url: whepUrl, token: session.streamID + "_s", media: "screen", started: false }; - } else { - session.whipoutScreenSettings.url = whepUrl; - } - if (!session.whipoutScreenSettings.token) { - session.whipoutScreenSettings.token = session.streamID + "_s"; - } - session.whipoutScreenSettings.media = "screen"; - session.whipoutScreenSettings.started = Date.now(); - - broadcastWhepSettings("screen"); - return true; -} - -function whipClient() { - // publish to whip.vdo.ninja with obs, to use. experimental - if (!session.whipView) { - return; - } - warnlog("WHIP Client started"); - - var socket = null; - var connecting = false; - var failedCount = 0; - - function connect() { - clearTimeout(connecting); - if (socket) { - if (socket.readyState === socket.OPEN) { - return; - } - try { - socket.close(); - } catch (e) { } - } - log("Trying to load whip websocket..."); - - socket = new WebSocket(session.whipServerURL); - - socket.onclose = function () { - failedCount += 1; - clearTimeout(connecting); - connecting = setTimeout(function () { - connect(); - }, 100 * (failedCount - 1)); - }; - - socket.onerror = function (e) { - console.error(e); - failedCount += 1; - clearTimeout(connecting); - connecting = setTimeout(function () { - connect(); - }, 100 * failedCount); - }; - - socket.onopen = function () { - failedCount = 0; - try { - var settings = {}; - socket.send(JSON.stringify({ join: session.whipView })); - } catch (e) { - connecting = setTimeout(function () { - connect(); - }, 1); - } - }; - - socket.addEventListener("message", async function (event) { - if (event.data) { - var data = JSON.parse(event.data); - - if ("sdp" in data) { - try { - var resp = await processWhipIn(data); - } catch (e) { - var resp = e && (e.message || e.toString()); - } - if (resp) { - var ret = {}; - var get = data.get; - data = {}; - if (get) { - data.get = get; - data.result = resp; - ret.callback = data; - socket.send(JSON.stringify(ret)); - } - } - } else if (data.type === "candidate") { - if (data.candidate && data.streamID) { - await handleIncomingIceCandidate(data); - } - } else if (data.type == "delete") { - warnlog("WHIP publisher is actively disconnecting"); - // session.closeRPC(i, true); - var ret = {}; - var get = data.get; - data = {}; - if (get) { - data.get = get; - data.result = "OK"; - ret.callback = data; - socket.send(JSON.stringify(ret)); - } - } else { - warnlog("Unsupported incoming data"); - } - } - }); - } - connect(); -} -async function processWhipIn(data) { - // LISTEN FOR REMOTE WHIP (from OBS?) - var msg = {}; - msg.description = {}; - msg.description.type = "offer"; - msg.description.sdp = data.sdp; - var UUID = session.generateRandomString(25); // fake - msg.UUID = UUID; - - if (session.forceNoAudioWhipIn || session.forceNoVideoWhipIn) { - try { - log(msg.description.sdp + ""); - msg.description.sdp = CodecsHandler.modifySdp(msg.description.sdp, session.forceNoAudioWhipIn, session.forceNoVideoWhipIn); - } catch (e) { - errorlog(e); - } - } - - if (!msg.description.sdp.includes("a=group:BUNDLE")) { // handling of bundle-only media lines; gstreamer's whipsink 1.25 for example - try { - const sdpLines = msg.description.sdp.split('\r\n'); - const bundleLines = sdpLines.filter(line => line.includes('a=mid:')); - if (bundleLines.length > 0) { - const mids = bundleLines.map(line => line.split(':')[1]); - sdpLines.splice(1, 0, `a=group:BUNDLE ${mids.join(' ')}`); - msg.description.sdp = sdpLines.join('\r\n'); - } - } catch (e) { - errorlog(e); - } - } - - if (data.streamID) { - msg.streamID = data.streamID; - } else { - msg.streamID = session.generateRandomString(15); // fake - } - - log("setupIncoming"); - await session.setupIncoming(msg); // could end up setting up the peer the wrong way. - - session.rpcs[UUID].whip = true; - var callback = null; - var promise = new Promise((resolve, reject) => { - callback = resolve; - }); - session.rpcs[UUID].whipCallback = callback; - - var callback2 = null; - var promise2 = new Promise((resolve, reject) => { - callback2 = resolve; - }); - session.rpcs[UUID].whipCallback2 = callback2; - - log("CONNECT PEER"); - session.connectPeer(msg); - log("CONNECT PEER DONE"); - - log("ICE BUNDLE PROMISE"); - setTimeout( - function (UUID) { - if (session.rpcs[UUID].whipCallback2) { - session.rpcs[UUID].whipCallback2([...session.rpcs[UUID].iceBundle]); - clearTimeout(session.rpcs[UUID].iceTimer); - session.rpcs[UUID].iceTimer = null; - session.rpcs[UUID].iceBundle = []; - session.rpcs[UUID].whipCallback2 = null; - } - }, - session.whepWait, - UUID - ); - - var iceBundle = await promise2; // waiting for ICE GATHER COMPLETE; default 2 second. change with &whipwait=2000 - - clearTimeout(session.rpcs[UUID].iceTimer); - session.rpcs[UUID].iceTimer = null; - session.rpcs[UUID].whipCallback2 = null; - - log("ICE BUNDLE DONE"); - log(iceBundle); - - await promise; - session.rpcs[UUID].whipCallback = null; - - sdpAnswer = session.rpcs[UUID].localDescription.sdp; - - if (session.localNetworkOnly) { - sdpAnswer = filterSDPLAN(sdpAnswer); - } - if (session.stunOnly) { // or whatever flag you want to use - sdpAnswer = filterStunOnly(sdpAnswer); - } - - //iceBundle.forEach(ice => { // not needed, since the localDescription has it embedded already, since we waited - // sdpAnswer += `a=${ice.candidate}\r\n`; - //}); - - /* - if (true){ // this code tries to force the TURN server into use, but it's not working that I can see. - - const sdpLines = sdpAnswer.split('\r\n'); - const modifiedLines = []; - let mediaSection = 0; - let candidateAdded = false; - let audioPort = null; - - for (let line of sdpLines) { - if (line.startsWith('m=')) { - mediaSection++; - if (mediaSection === 1) { - // Extract audio port - audioPort = line.split(' ')[1]; - } else if (mediaSection === 2 && audioPort) { - // Set video port to match audio port - line = `m=video ${audioPort} UDP/TLS/RTP/SAVPF 96`; - } - } - - if (line.startsWith('c=')) { - line = `c=IN IP4 51.222.12.223`; - } - - if (line.startsWith('a=candidate:') && !candidateAdded) { - line = `a=candidate:1 1 UDP 2 51.222.12.223 3478 typ relay raddr 0.0.0.0 rport 0`; - candidateAdded = true; - } - - modifiedLines.push(line); - } - - return modifiedLines.join('\r\n'); - } - */ - - return sdpAnswer; // return SDP answer for the remote WHIP request -} -async function handleIncomingIceCandidate(data) { - const UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === data.streamID); - if (UUID && session.rpcs[UUID]) { - try { - await session.rpcs[UUID].addIceCandidate(new RTCIceCandidate(data.candidate)); - log("Added incoming ICE candidate for stream: " + data.streamID); - } catch (e) { - errorlog("Error adding incoming ICE candidate: ", e); - } - } else { - warnlog("Received ICE candidate for unknown stream: " + data.streamID); - } -} -function processSDPFromServer(sdp) { - // not the description package; just the sdp - try { - if (session.mono && Firefox) { - // chrome defaults to mono already, but we can force Firefox mono if needed - sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 0 }, true); - } else if (Firefox) { - // Let Firefox be Firefox.. else it might break the server. mediamtx err 400 response if mono - } else if (session.stereo && session.stereo == 4) { - // pro audio only when viewing streams - sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 2 }, true); - } else if (session.stereo && !session.mono && session.stereo != 3) { - sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 1 }, true); - } - } catch (e) { - errorlog(e); - } - return sdp; -} - -function filterIceLAN(candidate) { - - if (typeof candidate === 'string') { - return filterSDPLAN(candidate); - } - if (!candidate) { return candidate; } - - try { - let candidateString = candidate.candidate; - let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/; - let isMDNSHostname = candidateString.includes(".local"); - - if (!(candidateString.includes("typ host") && (privateIPPattern.test(candidateString) || isMDNSHostname))) { - log("🟧 dropped candidate due to not being a LAN candidate"); - return false; - } - log("🟢 candidate allowed since host-type and has a LAN-IP or is a .local hostname"); - } catch (e) { - errorlog(e); - } - return true; -} -function filterSDPLAN(sdp) { - try { - return sdp - .split("\n") - .filter(line => { - if (line.startsWith("a=candidate:")) { - let parts = line.split(" "); - let type = parts[7]; // The 'typ' field in the candidate string - let address = parts[4]; - let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/; - let isMDNSHostname = address.endsWith(".local"); - log("🟥 dropped candidate from SDP due to not being a LAN candidate"); - return type === "host" && (privateIPPattern.test(address) || isMDNSHostname); - } - return true; - }) - .join("\n"); - } catch (e) { - errorlog(e); - return sdp; - } -} - -function filterStunOnlySDP(sdpString) { - try { - const lines = sdpString.split('\r\n'); - const filteredLines = lines.filter(line => { - // If it's a candidate line - if (line.startsWith('a=candidate:')) { - // Only keep STUN candidates - return line.includes(' typ srflx '); - } - - // Keep everything that's not a candidate line - return true; - }); - - // Process the filtered SDP - let processedLines = []; - let currentMedia = null; - let stunPort = null; - let stunAddress = null; - - // First pass - find STUN candidate details - for (const line of filteredLines) { - if (line.includes(' typ srflx ')) { - const parts = line.split(' '); - for (let i = 0; i < parts.length; i++) { - if (parts[i] === 'typ' && parts[i + 1] === 'srflx') { - stunPort = parts[5]; - stunAddress = parts[4]; - break; - } - } - } - } - - // Second pass - build the SDP with STUN info - for (const line of filteredLines) { - if (line.startsWith('m=')) { - currentMedia = line.split(' ')[0].substr(2); - if (stunPort && (currentMedia === 'audio' || currentMedia === 'video')) { - // Use STUN port for media lines - const parts = line.split(' '); - parts[1] = stunPort; - processedLines.push(parts.join(' ')); - } else { - processedLines.push(line); - } - } else if (line.startsWith('c=') && stunAddress) { - // Use STUN address for connection lines - processedLines.push(`c=IN IP4 ${stunAddress}`); - } else { - processedLines.push(line); - } - } - - return processedLines.join('\r\n'); - } catch (e) { - errorlog(e); - return sdpString; // Return original on error - } -} - -// If you need to modify the individual candidate filter as well: -function filterStunOnly(candidate) { - if (typeof candidate === 'string') { - return filterStunOnlySDP(candidate); - } - if (!candidate) { return candidate; } - console.log(candidate); - try { - let candidateString = candidate.candidate; - - // Only allow STUN candidates - if (!candidateString.includes("typ srflx")) { - log("🟧 Dropped non-STUN candidate"); - return false; - } - - // Make sure it's not containing private IP ranges - let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/; - if (privateIPPattern.test(candidateString)) { - log("🟧 Dropped STUN candidate with private IP"); - return false; - } - - log("🟢 Allowed STUN candidate"); - return true; - } catch (e) { - errorlog(e); - return false; - } -} - -/** - * IPv6 Preference Handling - * - * These functions help prefer IPv4 over IPv6 for WebRTC ICE candidates. - * This is useful for networks where IPv6 connectivity is flaky (e.g., firewalls - * that drop UDP on IPv6, or unstable IPv6 routing) while IPv4 works reliably. - * - * Default behavior: IPv4 candidates are prioritized (sent first in bundles) - * With &ipv6=0 or &preferipv4: IPv6 candidates are dropped entirely if IPv4 exists - * - * IPv6-only networks continue to work: if no IPv4 candidates exist, IPv6 is used. - */ - -/** - * Detects if an ICE candidate uses IPv6. - * IPv6 addresses contain colons, while IPv4 uses dots. - * Also handles mDNS hostnames (.local) which are treated as non-IPv6. - * - * @param {RTCIceCandidate|string} candidate - The ICE candidate to check - * @returns {boolean} true if the candidate is IPv6, false otherwise - */ -function isIpv6Candidate(candidate) { - if (!candidate) return false; - - try { - var candidateString = (typeof candidate === 'string') - ? candidate - : (candidate.candidate || ''); - - if (!candidateString) return false; - - // Parse the candidate string to extract the address - // Format: "candidate:...
    ..." - var parts = candidateString.split(' '); - if (parts.length < 5) return false; - - var address = parts[4]; - - // mDNS hostnames (.local) are not IPv6 - if (address && address.endsWith('.local')) { - return false; - } - - // IPv6 addresses contain colons, IPv4 uses dots only - // Examples: - // IPv4: "192.168.1.1" - // IPv6: "2001:db8::1", "::1", "fe80::1%eth0" - if (address && address.includes(':')) { - return true; - } - - return false; - } catch (e) { - errorlog("isIpv6Candidate error:", e); - return false; - } -} - -/** - * Filters IPv6 candidates from an SDP string. - * Used when session.disableIpv6 is true and IPv4 candidates exist. - * - * @param {string} sdp - The SDP string to filter - * @param {boolean} hasIpv4 - Whether IPv4 candidates exist in this SDP - * @returns {string} The filtered SDP string - */ -function filterSdpIpv6(sdp, hasIpv4) { - if (!sdp || typeof sdp !== 'string') return sdp; - - // If no IPv4 exists, we must keep IPv6 for connectivity - if (!hasIpv4) { - log("🟡 IPv6 kept in SDP: no IPv4 candidates available (IPv6-only network)"); - return sdp; - } - - try { - var lines = sdp.split('\n'); - var filteredLines = lines.filter(function(line) { - if (line.startsWith('a=candidate:')) { - var parts = line.split(' '); - if (parts.length >= 5) { - var address = parts[4]; - // Drop IPv6 candidates (contain colons, not .local) - if (address && address.includes(':') && !address.endsWith('.local')) { - log("🟧 Dropped IPv6 candidate from SDP: " + address); - return false; - } - } - } - return true; - }); - return filteredLines.join('\n'); - } catch (e) { - errorlog("filterSdpIpv6 error:", e); - return sdp; - } -} - -/** - * Checks if an SDP string contains any IPv4 candidates. - * Used to determine if we can safely filter out IPv6. - * - * @param {string} sdp - The SDP string to check - * @returns {boolean} true if IPv4 candidates exist - */ -function sdpHasIpv4Candidates(sdp) { - if (!sdp || typeof sdp !== 'string') return false; - - try { - var lines = sdp.split('\n'); - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - if (line.startsWith('a=candidate:')) { - var parts = line.split(' '); - if (parts.length >= 5) { - var address = parts[4]; - // IPv4: contains dots, no colons, not .local - if (address && !address.includes(':') && !address.endsWith('.local')) { - return true; - } - // mDNS (.local) counts as non-IPv6, so treat as potential IPv4 - if (address && address.endsWith('.local')) { - return true; - } - } - } - } - } catch (e) { - errorlog("sdpHasIpv4Candidates error:", e); - } - return false; -} - -/** - * Reorders ICE candidates to prioritize IPv4 over IPv6. - * IPv4 candidates are moved to the front of the array. - * This helps ensure IPv4 is tried first when both are available. - * - * @param {Array} candidates - Array of RTCIceCandidate objects - * @returns {Array} Reordered array with IPv4 candidates first - */ -function reorderCandidatesIpv4First(candidates) { - if (!candidates || !Array.isArray(candidates) || candidates.length === 0) { - return candidates; - } - - try { - var ipv4Candidates = []; - var ipv6Candidates = []; - - for (var i = 0; i < candidates.length; i++) { - if (isIpv6Candidate(candidates[i])) { - ipv6Candidates.push(candidates[i]); - } else { - ipv4Candidates.push(candidates[i]); - } - } - - // Return IPv4 first, then IPv6 - return ipv4Candidates.concat(ipv6Candidates); - } catch (e) { - errorlog("reorderCandidatesIpv4First error:", e); - return candidates; - } -} - -/** - * Filters out IPv6 candidates from an array if IPv4 candidates exist. - * Used when session.disableIpv6 is true. - * - * @param {Array} candidates - Array of RTCIceCandidate objects - * @returns {Object} { filtered: Array, hasIpv4: boolean, droppedIpv6: number } - */ -function filterIpv6FromCandidates(candidates) { - var result = { - filtered: [], - hasIpv4: false, - droppedIpv6: 0 - }; - - if (!candidates || !Array.isArray(candidates)) { - return result; - } - - var ipv4Candidates = []; - var ipv6Candidates = []; - - for (var i = 0; i < candidates.length; i++) { - if (isIpv6Candidate(candidates[i])) { - ipv6Candidates.push(candidates[i]); - } else { - ipv4Candidates.push(candidates[i]); - } - } - - result.hasIpv4 = ipv4Candidates.length > 0; - - if (result.hasIpv4) { - // We have IPv4, so we can drop IPv6 - result.filtered = ipv4Candidates; - result.droppedIpv6 = ipv6Candidates.length; - if (ipv6Candidates.length > 0) { - log("🟧 Dropped " + ipv6Candidates.length + " IPv6 candidate(s) since IPv4 is available"); - } - } else { - // No IPv4, must use IPv6 for connectivity - result.filtered = ipv6Candidates; - if (ipv6Candidates.length > 0) { - log("🟡 Using " + ipv6Candidates.length + " IPv6 candidate(s): no IPv4 available (IPv6-only network)"); - } - } - - return result; -} - -/** - * Filters IPv6 candidates from an RTCSessionDescription if needed. - * Used when sending SDP offers/answers to filter embedded candidates. - * - * @param {RTCSessionDescription} description - The description to filter - * @returns {RTCSessionDescription|Object} Filtered description (new object if filtered, original if not) - */ -function filterDescriptionIpv6(description) { - if (!description || !description.sdp) return description; - - // Only filter if disableIpv6 is set - if (!session.disableIpv6) return description; - - try { - var sdp = description.sdp; - - // Check if there are IPv4 candidates - if not, keep IPv6 for connectivity - if (!sdpHasIpv4Candidates(sdp)) { - log("🟡 IPv6 kept in SDP description: no IPv4 candidates (IPv6-only network)"); - return description; - } - - // Filter IPv6 from the SDP - var filteredSdp = filterSdpIpv6(sdp, true); - - // Return new description object with filtered SDP - return { - type: description.type, - sdp: filteredSdp - }; - } catch (e) { - errorlog("filterDescriptionIpv6 error:", e); - return description; - } -} - -async function whepIn(whepInput = false, whepInputToken = false, UUID = false) { - // PLAY WHEP; will continually bring it in, and retry continuously - var candidates = []; - var responseLocation = false; - var acceptPatch = false; - var eTag = false; - var icePwd = false; - var iceUfrag = false; - // var reconnect = null; - //var maxRetries = 5; - //var delay = 2000; - - if (!UUID) { - UUID = "whep_" + session.generateRandomString(25); // fake - } - - whepInput = whepInput || session.whepInput; - if (!whepInput) { - errorlog("no whepInput"); - return; - } - whepInputToken = whepInputToken || session.whepInputToken; - - async function whepConnect() { - //return new Promise((resolve, reject) => { - try { - if (!(UUID in session.rpcs)) { - session.rpcs[UUID] = {}; - } - ensureViewerRpcDefaults(UUID); - - if (!session.configuration) { - await chooseBestTURN(); - } - - if (session.encodedInsertableStreams) { - // most servers won't support this - session.configuration.encodedInsertableStreams = true; - } - - var config = { ...session.configuration }; - - // if (whepInput.includes("cloudflare")){ - // config.iceTransportPolicy = "relay"; // oof. Doesn't work with Cloudflare without this? REVISIT (Update: I guess they fixed it? Oct 23rd its working without it) - // } - - try { - session.rpcs[UUID].whep = new RTCPeerConnection(config); - } catch (err) { - errorlog(err); - if (!session.cleanOutput) { - warnUser("An RTC error occured"); - } - } - - var video = true; - var audio = true; - - if (session.novideo !== false && !session.novideo.includes(session.rpcs[UUID].streamID)) { - video = false; - } else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.video) { - video = false; - } - if (session.noaudio !== false && !session.noaudio.includes(session.rpcs[UUID].streamID)) { - audio = false; - } else if (session.excludeaudio && session.excludeaudio.includes(session.rpcs[UUID].streamID)) { - audio = false; - } else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.audio) { - audio = false; - } - - if (!audio && !video) { - errorlog("We will not request the whep source as no audio or video is requested"); - return; - } - - if (video) { - disableQualityDirector(UUID); - } - - if (!session.manual || !session.director) { - window.onresize = updateMixer; - window.onorientationchange = function () { - setTimeout(updateMixer, 200); - }; - } - - try { - if (video) { - session.rpcs[UUID].whep.addTransceiver("video", { direction: "recvonly" }); - } - if (audio) { - session.rpcs[UUID].whep.addTransceiver("audio", { direction: "recvonly" }); - } - } catch (e) { - errorlog(e); - } - - session.rpcs[UUID].whep.ontrack = function (event) { - warnlog("TRACK INBOUND!"); - warnlog(event); - session.onTrack(event, UUID); - // maxRetries = 5; // reset allowed reconnection limit - let track = null; - if (event.streams && event.streams[0]) { - try { - let newStream = event.streams[0]; - track = newStream.getVideoTracks()[0]; - } catch (e) { } - } else if (event.track && event.track.kind && (event.track.kind == "video")) { - track = event.track - } - if (track) { - log(track); - setTimeout(function (track, UUID) { - if (session.rpcs[UUID] && track && track.id) { - if (session.rpcs[UUID].stats && session.rpcs[UUID].stats[track.id] && ("keyFramesRequested_pli" in session.rpcs[UUID].stats[track.id])) { - // TODO ?? maybe not,b ut console.log("I need to request a keyframe. keyFramesRequested_pli: "+session.rpcs[UUID].stats[track.id].keyFramesRequested_pli); - } - } - }, 6100, track, UUID); // 3 seconds for stats to update + 2 second for keyframe to trigger + 1100 for delay - } - - // IF track is a video.. - // wait to see if a keyframe is received within 2 seconds - // if not keyframe, request one. - // session.whipOutKeyframeOnNewViewer - // session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0; - /* if ("pliCount" in stat) { - data.stats.total_pli_count = stat.pliCount; - } - - if ("keyFramesEncoded" in stat) { - data.stats.total_key_frames_encoded = stat.keyFramesEncoded; - } */ - }; - } catch (err) { - errorlog(err); - if (!session.cleanOutput) { - warnUser("An RTC error occured"); - } - } - - session.rpcs[UUID].whep.onnegotiationneeded = requestStream; // bug: https://groups.google.com/forum/#!topic/discuss-webrtc/3-TmyjQ2SeE - - session.rpcs[UUID].whep.oniceconnectionstatechange = function () { - if (session.rpcs[UUID] && session.rpcs[UUID].whep && (session.rpcs[UUID].whep.iceConnectionState === 'disconnected' || - session.rpcs[UUID].whep.iceConnectionState === 'failed')) { - console.warn("ICE connection failed or disconnected"); - retryWhepConnection(UUID); - } - }; - - session.rpcs[UUID].whep.onconnectionstatechange = function () { - if (session.rpcs[UUID] && session.rpcs[UUID].whep && session.rpcs[UUID].whep.connectionState === 'failed') { - console.warn("Whep connection failed"); - retryWhepConnection(UUID); - } - }; - - session.rpcs[UUID].whep.onicecandidate = function (event) { - if (!session.rpcs[UUID] || !session.rpcs[UUID].whep) { - return; - } - - if (event.candidate == null) { - warnlog("END OF ICE CANDIDATES"); - if (session.rpcs[UUID].whep.iceCompletedCallback) { - session.rpcs[UUID].whep.iceCompletedCallback(); - } - return; - } else if (eTag && icePwd && iceUfrag && acceptPatch && acceptPatch == "application/trickle-ice-sdpfrag" && event.candidate && responseLocation && !session.rpcs[UUID].whep.iceCompletedCallback) { - // "left over" candidates not sent with the SDP offer - log("Send patch request with ice candidate"); - - try { - if (session.localNetworkOnly) { - if (!filterIceLAN(event.candidate)) { - return; - } - } - if (session.stunOnly) { // or whatever flag you want to use - if (!filterStunOnly(event.candidate)) { - return; - } - } - } catch (e) { - errorlog(e); - } - - if (event.candidate.candidate) { - let patchCandidate = - "a=ice-ufrag:" + - iceUfrag + - "\r\n" + // <== what a mess.. https://datatracker.ietf.org/doc/html/draft-murillo-whep - "a=ice-pwd:" + - icePwd + - "\r\n" + - "m=audio 9 RTP/AVP 0\r\n" + - "a=mid:0\r\n" + - "a=" + - event.candidate.candidate + - "\r\n" + - "a=end-of-candidates\r\n"; - - // a=ice-ufrag:EsAw - // a=ice-pwd:P2uYro0UCOQ4zxjKXaWCBui1 - // m=audio RTP/AVP 0 - // a=mid:0 - // a=candidate:1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1 - // a=candidate:3471623853 1 udp 2122194687 198.51.100.1 61765 typ host generation 0 ufrag EsAw network-id 2 - // a=candidate:473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1 - // a=candidate:2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2 - // a=end-of-candidates - - // If-Match: "38sdf4fdsf54:EsAw" - ajax(patchCandidate, "trickle-ice-sdpfrag", false, { "if-match": eTag }); - } - } else { - try { - if (session.localNetworkOnly) { - if (!filterIceLAN(event.candidate)) { - return; - } - } - if (session.stunOnly) { // or whatever flag you want to use - if (!filterStunOnly(event.candidate)) { - return; - } - } - } catch (e) { - errorlog(e); - } - - candidates.push(event.candidate); // send later if I can? - } - //log(event.candidate); - }; - - log("onnegotiationneeded event setup"); - } - - function retryWhepConnection(UUID) { - if (!session.rpcs[UUID]) { - log("Session closed, stopping retry attempts"); - return; - } - - if (session.rpcs[UUID].suppressReconnect) { - session.rpcs[UUID].reconnecting = false; - return; - } - - const parentUUID = session.rpcs[UUID].realUUID || false; - if (parentUUID && session.rpcs[parentUUID] && session.rpcs[parentUUID].screenShareState === false) { - session.rpcs[UUID].reconnecting = false; - return; - } - - if (session.rpcs[UUID].reconnecting) { - return; - } - session.rpcs[UUID].reconnecting = true; - - const maxRetries = 5; - const initialDelay = 2000; - const maxDelay = 20000; - - let currentRetry = 0; - let currentDelay = initialDelay; - - function attemptReconnect() { - if (!session.rpcs[UUID]) { - log("Session closed during retry, stopping attempts"); - return; - } - - if (session.rpcs[UUID].suppressReconnect) { - session.rpcs[UUID].reconnecting = false; - return; - } - - const parentUUID = session.rpcs[UUID].realUUID || false; - if (parentUUID && session.rpcs[parentUUID] && session.rpcs[parentUUID].screenShareState === false) { - session.rpcs[UUID].reconnecting = false; - return; - } - - if (session.rpcs[UUID].whep && - (session.rpcs[UUID].whep.connectionState === 'connected' || - session.rpcs[UUID].whep.iceConnectionState === 'connected' || - session.rpcs[UUID].whep.iceConnectionState === 'completed')) { - log("WHEP connection is already established. No need to reconnect."); - session.rpcs[UUID].reconnecting = false; - return; - } - - log(`Attempting WHEP reconnection (attempt ${currentRetry + 1}/${maxRetries})`); - - if (session.rpcs[UUID].whep && session.rpcs[UUID].whep.close) { - session.rpcs[UUID].whep.close(); - } - - try { - if (session.rpcs[UUID].videoElement && "recorder" in session.rpcs[UUID].videoElement) { - session.rpcs[UUID].videoElement.recorder.stop(); - } - } catch (e) { - warnlog(e); - } - - try { - if (session.rpcs[UUID].streamSrc) { - session.rpcs[UUID].streamSrc.getTracks().forEach(function (track) { - track.stop(); - log("Track stopped"); - }); - session.rpcs[UUID].streamSrc = null; - } - } catch (e) { } - - try { - if (!session.rpcs[UUID].UUID && document.getElementById("container_" + UUID)) { - getById("container_" + UUID).parentNode.removeChild(getById("container_" + UUID)); - updateLockedElements(); - } - } catch (e) { - warnlog(e); - } - - try { - if (session.rpcs[UUID].videoElement) { - session.rpcs[UUID].videoElement.remove(); - session.rpcs[UUID].videoElement = null; - } - if (session.rpcs[UUID].canvas) { - session.rpcs[UUID].canvas.remove(); - } - if (session.rpcs[UUID].imageElement) { - session.rpcs[UUID].imageElement.remove(); - } - if ("eventPlayActive" in session.rpcs[UUID]) { - clearInterval(session.rpcs[UUID].eventPlayActive); - } - - if (!session.director || session.switchMode) { - setTimeout(function () { - updateMixer(); - }, 1); - } - } catch (e) { - warnlog(e); - } - - whepConnect(UUID).then(() => { - log("WHEP reconnection successful"); - session.rpcs[UUID].reconnecting = false; - }).catch(error => { - warnlog("WHEP reconnection failed:", error); - currentRetry++; - if (currentRetry < maxRetries) { - currentDelay = Math.min(currentDelay * 2, maxDelay); - log(`Retrying in ${currentDelay}ms`); - setTimeout(attemptReconnect, currentDelay); - } else { - log("Max retries reached, stopping reconnection attempts"); - session.rpcs[UUID].reconnecting = false; - // Implement user feedback here - } - }); - } - - attemptReconnect(); - } - - var requestingStream = false; - function requestStream(event) { - if (requestingStream) { - log(event); - errorlog("onnegotiationneeded again?"); - return; - } - requestingStream = true; - warnlog("ON NEGO NEEDED"); - warnlog(event); - - try { - session.rpcs[UUID].whep - .createOffer() - .then(async function (offer) { - if (session.localNetworkOnly) { - offer.sdp = filterSDPLAN(offer.sdp); - } - if (session.stunOnly) { // or whatever flag you want to use - offer.sdp = filterStunOnly(offer.sdp); - } - - offer.sdp = processSDPFromServer(offer.sdp); - //offer.sdp = CodecsHandler.setOpusAttributes(offer.sdp, {'stereo': 1}); - return session.rpcs[UUID].whep.setLocalDescription(offer); - }) - .then(async function () { - //log(session.rpcs[UUID].whep.localDescription); - - try { - if (session.whepWait) { - log("Waiting for ice candidates to collect. At least 300ms recommended; at most 30-seconds."); - let startTime = Date.now(); - const { promise, resolve } = sleepCancellable(session.whepWait); // 2000ms default; I want to give the ICE / STUN / TURN time to collect. - session.rpcs[UUID].whep.iceCompletedCallback = resolve; // Can complete earlier if possible. - await promise; // pausing for a moment; until all collected or timed out - log("Finished waiting for ice candidates. Waited " + (Date.now() - startTime) / 1000 + "-seconds"); - delete session.rpcs[UUID].whep.iceCompletedCallback; - } - } catch (e) { - errorlog(e); - } - - // candidates = []; // clear collected candidates so far, as they are part of the localDescription's footer probably - var sdp = session.rpcs[UUID].whep.localDescription.sdp; - sdp = processSDPFromServer(sdp); - - //description.sdp = processSDPFromServer(description.sdp); // cause server to not send - if (sdp.includes("sendrecv")) { - errorlog("Should not include sendrecv"); - sdp = sdp.replace("a=sendrecv", "a=recvonly"); - sdp = sdp.replace("v=sendrecv", "v=recvonly"); - } - ajax(sdp, "sdp"); - }) - .catch(function (err) { - requestingStream = false; - }); - } catch (e) { - requestingStream = false; - errorlog(e); - } - } - - function ajax(dataPayload, type, callback = false, headers = false) { - // https://datatracker.ietf.org/doc/html/draft-murillo-whep - //log(dataPayload); - try { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function () { - if (this.readyState == 4 && (this.status == 200 || this.status == 201)) { - try { - // 200 not in spec (meant to be an options response), but I want to be flexible - let headers = xhttp.getAllResponseHeaders(); - var contentType = false; - if (headers.indexOf("content-type") >= 0) { - contentType = this.getResponseHeader("content-type"); - } - if (headers.indexOf("location") >= 0) { - responseLocation = this.getResponseHeader("location"); - } - if (headers.indexOf("accept-patch") >= 0) { - acceptPatch = this.getResponseHeader("accept-patch"); - } - if (headers.indexOf("etag") >= 0) { - eTag = this.getResponseHeader("etag"); - } - if (responseLocation && !(responseLocation.startsWith("http://") || responseLocation.startsWith("https://"))) { - let requestURL = new URL(whepInput); // Replace 'yourRequestURL' with the URL you posted to. - let protocol = requestURL.protocol; - let hostname = requestURL.hostname; - let port = requestURL.port || (protocol === "https:" ? "443" : "80"); // Default port based on protocol - responseLocation = `${protocol}//${hostname}:${port}${responseLocation}`; - } - if (contentType && contentType.startsWith("application/sdp")) { - var description = {}; - description.sdp = this.responseText; - description.type = "answer"; - warnlog("Processing answer:"); - iceUfrag = description.sdp.match(/a=ice-ufrag:(.*)\r\n/); - if (iceUfrag) { - iceUfrag = iceUfrag[1]; - } - icePwd = description.sdp.match(/a=ice-pwd:(.*)\r\n/); - if (icePwd) { - icePwd = icePwd[1]; - } - if (session.localNetworkOnly) { - description.sdp = filterSDPLAN(description.sdp); - } - if (session.stunOnly) { // or whatever flag you want to use - description.sdp = filterStunOnly(description.sdp); - } - description.sdp = processSDPFromServer(description.sdp); // setup stereo/mono - session.rpcs[UUID].whep - .setRemoteDescription(description) - .then(function () { - warnlog("SHOULD BE CONNECTED?"); - requestingStream = false; - }) - .catch(function (e) { - log(e); - requestingStream = false; - }); - // the request is done, but lets handle any old ice candidates - if (eTag && icePwd && iceUfrag && acceptPatch && acceptPatch == "application/trickle-ice-sdpfrag" && candidates.length && responseLocation && !session.rpcs[UUID].whep.iceCompletedCallback) { - // "left over" candidates not sent with the SDP offer - log("Send patch request with ice candidates"); - let patchCandidates = - "a=ice-ufrag:" + - iceUfrag + - "\r\n" + // <== what a mess.. https://datatracker.ietf.org/doc/html/draft-murillo-whep - "a=ice-pwd:" + - icePwd + - "\r\n" + - "m=audio 9 RTP/AVP 0\r\n" + // if I leave out the port (9), then MediaMTX breaks, but this is not in the draft spec as linked above - "a=mid:0\r\n"; - candidates.forEach(candidate => { - patchCandidates += "a=" + candidate.candidate + "\r\n"; - }); - candidates = []; - patchCandidates += "a=end-of-candidates\r\n"; - // - // If-Match: "38sdf4fdsf54:EsAw" - ajax(patchCandidates, "trickle-ice-sdpfrag", false, { "if-match": eTag }); - } else { - warnlog("Trickling candidates via PATCH requests not supported it seems"); - } - } else if (contentType == "application/error") { - if (!session.cleanOutput) { - warnUser("Unknown WHEP playback error"); - } - } else if (callback) { - callback(); - } - } catch (e) { - requestingStream = false; - } - } else if (this.readyState == 4 && this.status == 204) { - requestingStream = false; - if (type == "trickle-ice-sdpfrag") { - // patch candidate request accepted? - } else { - // not in spec? - } - } else if (this.readyState == 4) { - requestingStream = false; - // console.warn(this.status, this.readyState); - if (this.status == 432) { - } else if (this.status == 405) { - // GET, HEAD or PUT not allowed atm - } else if (this.status == 501) { - } else if (this.status == 412) { - // etag trickle did not match - } - if (!this.status || this.status >= 400) { - if (type === "sdp") { - retryWhepConnection(UUID); - } - } - } - }; - if (type === "trickle-ice-sdpfrag") { - if (responseLocation) { - xhttp.open("PATCH", responseLocation, true); - } else { - xhttp.open("PATCH", whepInput, true); // cause who knows. worth trying? - } - } else { - xhttp.open("POST", whepInput, true); - } - xhttp.setRequestHeader("Content-Type", "application/" + type); - - if (whepInputToken) { - xhttp.setRequestHeader("Authorization", "Bearer " + whepInputToken); // spam it on every request; fail safe - } - - if (headers) { - Object.keys(headers).forEach(key => { - xhttp.setRequestHeader(key, headers[key]); - }); - } - xhttp.onerror = function (e) { - errorlog(e); - requestingStream = false; - if (!session.cleanOutput) { - if (whepInput.startsWith("https://")) { - if (location.protocol !== "https:") { - warnUser("WHEP playback failed.\n\nThe website needs to be loaded via https (ssl) to access media devices."); - } else if ("isSecureContext" in window && window.isSecureContext === false) { - warnUser("WHEP playback failed.\n\nThe website may have assets loaded in an insecure context."); - } else { - retryWhepConnection(UUID); - } - } else { - // vdo.ninja itself is secure - if (location.protocol === "https:") { - if (location.hostname == "vdo.ninja") { - warnUser("WHEP playback failed.\n\nThe WHEP URL needs to be using https if from an SSL-enabled website.\n\nPerhaps try using http://insecure.vdo.ninja instead.", false, false); - } else { - warnUser("WHEP playback failed.\n\nThe WHEP URL needs to be using https if from an SSL-enabled website."); - } - } else if ("isSecureContext" in window && window.isSecureContext === false) { - warnUser("WHEP playback failed.\n\nThe website may have assets loaded in a secure context."); - } else { - retryWhepConnection(UUID); - } - } - } - }; - - xhttp.send(dataPayload); - } catch (e) { - requestingStream = false; - errorlog(e); - } - } - - whepConnect(); - return UUID; -} -//////// -function whepOut() { - // publish to whep.vdo.ninja with obs, to use. experimental - if (!session.whepHost) { - return; - } - warnlog("WHEP Client started"); - - var socket = null; - var connecting = false; - var failedCount = 0; - - function connect() { - clearTimeout(connecting); - if (socket) { - if (socket.readyState === socket.OPEN) { - return; - } - try { - socket.close(); - } catch (e) { } - } - log("Trying to load whep websocket..."); - - socket = new WebSocket("wss://whep.vdo.ninja"); - - socket.onclose = function () { - failedCount += 1; - clearTimeout(connecting); - connecting = setTimeout(function () { - connect(); - }, 100 * (failedCount - 1)); - }; - - socket.onerror = function (e) { - console.error(e); - failedCount += 1; - clearTimeout(connecting); - connecting = setTimeout(function () { - connect(); - }, 100 * failedCount); - }; - - socket.onopen = function () { - failedCount = 0; - try { - var settings = {}; - socket.send(JSON.stringify({ join: session.whepHost })); - } catch (e) { - connecting = setTimeout(function () { - connect(); - }, 1); - } - }; - - socket.addEventListener("message", async function (event) { - if (event.data) { - log(event.data); - var data = JSON.parse(event.data); - - if ("sdp" in data) { - var resp = await processWHEPout(data); - if (resp) { - var ret = {}; - var get = data.get; - data = {}; - if (get) { - data.get = get; - data.result = resp; - ret.callback = data; - log(ret); - socket.send(JSON.stringify(ret)); - } - } - } else if (data.type == "delete") { - warnlog("WHIP Client is actively disconnecting"); - // session.closeRPC(i, true); - var ret = {}; - var get = data.get; - data = {}; - if (get) { - data.get = get; - data.result = "OK"; - ret.callback = data; - log(ret); - socket.send(JSON.stringify(ret)); - } - } - } - }); - } - connect(); -} - -async function processWHEPout(data) { - // LISTEN FOR REMOTE WHIP - - var description = {}; - description.type = "offer"; - - var isGstreamer = false; - description.sdp = data.sdp.replace(/a=rtpmap:111 OPUS\/48000\r\n/g, "a=rtpmap:111 opus/48000/2\r\n"); // gstreamer fix - if (description.sdp !== data.sdp) { - isGstreamer = true; // ugh. i'll need to revisit when gstreamer/whepsrc improves. - } - - var UUID = session.generateRandomString(25) + "_whepout"; // client side made up; just needs to be unique is all; - - if (UUID in session.pcs) { - UUID = session.generateRandomString(25) + "_whepout"; // ha, pretty pointless. - } - - try { - if (!session.configuration) { - await chooseBestTURN(); - } - if (session.encodedInsertableStreams) { - // most servers won't support this - session.configuration.encodedInsertableStreams = true; - } - var config = { ...session.configuration }; - - session.pcs[UUID] = new RTCPeerConnection(config); - - session.pcs[UUID].onicecandidate = event => { - if (event.candidate) { - // Handle ICE candidate.. or not. - } - // try { - // if (session.localNetworkOnly) { - //if (session.localNetworkOnly){ - // if (!filterIceLAN(event.candidate)){ - // return; - // } - // } - // } - // } catch(e) {errorlog(e);} - }; - - if (session.localNetworkOnly) { - description.sdp = filterSDPLAN(description.sdp); - } - if (session.stunOnly) { // or whatever flag you want to use - description.sdp = filterStunOnly(description.sdp); - } - - try { - await session.pcs[UUID].setRemoteDescription(description); - } catch (e) { - errorlog(e); - errorlog("If you are seeing this error, the browser likely isn't able to match what you are requesting. Compare a normal browser-create SDP with what you are offering."); - } - - const transceivers = session.pcs[UUID].getTransceivers(); - - transceivers.forEach(transceiver => { - const direction = transceiver.currentDirection || transceiver.direction; - // Set to sendonly or sendrecv if not already set and if there is a local track to send - if (direction !== "sendrecv" && direction !== "sendonly") { - transceiver.direction = "sendonly"; // or 'sendrecv' if you also want to receive - } else { - return; - } - // Assuming you have a track to send - session.videoElement.srcObject.getAudioTracks().forEach(track => { - if (transceiver.direction.includes("send")) { - if (transceiver.sender.track && transceiver.sender.track.kind != "audio") { - return; - } else if (transceiver.receiver.track && transceiver.receiver.track.kind != "audio") { - return; - } - - transceiver.sender - .replaceTrack(track) - .then(() => { - log(`Added track: ${track.kind}`); - }) - .catch(errorlog); - } - }); - - session.videoElement.srcObject.getVideoTracks().forEach(track => { - if (transceiver.direction.includes("send")) { - if (transceiver.sender.track && transceiver.sender.track.kind != "video") { - return; - } else if (transceiver.receiver.track && transceiver.receiver.track.kind != "video") { - return; - } - - transceiver.sender - .replaceTrack(track) - .then(() => { - log(`Added track: ${track.kind}`); - }) - .catch(errorlog); - } - }); - }); - - //await sleep(300); // give it time to collect ice candidates. too lazy to do a promise callback right now - - const localDescription = await session.pcs[UUID].createAnswer(); - - if (session.localNetworkOnly) { - localDescription.sdp = filterSDPLAN(localDescription.sdp); - } - - if (session.stunOnly) { // or whatever flag you want to use - localDescription.sdp = filterStunOnly(localDescription.sdp); - } - - await session.pcs[UUID].setLocalDescription(localDescription); - - await sleep(session.whepWait); - - if (isGstreamer) { - return session.pcs[UUID].localDescription.sdp.replace("a=rtpmap:111 opus/48000/2\r\n", "a=rtpmap:111 OPUS/48000\r\n"); // not sure if this makes sense tho. - } - - return session.pcs[UUID].localDescription.sdp; - //} - } catch (error) { - console.error("Error setting up the peer connection:", error); - } -} -///// -function pokePostAPI(action, data, streamID) { - var msg = {}; - msg.update = {}; - msg.update.streamID = streamID || session.streamID || null; - msg.update.action = action; - msg.update.value = data; - - try { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function () { - if (this.readyState == 4 && (this.status == 200 || this.status == 201)) { - log("good"); - } else { - warnlog("post api didn't work?"); - } - }; - xhttp.open("POST", session.postApi, true); - xhttp.setRequestHeader("Content-type", "application/json"); - - xhttp.onerror = function (e) { - errorlog(e); - }; - xhttp.send(JSON.stringify(msg)); - } catch (e) { - errorlog(e); - } -} - -var queuedSendingAPIMsgs = []; -function pokeAPI(action, data, streamID = null) { - if (session.postApi) { - pokePostAPI(action, data, streamID); - } - - if (!session.api) { - return; - } - - if (session.apiSocket) { - try { - var msg = {}; - msg.update = {}; - msg.update.streamID = streamID || session.streamID || null; - msg.update.action = action; - msg.update.value = data; - session.apiSocket.send(JSON.stringify(msg)); - } catch (e) { - errorlog(e); - } - } else if (session.apiSocket !== null) { - queuedSendingAPIMsgs.push([action, data, streamID]); - if (queuedSendingAPIMsgs.length > 20) { - queuedSendingAPIMsgs.shift(); - } - } -} - -function pokeDiscord(action, data = {}) { - if (!session || !session.discordHook) { return; } - const { streamID, label, ses, hangup, startTime } = data; - if (hangup && !session.discordHookSensitive) return; - const duration = Math.floor((Date.now() - (startTime || 0)) / 1000); - const message = { - embeds: [{ - title: "Incoming stream disconnected unexpectedly and it didn't reconnect in 60s", - color: 0xFF0000, - fields: [ - { name: "Incoming Stream ID", value: streamID || "Unknown", inline: true }, - { name: "Duration of connection", value: `${duration}s`, inline: true } - ], - timestamp: new Date().toISOString() - }] - }; - if (label) message.embeds[0].fields.push({ name: "Viewer", value: label, inline: true }); - if (session.label) message.embeds[0].fields.push({ name: "Publisher", value: session.label, inline: true }); - setTimeout(() => { - // Check if stream is reconnected by looking through all RPCs - const isReconnected = Object.values(session.rpcs || {}).some(rpc => rpc.streamID === streamID); - if (!isReconnected) { - fetch(session.discordHook, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(message) - }).catch(err => console.error('Discord webhook failed:', err)); - } - }, 60000); -} - -function getGuestTarget(type, id) { - var element = document.querySelector('[data-sid="' + id + '"][data-action-type="' + type + '"], [data-sid="' + id + '"] [data-action-type="' + type + '"]'); // data-sid="P5MQpia" - if (!element) { - return getRightOrderedElement('[data--u-u-i-d] [data-action-type="' + type + '"]', id); - } - return element; -} - -function getGuestTargetScene(scene, id) { - var element = document.querySelector('[data-action-type="addToScene"][data-scene="' + scene + '"][data-sid="' + id + '"], [data-sid="' + id + '"] [data-action-type="addToScene"][data-scene="' + scene + '"]'); // data-sid="P5MQpia" - if (!element) { - return getRightOrderedElement('[data-action-type="addToScene"][data-scene="' + scene + '"][data--u-u-i-d]', id); - } - return element; -} -function getGuestTargetGroup(group, id) { - var element = document.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"][data-sid="' + id + '"], [data-sid="' + id + '"] [data-action-type="toggle-group"][data-group="' + group + '"]'); // data-sid="P5MQpia" - if (!element) { - return getRightOrderedElement('[data-action-type="toggle-group"][data-group="' + group + '"][data--u-u-i-d]', id); - } - return element; -} - -async function targetGuest(target, action, value = null, value2 = null) { - if (target) { - if ((target == (parseInt(target) + "")) && (target < 100)) { - target -= 1; - } - } else { - target = 1; - } - warnlog("target " + target); - warnlog("action " + action); - warnlog("value " + value); - if ((action == 0) || (action == "forward") || (action == "transfer")) { - var element = getGuestTarget("forward", target); - if (element) { - return await directMigrate(element, true, value); // if value is set, it will auto transfer the guest to that room. - } else { - return false; - } - } else if ((action == 1) || (action == "addScene")) { - var scene = 1; - if (value == "null" || value == null || value == "toggle") { - scene = 1; - } else if (value !== true && value !== false) { - scene = value; - } - var element = getGuestTargetScene(scene, target); // oscid/action/target/value 1/1/scene - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directEnable(element, true); // false or true return - } - } else if (action == 2 || action == "muteScene") { - var element = getGuestTarget("mute-scene", target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directMute(element, true); // false/true - } - } else if (action == 3 || action == "mic" || action == "audio") { - var element = getGuestTarget("mute-guest", target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return remoteMute(element, true); // false/true - } - } else if (action == 4 || action == "hangup") { - var element = getGuestTarget("hangup", target); - if (element) { - return directHangup(element, true); // false or true; false if confirmed no - } - } else if (action == 5 || action == "soloChat" || action == "soloTalk") { - // see soloChatBidirectional action=9 for two-way - var element = getGuestTarget("solo-chat", target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return session.toggleSoloChat(element.dataset.UUID); - } - } else if (action == 6 || action == "speaker") { - var element = getGuestTarget("toggle-remote-speaker", target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return remoteSpeakerMute(element); - } - } else if (action == 7 || action == "display") { - var element = getGuestTarget("toggle-remote-display", target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return remoteDisplayMute(element); - } - } else if (action == 8 || action == "group") { - if (value == "null" || value == null) { - value = 1; - } - var element = getGuestTargetGroup(value, target); - if (element) { - return changeGroup(element, null, value); - } - } else if (action == 9 || action == "soloChatBidirectional" || action == "soloTalkBidirectional") { - var element = getGuestTarget("solo-chat", target); - if (element) { - var ctrl = {}; - ctrl.ctrlKey = true; - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return session.toggleSoloChat(element.dataset.UUID, ctrl); - } - } else if (action == 10 || action == "video" || action == "camera") { - var element = getGuestTarget("mute-video-guest", target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return remoteMuteVideo(element, true); // false/true - } - } else if (action == 12 || action == "addScene2") { - var element = getGuestTargetScene(2, target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directEnable(element, true); - } - } else if (action == 13 || action == "addScene3") { - var element = getGuestTargetScene(3, target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directEnable(element, true); - } - } else if (action == 14 || action == "addScene4") { - var element = getGuestTargetScene(4, target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directEnable(element, true); - } - } else if (action == 15 || action == "addScene5") { - var element = getGuestTargetScene(5, target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directEnable(element, true); - } - } else if (action == 16 || action == "addScene6") { - var element = getGuestTargetScene(6, target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directEnable(element, true); - } - } else if (action == 17 || action == "addScene7") { - var element = getGuestTargetScene(7, target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directEnable(element, true); - } - } else if (action == 18 || action == "addScene8") { - var element = getGuestTargetScene(8, target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return directEnable(element, true); - } - } else if (action == 19 || action == "forceKeyframe") { - var element = getGuestTarget("force-keyframe", target); - if (element) { - return requestKeyframeScene(element); - } - } else if (action == 20 || action == "soloVideo") { - var element = getGuestTarget("solo-video", target); - if (element) { - if (value === true) { - element.value = 1; - } else if (value === false) { - element.value = 0; - } - return requestInfocus(element); - } - } else if (action == 21 || action == "sendChat") { - var element = getGuestTarget("solo-video", target); // just something that probably exists. - if (element) { - return sendChat(value, element.dataset.UUID); - } - } else if (action == "pgm" || action == "channel") { - var element = getGuestTarget("isolate-channel", target); // just something that probably exists. - if (element) { - return directIsolateChannel(element.dataset.UUID, (parseInt(value) || null)); - } - } else if (action == 22 || action == "sendDirectorChat") { - var element = getGuestTarget("solo-video", target); // just something that probably exists. - if (element) { - return sendChat(value, element.dataset.UUID, true); - } - } else if (action == "sendPinnedDirectorChat") { - var element = getGuestTarget("solo-video", target); // just something that probably exists. - if (element) { - return sendChat(value, element.dataset.UUID, 2); - } - } else if (action == 27 || action == "volume") { - var element = getGuestTarget("volume", target); - if (element) { - element.value = parseInt(value) || 0; - return remoteVolume(element); - } - } else if ((action == 28) || (action == "setslot")) { - var element = getGuestTarget("setslot", target); - if (element) { - return setSlot(element, value); - } else { - return false; - } - } else if (action == 29 || action == "mixorder") { - var element = getGuestTarget("order-down", target); - if (element) { - if (value === true) { - changeOrder(+1, element.dataset.UUID); - } else if (value === false) { - changeOrder(-1, element.dataset.UUID); - } else { - changeOrder(value, element.dataset.UUID); - } - return true; - } else { - return false; - } - } else if (action == "requestResolution") { // director's preview or scene preview or s/e; not capture resolution - var element = getGuestTarget("solo-video", target); // just need to find the guest - if (element) { - let resolution = value.split("x"); - if (resolution.length == 2) { - session.requestResolution(element.dataset.UUID, parseInt(resolution[0]), parseInt(resolution[1])); - return true; - } else { - return "Failed. Must be WIDTHxHEIGHT"; - } - - } - return false; - } else if (action == "setWidth") { // actual capture resolution ; director only - var element = getGuestTarget("solo-video", target); // just need to find the guest - if (element) { - requestVideoHack("width", parseInt(value), element.dataset.UUID); - return true; - } - return false; - } else if (action == "setHeight") { - var element = getGuestTarget("solo-video", target); // just need to find the guest - if (element) { - requestVideoHack("height", parseInt(value), element.dataset.UUID); - return true; - } - return false; - } else if (action == "setAspectRatio") { - var element = getGuestTarget("solo-video", target); // just need to find the guest - if (element) { - requestVideoHack("aspectRatio", parseFloat(value), element.dataset.UUID); - return true; - } - return false; - } else if (action == "requestAspectRatio") { - var element = getGuestTarget("solo-video", target); // just need to find the guest - if (element) { - let maxDimension = parseInt(value2) || 1920; - let aspectRatio = 16 / 9; // default - - // Parse aspect ratio - if (value) { - if (value.includes(":")) { - let parts = value.split(":"); - aspectRatio = parseFloat(parts[0]) / parseFloat(parts[1]); - } else { - aspectRatio = parseFloat(value); - } - } - - // Calculate dimensions - let width, height; - if (aspectRatio >= 1) { - width = maxDimension; - height = Math.round(maxDimension / aspectRatio); - } else { - height = maxDimension; - width = Math.round(maxDimension * aspectRatio); - } - - session.requestResolution(element.dataset.UUID, width, height); - return true; - } - return false; - } else if (action == "startRoomTimer") { - var element = getGuestTarget("create-timer", target); - if (element) { - element.value = 0; - return directTimer(element, false, value); - } - } else if (action == "pauseRoomTimer") { - var element = getGuestTarget("create-timer", target); - if (element) { - if (element.value == 3) { - return directTimer(element, { ctrlKey: true }); - } else { - return directTimer(element, { ctrlKey: true }); - } - } - } else if (action == "stopRoomTimer") { - var element = getGuestTarget("create-timer", target); - if (element) { - element.value = 1; - return directTimer(element); - } - } else if (Commands[action]) { - try { - return Commands[action](value, target); - } catch (e) { - errorlog(e); - } - } - return false; -} - -function oscClient() { - // api.vdo.ninja api OSC (websocket / https API hotkey support). The iFrame API method provides greater customization. - if (!session.api) { - return; - } - warnlog("oscClient started"); - - var socket = null; - var connecting = false; - var failedCount = 0; - - function connect() { - clearTimeout(connecting); - if (socket) { - if (socket.readyState === socket.OPEN) { - return; - } - try { - socket.close(); - } catch (e) { } - } - socket = new WebSocket(session.apiserver); - - socket.onclose = function () { - session.apiSocket = false; - failedCount += 1; - clearTimeout(connecting); - connecting = setTimeout(function () { - connect(); - }, 100 * (failedCount - 1)); - }; - - socket.onerror = function () { - failedCount += 1; - clearTimeout(connecting); - connecting = setTimeout(function () { - connect(); - }, 100 * failedCount); - }; - - socket.onopen = function () { - failedCount = 0; - try { - socket.send(JSON.stringify({ join: session.api })); - session.apiSocket = socket; - if (queuedSendingAPIMsgs.length) { - queuedSendingAPIMsgs.forEach(msg => { - pokeAPI(msg[0], msg[1], msg[2]); - }); - queuedSendingAPIMsgs = []; - } - pokeAPI("details", getDetailedState(session.streamID)); - } catch (e) { - connecting = setTimeout(function () { - connect(); - }, 1); - } - }; - - socket.addEventListener("message", async function (event) { - if (event.data) { - var data = JSON.parse(event.data); - - if ("msg" in data) { - data = data.msg; - } - - if ("value" in data) { - if ("action" in data && data.action == "layout") { - data.value = safelyDecodeValue(data.value); - } - } - - var resp = await processMessage(data); - if (resp !== null) { - var ret = {}; - data.result = resp; - ret.callback = data; - log(ret); - socket.send(JSON.stringify(ret)); - } - } - }); - } - connect(); -} - -function safelyDecodeValue(value) { // since the layout can be a number, json, or base64 encoded json - if (Number.isInteger(Number(value))) { - return parseInt(value, 10); - } - try { - return JSON.parse(value); // Try parsing as JSON - } catch (e) { - } - try { - const decodedValue = atob(value); // Try decoding as base64 instead - try { - return JSON.parse(decodedValue); // Try parsing as JSON now that we did b64 decoding - } catch (e) { - return decodedValue; - } - } catch (e) { - return value; - } -} - -function setupCommands() { - var commands = {}; - - commands.raisehand = function (value = null, value2 = null) { - return raisehand(); - }; - commands.togglehand = function (value = null, value2 = null) { - return raisehand(); - }; - commands.togglescreenshare = function (value = null, value2 = null) { - toggleScreenShare(); - return session.screenShareState; - }; - commands.chat = function (value = null, value2 = null) { - toggleChat(value); - return session.chat; - }; - commands.speaker = function (value = null, value2 = null) { - if (value === true) { - // unmute - session.speakerMuted = false; // set - toggleSpeakerMute(true); // apply - } else if (value === false) { - // mute - session.speakerMuted = true; // set - toggleSpeakerMute(true); // apply - } else if (value === "toggle") { - // toggle - toggleSpeakerMute(); - } - return session.speakerMuted; - }; // mute speaker - commands.mic = function (value = null, value2 = null) { - if (value === true) { - // unmute - session.muted = false; // set - log(session.muted); - toggleMute(true); // apply - } else if (value === false) { - // mute - session.muted = true; // set - log(session.muted); - toggleMute(true); // apply - } else if (value === "toggle") { - // toggle - toggleMute(); - } - return session.muted; - }; - commands.camera = function (value = null, value2 = null) { - if (value === true) { - // unmute - session.videoMuted = false; // set - log(session.videoMuted); - toggleVideoMute(true); // apply - } else if (value === false) { - // mute - session.videoMuted = true; // set - log(session.videoMuted); - toggleVideoMute(true); // apply - } else if (value === "toggle") { - // toggle - toggleVideoMute(); - } - return session.videoMuted; - }; - commands.video = function (value = null, value2 = null) { - if (value === true) { - // unmute - session.videoMuted = false; // set - log(session.videoMuted); - toggleVideoMute(true); // apply - } else if (value === false) { - // mute - session.videoMuted = true; // set - log(session.videoMuted); - toggleVideoMute(true); // apply - } else if (value === "toggle") { - // toggle - toggleVideoMute(); - } - return session.videoMuted; - }; - commands.hangup = function (value = null, value2 = null) { - hangup(); - return true; - }; - commands.bitrate = function (value = null, value2 = null) { - if (value === false) { - value = 0; - } else if (value === true) { - value = -1; - } else { - value = parseInt(value) || 0; - } - for (var i in session.rpcs) { - try { - session.requestRateLimit(value, i); - } catch (e) { - errorlog(e); - } - } - return value; - }; - - commands.requestStats = function (value = null, value2 = null) { - var myStats = { ...session.stats }; - - myStats.streamID = session.streamID; - - if (session.whipOut && session.whipOut.stats) { - myStats.whipStats = session.whipOut.stats; - } - if (session.whepIn && session.whepIn.stats) { - myStats.whepStats = session.whepIn.stats; - } - - myStats.pcs = {}; - myStats.rpcs = {}; - - for (var uuid in session.pcs) { - myStats.pcs[uuid] = session.pcs[uuid].stats; - } - for (var uuid in session.rpcs) { - myStats.rpcs[uuid] = session.rpcs[uuid].stats; - myStats.rpcs[uuid].streamID = session.rpcs[uuid].streamID; - } - - return myStats; - }; - - commands.getDetails = function (value = null, value2 = null) { - return getDetailedState(value); - }; - - commands.getStats = function (value = null, value2 = null) { - return getQuickStats(value); - }; - - commands.getGuestList = function (value = null, value2 = null) { - return getGuestList(); - }; - - commands.reload = function (value = null, value2 = null) { - reloadRequested(); - return true; - }; - commands.volume = function (value = null, value2 = null) { - if (value === false) { - value = 0; - } else if (value === true) { - value = 100; - } else { - value = parseInt(value) || 0; - } - value = parseFloat(value / 100); - for (var i in session.rpcs) { - try { - session.rpcs[i].videoElement.volume = parseFloat(value); - } catch (e) { - errorlog(e); - } - } - return value; - }; - - commands.forceKeyframe = function (value = null, value2 = null) { - return session.forcePLI(); - }; - - commands.panning = function (value = null, value2 = null) { - if (value === false) { - value = 90; - } else if (value === true) { - value = 90; - } else { - value = parseInt(value); - } - for (var uuid in session.rpcs) { - try { - adjustPan(uuid, value); // &panning needs to be added to enable. playback only; not mic out. - } catch (e) { - errorlog(e); - } - } - return value; - }; - - commands.record = function (value = null, value2 = null) { - if (!session.videoElement) { - return; - } - - if (value === false) { - // mute - if ("recording" in session.videoElement) { - recordLocalVideo("stop"); - } - } else if (value === true) { - if ("recording" in session.videoElement) { - // already recording - } else { - recordLocalVideo("start"); - } - } - return value; - }; - - commands.group = function (value = null, value2 = null) { - if (value && value !== "null") { - return changeGroupDirectorAPI(value); - } - return false; - }; - - commands.joinGroup = function (value = null, value2 = null) { - if (value && value !== "null") { - return changeGroupDirectorAPI(value, true); - } - return false; - }; - - commands.leaveGroup = function (value = null, value2 = null) { - if (value && value !== "null") { - return changeGroupDirectorAPI(value, false); - } - return false; - }; - - commands.viewGroup = function (value = null, value2 = null) { - if (value && value !== "null") { - return changeGroupViewDirectorAPI(value); - } - return false; - }; - - commands.joinViewGroup = function (value = null, value2 = null) { - if (value && value !== "null") { - return changeGroupViewDirectorAPI(value, true); - } - return false; - }; - - commands.leaveViewGroup = function (value = null, value2 = null) { - if (value && value !== "null") { - return changeGroupViewDirectorAPI(value, false); - } - return false; - }; - - commands.sendChat = function (value = null, value2 = null) { - sendChat(value); - // sendChatMessage // this would add it to the chat message - return true; - }; - - commands.sendChatMessage = function (value = null, value2 = null) { - sendChatMessage(value); - return true; - }; - - commands.showChatOverlay = function (value = null, value2 = null) { - getChatMessage(value, false, false, true); - return true; - }; - - commands.startRoomTimer = function (value = null, value2 = null) { - getById("globalTimerDirectorToggle").value = 0; // reset - directRoomTimer(getById("globalTimerDirectorToggle"), false, value); - return true; - }; - - commands.pauseRoomTimer = function (value = null, value2 = null) { - if (getById("globalTimerDirectorToggle").value == 3) { - directRoomTimer(getById("globalTimerDirectorToggle"), { ctrlKey: true }, value); - } else { - directRoomTimer(getById("globalTimerDirectorToggle"), { ctrlKey: true }, value); - } - return true; - }; - - commands.stopRoomTimer = function (value = null, value2 = null) { - getById("globalTimerDirectorToggle").value = 1; // pause - directRoomTimer(getById("globalTimerDirectorToggle"), false, value); - return true; - }; - - commands.tallylight = function (value = null, value2 = null) { - if (value == "onair") { - session.tallyOverride = 1; - } else if (value == "active") { - session.tallyOverride = 2; - } else if (value == "standby") { - session.tallyOverride = 3; - } else if (value == "false") { - session.tallyOverride = false; - } else if (value == "off") { - session.tallyOverride = false; - } else if (value) { - session.tallyOverride = parseInt(value) || 0; - } else { - session.tallyOverride = 0; - } - applySceneState(); - return true; - }; - - commands.prevSlide = function (value = null, value2 = null) { - var data = {}; - data.d = [176, 110, 10]; - playbackMIDI(data); - return true; - }; - - commands.nextSlide = function (value = null, value2 = null) { - var data = {}; - data.d = [176, 110, 11]; - playbackMIDI(data); - return true; - }; - - commands.zoom = function (value = null, value2 = null) { - if (value !== null) { - const zoomValue = parseFloat(value); - const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; - session.remoteZoom(zoomValue, isAbsolute); - return { zoom: zoomValue, absolute: isAbsolute }; - } - return false; - }; - - commands.focus = function (value = null, value2 = null) { - if (value !== null) { - const focusValue = parseFloat(value); - const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; - session.remoteFocus(focusValue, isAbsolute); - return { focus: focusValue, absolute: isAbsolute }; - } - return false; - }; - - commands.pan = function (value = null, value2 = null) { - if (value !== null) { - const panValue = parseFloat(value); - const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; - session.remotePan(panValue, isAbsolute); - return { pan: panValue, absolute: isAbsolute }; - } - return false; - }; - - commands.tilt = function (value = null, value2 = null) { - if (value !== null) { - const tiltValue = parseFloat(value); - const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; - session.remoteTilt(tiltValue, isAbsolute); - return { tilt: tiltValue, absolute: isAbsolute }; - } - return false; - }; - - commands.exposure = function (value = null, value2 = null) { - if (value !== null) { - const exposureValue = parseFloat(value); - const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; - session.remoteExposure(exposureValue, isAbsolute); - return { exposure: exposureValue, absolute: isAbsolute }; - } - return false; - }; - - commands.soloVideo = function (value = null, value2 = null) { - var element = getById("highlightDirector"); - if (value && value == "toggle") { - return requestInfocus(element); - } else if (value && value !== "null") { - return requestInfocus(element, null, true); - } else if (value && value === "null") { - return requestInfocus(element); - } else { - return requestInfocus(element, null, false); - } - return false; - }; - commands.highlight = function (value = null, value2 = null) { - return commands.soloVideo(value, value2); - }; - - commands.activeSpeaker = function (value = null, value2 = null) { - var res = {}; - res.previous = session.activeSpeaker; - if ((value && (value == "toggle" || value == "null")) || (value === null)) { - res.action = "toggle"; - if (session.activeSpeaker) { - session.activeSpeaker = false; - } else { - session.activeSpeaker = urlParams.get("activespeaker") || urlParams.get("speakerview") || urlParams.get("sas") || 1; - } - } else if (value && parseInt(value)) { - session.activeSpeaker = parseInt(value) || 1; - } else { - session.activeSpeaker = false; - } - if (session.activeSpeaker && !session.activeSpeakerInterval) { - if (!session.audioEffects) { - session.audioEffects = true; - for (var UUID in session.rpcs) { - updateIncomingAudioElement(UUID); - } - } - activeSpeaker(false); - session.activeSpeakerInterval = setInterval(function () { - activeSpeaker(false); - }, 100); - } else { - updateMixer(); - } - res.current = session.activeSpeaker; - res.value = value; - return res; - }; - - commands.setBufferDelay = function (value = null, value2 = null) { - let delay = parseInt(value) || 0; - - if (!value2) { - session.buffer = delay; - } else if (value2 === "*") { - for (var uuid in session.rpcs) { - session.rpcs[uuid].buffer = delay; - playoutdelay(uuid); - document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + uuid + '"] input[data-buffer-value]').forEach(ele => { - ele.value = delay; - }); - return true; - } - } else if (value2 in session.rpcs) { - session.rpcs[value2].buffer = delay; - playoutdelay(value2); - document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + value2 + '"] input[data-buffer-value]').forEach(ele => { - ele.value = delay; - }); - return true; - } else if (value2) { - let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === value2); - if (session.rpcs[UUID]) { - session.rpcs[UUID].buffer = delay; - playoutdelay(UUID); - document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + UUID + '"] input[data-buffer-value]').forEach(ele => { - ele.value = delay; - }); - return true; - } else { - errorlog("The stream ID specified does not exist: " + value2); - } - } - return false; - }; - - commands.layout = function (value = null, value2 = null) { - let response = {}; - response.input = value; - - try { - if (parseInt(value) == value) { - log(value); - value = parseInt(value); - if (value == 0) { - value = false; // Auto mixer - } else { - value -= 1; // Adjust index to match layouts array (1-based to 0-based) - } - response.index = value; - } else if (checkType(value) === "Array") { - log(value); - session.layout_array = value; - if (session.layout_array) { - session.layout = combinedLayout(session.layout_array); - } - updateMixer(); - - if (session.director) { - issueLayout("0"); - response.issued = true; - response.scene = "0"; - pokeIframeAPI("layout-updated", value); - } - - response.type = 1; - updateMixer(); - response.combined_layout = session.layout; - var temp = previousDebug; - previousDebug = response - return { response: response, previous: temp } - } else if (checkType(value) === "Object") { - log(value); - session.layout = value; - if (session.director) { - issueLayout("0"); - response.issued = true; - response.scene = "0"; - pokeIframeAPI("layout-updated", session.layout); - } - response.type = 2; - updateMixer(); - response.layout = session.layout; - var temp = previousDebug; - previousDebug = response - return { response: response, previous: temp } - } - - if (value === null) { - session.layout = false; - pokeIframeAPI("layout-updated", session.layout); - pokeIframeAPI("layout-index", 0); - if (session.director) { - issueLayout("0"); - response.issued = true; - response.scene = "0"; - - } - response.type = 3; - response.layout = session.layout; - updateMixer(); - var temp = previousDebug; - previousDebug = response - return { response: response, previous: temp } - } else if (value === false) { - session.layout = false; - pokeIframeAPI("layout-updated", session.layout); - pokeIframeAPI("layout-index", 0); - if (session.director) { - issueLayout("0"); - response.issued = true; - response.scene = "0"; - } - response.layout = session.layout; - response.type = 3; - updateMixer(); - var temp = previousDebug; - previousDebug = response - return { response: response, previous: temp } - } else if (session.layouts && session.layouts[value]) { - try { - log(session.layouts); - var temp = session.layouts[value]; - response.type = 4; - if (session.director) { - var combined = {}; - for (var i = 0; i < temp.length; i++) { - if (!temp[i]) continue; - let streamID = null; - // First check if there's a slot assigned - if ("slot" in temp[i]) { - const slotNumber = parseInt(temp[i].slot) + 1; - streamID = session.currentSlots[slotNumber]; - } - // If no stream found via slot, check defaultStreamID - if (!streamID && temp[i].defaultStreamID) { - // Check if this defaultStreamID is connected and not assigned to another slot - let isConnected = false; - let isAlreadyAssigned = false; - for (let j in session.rpcs) { - if (session.rpcs[j].streamID === temp[i].defaultStreamID) { - isConnected = true; - // Check if this stream is assigned to any slot - for (let slot in session.currentSlots) { - if (session.currentSlots[slot] === temp[i].defaultStreamID) { - isAlreadyAssigned = true; - break; - } - } - break; - } - } - if (isConnected && !isAlreadyAssigned) { - streamID = temp[i].defaultStreamID; - } - } - // If we found a streamID, use it, otherwise add to empty slot - if (streamID) { - combined[streamID] = temp[i]; - } else { - if (!combined[""]) combined[""] = []; - combined[""].push(temp[i]); - } - } - session.layout = combined; - log("issuing layout:"); - log(session.layout); - issueLayout("0"); - response.issued = true; - response.scene = "0"; - response.combined_layout = session.layout; - pokeIframeAPI("layout-updated", session.layout); - pokeIframeAPI("layout-index", value + 1); - } - } catch (e) { - errorlog(e); - response.error = e.message - } - updateMixer(); - temp = previousDebug; - previousDebug = response - return { response: response, previous: temp } - } else { - errorlog("no layout found"); - log(session.layouts); - var temp = previousDebug; - previousDebug = response - return { response: response, previous: temp } - } - } catch (e) { - response.error = e.message - errorlog(e); - } - var temp = previousDebug; - previousDebug = response - return { response: response, previous: temp } - }; - - commands.width = function (value = null, value2 = null) { - // affects LOCAL camera width - let width = value ? parseInt(value) : null; - if (width) { - updateCameraConstraints("width", width, false, false); - return true; - } - return false; - }; - - commands.height = function (value = null, value2 = null) { - // affects LOCAL camera height - let height = value ? parseInt(value) : null; - if (height) { - updateCameraConstraints("height", height, false, false); - return true; - } - return false; - }; - - commands.aspectRatio = function (value = null, value2 = null) { - // affects LOCAL camera aspect ratio - if (!value) return false; - - let aspectRatio; - if (typeof value === 'string' && value.includes(":")) { - let parts = value.split(":"); - aspectRatio = parseFloat(parts[0]) / parseFloat(parts[1]); - } else { - aspectRatio = parseFloat(value); - } - - if (aspectRatio && !isNaN(aspectRatio)) { - updateCameraConstraints("aspectRatio", aspectRatio, false, false); - return true; - } - return false; - }; - - commands.videoConstraint = function (value = null, value2 = null) { - // Generic video constraint setter for LOCAL camera - // Usage: action=videoConstraint&value=CONSTRAINT_NAME&value2=CONSTRAINT_VALUE - if (!value || value2 === null || value2 === undefined) return false; - - // Parse value2 based on common types - let constraintValue = value2; - - // Handle boolean strings - if (value2 === "true") { - constraintValue = true; - } else if (value2 === "false") { - constraintValue = false; - } else if (value2 == parseFloat(value2)) { - // Handle numeric values - constraintValue = parseFloat(value2); - } - - // Special handling for aspectRatio with colon notation - if (value === "aspectRatio" && typeof value2 === 'string' && value2.includes(":")) { - let parts = value2.split(":"); - constraintValue = parseFloat(parts[0]) / parseFloat(parts[1]); - } - - // Apply the constraint - updateCameraConstraints(value, constraintValue, false, false); - return true; - }; - - return commands; -} -var Commands = setupCommands(); - -var previousDebug = {}; - -function checkType(value) { - if (Array.isArray(value)) { - return 'Array'; - } else if (typeof value === 'object' && value !== null) { - return 'Object'; - } else { - return 'Neither an Array nor an Object'; - } -} -async function processMessage(data) { - // api.vdo.ninja/apikey/action/value - try { - warnlog(data); - if ("target" in data && data.target !== "null" && data.target !== null) { - if ("action" in data) { - if ("value" in data && data.value !== "null" && data.value !== null) { - return await targetGuest(data.target, data.action, data.value, data.value2 || null); - } else { - return await targetGuest(data.target, data.action, null); - } - } - } else if ("action" in data && data.action !== "null" && data.action !== null) { - if (data.action in Commands) { - if ("value" in data && data.value !== "null" && data.value !== null) { - if (data.value == "true") { - data.value = true; - } else if (data.value == "false") { - data.value = false; - } - return Commands[data.action](data.value, data.value2 || null); - } else { - return Commands[data.action](); - } - } - } - } catch (e) { - errorlog(e); - } - return null; -} - -function midiHotkeysNote(note, velocity = false) { - if (session.midiHotkeys == 1) { - if (note == "G3") { - // open and close the chat window - toggleChat(); - return session.chat; - } else if (note == "A3") { - // mute your audio output - toggleMute(); - return session.muted; - } else if (note == "B3") { - // mute your video output - toggleVideoMute(); - return session.videoMuted; - } else if (note == "C4") { - // enable / disable screenshare - toggleScreenShare(); - return session.screenShareState; - } else if (note == "D4") { - // completely kill your connection/session - hangup(); - return true; - } else if (note == "E4") { - // raise your hand; director sees this - raisehand(); - return raisehand(); - } else if (note == "F4") { - // start/stop local recording - return recordLocalVideoToggle(); - } else if (note == "G4") { - // Director Enables their Audio output - press2talk(true); - return true; - } else if (note == "A4") { - // Director cut's their audio/video output - hangup2(); - return true; - } else if (note == "B4") { - // toggle speaker - toggleSpeakerMute(); - return session.speakerMuted; - } - } else if (session.midiHotkeys == 2) { - if (note == "G1") { - // open and close the chat window - toggleChat(); - } else if (note == "A1") { - // mute your audio output - toggleMute(); - } else if (note == "B1") { - // mute your video output - toggleVideoMute(); - } else if (note == "C2") { - // enable / disable screenshare - toggleScreenShare(); - } else if (note == "D2") { - // completely kill your connection/session - hangup(); - } else if (note == "E2") { - // raise your hand; director sees this - raisehand(); - } else if (note == "F2") { - // start/stop local recording - recordLocalVideoToggle(); - } else if (note == "G2") { - // Director Enables their Audio output - press2talk(true); - } else if (note == "A2") { - // Director cut's their audio/video output - hangup2(); - } else if (note == "B2") { - // toggle speaker - toggleSpeakerMute(); - } - } else if (session.midiHotkeys == 3) { - if (note == "C1") { - if (velocity == "0") { - // open and close the chat window - toggleChat(); - } else if (velocity == "1") { - // mute your audio output - toggleMute(); - } else if (velocity == "2") { - // mute your video output - toggleVideoMute(); - } else if (velocity == "3") { - // enable / disable screenshare - toggleScreenShare(); - } else if (velocity == "4") { - // completely kill your connection/session - hangup(); - } else if (velocity == "5") { - // raise your hand; director sees this - raisehand(); - } else if (velocity == "6") { - // start/stop local recording - recordLocalVideoToggle(); - } else if (velocity == "7") { - // Director Enables their Audio output - press2talk(true); - } else if (velocity == "8") { - // Director cut's their audio/video output - hangup2(); - } else if (velocity == "9") { - // toggle speaker - toggleSpeakerMute(); - } - } - } - /* if (velocity !== false && typeof velocity !== "undefined") { - // Get integer value of velocity - const velocityValue = parseInt(velocity); - - // Check if valid MIDI velocity (0-127) - if (!isNaN(velocityValue) && velocityValue >= 0 && velocityValue <= 127) { - // Camera control MIDI commands using Channel 1, various CC numbers - if (note == "C5") { - // Zoom - scale 0-127 to percentage or use relative value - const normalizedValue = velocityValue / 127; // 0 to 1 range - session.remoteZoom(normalizedValue, true); // absolute value - return { zoom: normalizedValue, absolute: true }; - } else if (note == "D5") { - // Focus - scale 0-127 to focus value - const normalizedValue = velocityValue / 127; // 0 to 1 range - session.remoteFocus(normalizedValue); - return { focus: normalizedValue }; - } else if (note == "E5") { - // Pan - scale 0-127 to pan value - const normalizedValue = (velocityValue - 64) / 64; // -1 to 1 range - session.remotePan(normalizedValue); - return { pan: normalizedValue }; - } else if (note == "F5") { - // Tilt - scale 0-127 to tilt value - const normalizedValue = (velocityValue - 64) / 64; // -1 to 1 range - session.remoteTilt(normalizedValue); - return { tilt: normalizedValue }; - } else if (note == "G5") { - // Exposure - scale 0-127 to exposure value - const normalizedValue = velocityValue / 127; // 0 to 1 range - session.remoteExposure(normalizedValue); - return { exposure: normalizedValue }; - } - } - } */ -} - -function getRightOrderedElement(selector, guestslot, UUID = false) { - var elements = getById("guestFeeds").children; - if (!UUID) { - for (var i = 0; i < elements.length; i++) { - try { - UUID = elements[i].UUID; - var lock = parseInt(document.getElementById("position_" + UUID).dataset.locked); - if (lock && lock == guestslot + 1) { - return elements[i].querySelector(selector) || false; - } - } catch (e) { } - } - } - - if (elements[guestslot]) { - return elements[guestslot].querySelector(selector) || false; - } else { - return false; - } -} - -function midiHotkeysCommand_offset(command, value, offset = 1) { - for (var i = 0; i < 9; i++) { - if (command == offset + i) { - var ele = getRightOrderedElement('[data-action-type="mute-guest"][data--u-u-i-d]', command - offset); - if (ele) { - remoteMute(ele, true); - } - } - } -} - -function midiHotkeysCommand(command, value) { - if (command == 110) { - // Existing controls 0-8, 10-11 remain unchanged - if (value == 0) { - toggleChat(); - } else if (value == 1) { - toggleMute(); - } else if (value == 2) { - toggleVideoMute(); - } else if (value == 3) { - toggleScreenShare(); - } else if (value == 4) { - hangup(); - } else if (value == 5) { - raisehand(); - } else if (value == 6) { - recordLocalVideoToggle(); - } else if (value == 7) { - press2talk(true); - } else if (value == 8) { - hangup2(); - } - // 10 & 11 reserved for PPT slides - - // Camera controls - relative adjustments - else if (value == 20) { - // Zoom in (relative +10%) - Commands.zoom(0.1); - } else if (value == 21) { - // Zoom out (relative -10%) - Commands.zoom(-0.1); - } else if (value == 22) { - // Pan left (relative -10%) - Commands.pan(-0.1); - } else if (value == 23) { - // Pan right (relative +10%) - Commands.pan(0.1); - } else if (value == 24) { - // Tilt up (relative +10%) - Commands.tilt(0.1); - } else if (value == 25) { - // Tilt down (relative -10%) - Commands.tilt(-0.1); - } else if (value == 26) { - // Exposure increase (relative +10%) - Commands.exposure(0.1); - } else if (value == 27) { - // Exposure decrease (relative -10%) - Commands.exposure(-0.1); - } else if (value == 28) { - // Focus near (relative -10%) - Commands.focus(-0.1); - } else if (value == 29) { - // Focus far (relative +10%) - Commands.focus(0.1); - } - - // Camera presets - absolute positions - else if (value == 30) { - // Camera preset 1: Center position - Commands.zoom(1.0, "abs"); - Commands.pan(0, "abs"); - Commands.tilt(0, "abs"); - } else if (value == 31) { - // Camera preset 2: Wide shot - Commands.zoom(0.5, "abs"); - Commands.pan(0, "abs"); - Commands.tilt(0, "abs"); - } else if (value == 32) { - // Camera preset 3: Close-up - Commands.zoom(2.0, "abs"); - Commands.pan(0, "abs"); - Commands.tilt(0, "abs"); - } - - } else if (command > 110) { - // Existing guest slot controls remain unchanged - var guestslot = command - 111; - if (value == 0) { - var ele = getRightOrderedElement('[data-action-type="forward"][data--u-u-i-d]', guestslot); - if (ele) { - directMigrate(ele, true); - } - } else if (value == 1) { - var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="1"][data--u-u-i-d]', guestslot); - if (ele) { - directEnable(ele, true); - } - } else if (value == 2) { - var ele = getRightOrderedElement('[data-action-type="mute-scene"][data--u-u-i-d]', guestslot); - if (ele) { - directMute(ele, true); - } - } else if (value == 3) { - var ele = getRightOrderedElement('[data-action-type="mute-guest"][data--u-u-i-d]', guestslot); - if (ele) { - remoteMute(ele, true); - } - } else if (value == 4) { - var ele = getRightOrderedElement('[data-action-type="hangup"][data--u-u-i-d]', guestslot); - if (ele) { - directHangup(ele, true); - } - } else if (value == 5) { - var ele = getRightOrderedElement('[data-action-type="solo-chat"][data--u-u-i-d]', guestslot); - if (ele) { - session.toggleSoloChat(ele.dataset.UUID); - } - } else if (value == 6) { - var ele = getRightOrderedElement('[data-action-type="toggle-remote-speaker"][data--u-u-i-d]', guestslot); - if (ele) { - remoteSpeakerMute(ele); - } - } else if (value == 7) { - var ele = getRightOrderedElement('[data-action-type="toggle-remote-display"][data--u-u-i-d]', guestslot); - if (ele) { - remoteDisplayMute(ele); - } - } else if (value == 8) { - var ele = getRightOrderedElement('[data-action-type="force-keyframe"][data--u-u-i-d]', guestslot); - if (ele) { - requestKeyframeScene(ele); - } - } else if (value == 12) { - var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="2"][data--u-u-i-d]', guestslot); - if (ele) { - directEnable(ele, true); - } - } else if (value == 13) { - var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="3"][data--u-u-i-d]', guestslot); - if (ele) { - directEnable(ele, true); - } - } else if (value == 14) { - var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="4"][data--u-u-i-d]', guestslot); - if (ele) { - directEnable(ele, true); - } - } else if (value == 15) { - var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="5"][data--u-u-i-d]', guestslot); - if (ele) { - directEnable(ele, true); - } - } else if (value == 16) { - var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="6"][data--u-u-i-d]', guestslot); - if (ele) { - directEnable(ele, true); - } - } else if (value == 17) { - var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="7"][data--u-u-i-d]', guestslot); - if (ele) { - directEnable(ele, true); - } - } else if (value == 18) { - var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="8"][data--u-u-i-d]', guestslot); - if (ele) { - directEnable(ele, true); - } - } else if (value >= 27) { - var ele = getRightOrderedElement('[data-action-type="volume"][data--u-u-i-d]', guestslot); - if (ele) { - var audioGain = parseInt(value - 27) || 0; - if (audioGain < 0) { - audioGain = 0; - } - - ele.value = 200; - for (var i = 1; i <= 200; i++) { - if (volumeLUT[i] >= audioGain) { - ele.value = i; - break; - } - } - remoteVolume(ele); - } - } - } - - // Additional MIDI CC commands for finer camera control (80-89) - else if (command >= 80 && command <= 89) { - // Map MIDI CC values (0-127) to camera control values - const normalizedValue = value / 127; // 0 to 1 range - - if (command == 80) { - // CC80: Zoom absolute (0-127 maps to 0-2x zoom) - Commands.zoom(normalizedValue * 2, "abs"); - } else if (command == 81) { - // CC81: Pan absolute (0-127 maps to -1 to +1) - Commands.pan((normalizedValue * 2) - 1, "abs"); - } else if (command == 82) { - // CC82: Tilt absolute (0-127 maps to -1 to +1) - Commands.tilt((normalizedValue * 2) - 1, "abs"); - } else if (command == 83) { - // CC83: Exposure absolute (0-127 maps to 0-1) - Commands.exposure(normalizedValue, "abs"); - } else if (command == 84) { - // CC84: Focus absolute (0-127 maps to 0-1) - Commands.focus(normalizedValue, "abs"); - } - } -} - -session.createResourceChannel = function (UUID) { - if (!session.pcs[UUID] || !session.pcs[UUID].allowResources) return; - - const channel = session.pcs[UUID].createDataChannel("resources", { - ordered: true, - maxRetransmits: 30 - }); - - session.pcs[UUID].resourceChannel = channel; - if (session.pcs[UUID] && !session.pcs[UUID].resourceQueue) { // Initialize if not exists - session.pcs[UUID].resourceQueue = [...session.resources]; - } - session.pcs[UUID].processingResource = false; - - session.pcs[UUID].resourceChannel.onopen = () => { - log("Resource channel opened"); - if (session.pcs[UUID].resourceQueue && session.pcs[UUID].resourceQueue.length > 0) { - session.processResourceQueue(UUID); // Process any queued items - } - }; - - session.pcs[UUID].resourceChannel.onclose = () => { - warnlog("Resource channel closed"); - if (session.pcs[UUID]) { - session.pcs[UUID].processingResource = false; // Reset processing state - } - }; - - session.pcs[UUID].resourceChannel.onerror = (e) => { - warnlog("Resource channel error"); - if (session.pcs[UUID]) { - session.pcs[UUID].processingResource = false; // Reset processing state on error - } - }; -}; -async function handleImageUpload(file, field) { - const buffer = await file.arrayBuffer(); - const resource = { - type: field.templateName, // Changed from field.type to field.templateName - id: field.id, - data: buffer, - metadata: { - filename: file.name, - size: file.size, - type: field.type, - label: field.type, - filetype: file.type, - id: field.id, - templateName: field.templateName - } - }; - - for (var UUID in session.pcs) { - if (session.pcs[UUID].allowResources) { - session.sendResource(resource, UUID); - } - } - session.resources.push(resource); -} -session.sendResource = async function (resource, UUID) { - // Resource format: { type: "avatar"|"overlay"|"qr"|"icon", data: ArrayBuffer, metadata: Object } - if (!session.pcs[UUID]) return false; - - if (!session.pcs[UUID].resourceQueue) { - session.pcs[UUID].resourceQueue = [...session.resources]; // Initialize queue if not exists - } - - // Queue the resource - session.pcs[UUID].resourceQueue.push(resource); - - // If channel exists and is open, process queue - if (session.pcs[UUID].resourceChannel && session.pcs[UUID].resourceChannel.readyState === "open") { - if (!session.pcs[UUID].processingResource) { - session.processResourceQueue(UUID); - } - } else if (!session.pcs[UUID].resourceChannel && session.pcs[UUID].allowResources) { - // If channel doesn't exist but resources are allowed, create it - session.createResourceChannel(UUID); - } - - return true; -}; - -session.processResourceQueue = async function (UUID) { - if (!session.pcs[UUID] || !session.pcs[UUID].resourceChannel || session.pcs[UUID].processingResource) return; - - const channel = session.pcs[UUID].resourceChannel; - const queue = session.pcs[UUID].resourceQueue; - - if (queue.length === 0) return; - - session.pcs[UUID].processingResource = true; - - try { - const resource = queue[0]; - const chunkSize = 16384; // 16KB chunks - const totalChunks = Math.ceil(resource.data.byteLength / chunkSize); - - log(resource); - - channel.send(JSON.stringify(resource.metadata)); - log("sending.."); - await new Promise(r => setTimeout(r, 100)); // Small delay to ensure metadata is processed - - for (let i = 0; i < totalChunks; i++) { - const start = i * chunkSize; - const end = Math.min(start + chunkSize, resource.data.byteLength); - const chunk = resource.data.slice(start, end); - - channel.send(chunk); - - // Add small delay between chunks to prevent flooding - await new Promise(r => setTimeout(r, 50)); - } - log("sent."); - queue.shift(); - - } catch (e) { - errorlog(e); - } - - session.pcs[UUID].processingResource = false; - - if (queue.length > 0) { - setTimeout(() => session.processResourceQueue(UUID), 100); - } -}; - -session.recieveResourcesChannel = async function (UUID, channel) { - if (!session.allowResources) { - warnlog("Someone is trying to send resources despite you asking not to.."); - return; - } - log("Created resource channel"); - - session.rpcs[UUID].resourceChannel = channel; - session.rpcs[UUID].resourceChannel.binaryType = "arraybuffer"; - - let metadata = null; - let receivedChunks = []; - let receivedSize = 0; - - channel.onmessage = async (e) => { // Fixed: Using channel parameter instead of nested property - try { - if (typeof e.data === "string") { - const data = JSON.parse(e.data); - log(data); - if (data.templateName) { - metadata = data; - receivedChunks = []; - receivedSize = 0; - } - } else { - if (!metadata) return; - - receivedChunks.push(e.data); - receivedSize += e.data.byteLength; - - if (receivedSize >= metadata.size) { - const completeBuffer = await new Blob(receivedChunks).arrayBuffer(); - session.processReceivedResource(UUID, completeBuffer, metadata); - - receivedChunks = []; - receivedSize = 0; - } - } - } catch (error) { - errorlog(error); - } - }; - - channel.onopen = e => { - log("Opened resource channel"); - }; -}; - -session.processReceivedResource = function (UUID, buffer, data) { - if (!session.rpcs[UUID]) return; - - const createObjectURL = (buffer, type) => { - const blob = new Blob([buffer], { type: type || "image/png" }); - return URL.createObjectURL(blob); - }; - - try { - log(data); - - session.rpcs[UUID].meta[data.templateName] = data; - - if (session.rpcs[UUID].meta[data.templateName].value) { - URL.revokeObjectURL(session.rpcs[UUID].meta[data.templateName].value); - } - - session.rpcs[UUID].meta[data.templateName].value = createObjectURL(buffer, data.type); - log(session.rpcs[UUID].meta[data.templateName].value); - updateMixer(); - } catch (error) { - errorlog(error); - } -}; -function sendRawMIDI(input, UUID = false, streamID = false) { - var msg = {}; - msg.midi = {}; - msg.midi.d = Array.from(input.data); // Convert to regular array - msg.midi.s = input.timestamp || Date.now(); - if (input.message && input.message.channel) { - msg.midi.c = input.message.channel; - } else if (input && input.channel) { - msg.midi.c = input.channel; - } - - if (UUID && session.pcs[UUID] && session.pcs[UUID].allowMIDI) { - session.sendMessage(msg, UUID); - } else if (UUID && session.rpcs[UUID] && session.rpcs[UUID].allowMIDI) { - session.sendRequest(msg, UUID); - } else if (streamID) { - for (var UID in session.rpcs) { - if (session.rpcs[UID].allowMIDI && session.rpcs[UID].streamID === streamID) { - // specific to gstreamer code aplication - session.sendRequest(msg, UID); - return; // only one stream ID should match - } - } - } else { - var list = []; - for (var UID in session.pcs) { - if (session.pcs[UID].allowMIDI) { - if (session.sendMessage(msg, UID)) { - list.push(UID); - } - } - } - for (var UID in session.rpcs) { - if (session.rpcs[UID].allowMIDI) { - // specific to gstreamer code aplication - if (!list.includes(UID)) { - session.sendRequest(msg, UID); - } - } - } - } -} - -function sendMIDINote(note, on = true, channel = 1, uuid = null) { - // MIDI Note On status byte: 144 + (channel - 1) - // MIDI Note Off status byte: 128 + (channel - 1) - const statusByte = on ? (144 + (channel - 1)) : (128 + (channel - 1)); - const velocity = on ? 127 : 0; // 127 for note on, 0 for note off - - // Convert note names like "C1", "D3" to MIDI note numbers - let noteNumber; - if (typeof note === "string") { - const noteName = note.slice(0, -1); - const octave = parseInt(note.slice(-1)); - const noteValues = { - "C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3, - "E": 4, "F": 5, "F#": 6, "Gb": 6, "G": 7, - "G#": 8, "Ab": 8, "A": 9, "A#": 10, "Bb": 10, "B": 11 - }; - - // C1 is MIDI note 24, each octave is 12 notes - noteNumber = 24 + (octave - 1) * 12 + noteValues[noteName]; - } else { - noteNumber = note; - } - - // Create MIDI message and send it - const data = {}; - data.data = [statusByte, noteNumber, velocity]; - sendRawMIDI(data, uuid); - - return { note: noteNumber, status: statusByte, velocity: velocity }; -} -function buttonMIDI(ele, state = null) { - const note = ele.dataset.midiNote; - const uuid = ele.dataset.uuid || null; - const isToggleMode = ele.dataset.midiMode === 'toggle'; - - // Handle state tracking similar to changeGroup function - let newState; - let changed = false; - - if (state === true) { - // Explicit true state requested - if (!ele.classList.contains("pressed") || CtrlPressed) { - changed = true; - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } - newState = true; - } else if (state === false) { - // Explicit false state requested - if (ele.classList.contains("pressed") || CtrlPressed) { - changed = true; - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } - newState = false; - } else if (CtrlPressed) { - newState = ele.classList.contains("pressed"); - changed = true; - } else { - // Toggle current state - newState = !ele.classList.contains("pressed"); - changed = true; - - if (newState) { - ele.classList.add("pressed"); - ele.ariaPressed = "true"; - } else { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - } - } - - // Only send MIDI if state actually changed - if (changed) { - if (isToggleMode) { - // Dual channel toggle - sendMIDINote(note, true, newState ? 1 : 2, uuid); - } else { - // Single channel note on/off - const channel = parseInt(ele.dataset.midiChannel || "1"); - sendMIDINote(note, newState, channel, uuid); - - // Only auto-unpress non-toggle buttons - if (newState) { - setTimeout(() => { - ele.classList.remove("pressed"); - ele.ariaPressed = "false"; - }, 120); - } - } - - // Sync director state if available - if (typeof syncDirectorState === 'function') { - syncDirectorState(ele); - } - } - - return newState; -} - -let currentOscillatorIdMidi = 0; - -function setupMidiOscillator(callbackFunction, frameRate, timeOne = null, thisOscillatorId = null) { - if (!thisOscillatorId) { - thisOscillatorId = ++currentOscillatorIdMidi; - } else if (currentOscillatorIdMidi !== thisOscillatorId) { - return false; - } - - if (!session.audioCtx) { - session.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - } - - let oscillator = session.audioCtx.createOscillator(); - let silence = session.audioCtx.createGain(); - silence.gain.value = 0; - oscillator.connect(silence); - silence.connect(session.audioCtx.destination); - - if (!timeOne) { - timeOne = session.audioCtx.currentTime; - } - - oscillator.onended = () => { - oscillator.disconnect(); - silence.disconnect(); - if (currentOscillatorIdMidi === thisOscillatorId) { - let timeTwo = session.audioCtx.currentTime; - if (typeof callbackFunction === "function") { - callbackFunction(); - } - setupMidiOscillator(callbackFunction, frameRate, timeTwo, thisOscillatorId); - } - }; - - oscillator.start(timeOne); - oscillator.stop(timeOne + 1 / frameRate); - - return function (check = false) { - if (check && currentOscillatorIdMidi !== thisOscillatorId) { - return true; - } else if (check) { - return false; - } - if (currentOscillatorIdMidi === thisOscillatorId) { - currentOscillatorIdMidi++; - } - return false; - }; -} - -function playOutMidi(msg) { - if (session.midiIn === true || session.midiIn == parseInt(session.midiIn)) { - if ("d" in msg) { - const outputs = session.midiIn === true ? WebMidi.outputs : [WebMidi.outputs[parseInt(session.midiIn) - 1]]; - - outputs.forEach(output => { - try { - if ("c" in msg) { - output.channels[msg.c].send(msg.d); - } else if (msg.d) { - output.send(msg.d); - } - } catch (e) { - errorlog(e); - } - }); - } - } -} - -function displayOverlayMessage(label, msg) { - if (!(session.cleanOutput && session.cleanish == false)) { - var textOverlay = getById("overlayMsgs"); - if (textOverlay) { - textOverlay.innerText = msg - textOverlay.style.display = "block"; - var showtime = msg.length * 200 + 3000; - if (showtime > 8000) { - showtime = 8000; - } - setTimeout( - function (ele) { - try { - ele.parentNode.removeChild(ele); - } catch (e) { } - }, - showtime, - spanOverlay - ); - } - } -} - -let lastMTCValues = { hours: 0, minutes: 0, seconds: 0, frames: 0, type: 0 }; -let mtcCounter = 0; -let lastDisplayedTime = 0; -function handleQuarterFrame(data) { - const mtcType = (data >> 4) & 0x7; - const mtcValue = data & 0xF; - - //console.log(`Quarter Frame: Type ${mtcType}, Value ${mtcValue}`); - - switch (mtcType) { - case 0: lastMTCValues.frames = mtcValue; break; - case 1: lastMTCValues.frames |= (mtcValue << 4); break; - case 2: lastMTCValues.seconds = mtcValue; break; - case 3: lastMTCValues.seconds |= (mtcValue << 4); break; - case 4: lastMTCValues.minutes = mtcValue; break; - case 5: lastMTCValues.minutes |= (mtcValue << 4); break; - case 6: lastMTCValues.hours = mtcValue; break; - case 7: - lastMTCValues.hours |= (mtcValue & 0x1) << 4; - lastMTCValues.type = (mtcValue >> 1) & 0x3; - break; - } - - mtcCounter++; - if (mtcCounter === 8) { - mtcCounter = 0; - displayTimecode(lastMTCValues); - } -} - -function handleFullFrame(data) { - const hours = data[0]; - const minutes = data[1]; - const seconds = data[2]; - const frames = data[3]; - // The frame rate information might be encoded differently or not present - // We'll need to determine how to extract this information from your specific implementation - const type = 0; // Default to 24 fps for now, adjust as needed - - lastMTCValues = { hours, minutes, seconds, frames, type }; - displayTimecode(lastMTCValues); - mtcCounter = 0; // Reset counter after full frame -} - -function displayTimecode(timecode) { - const frameRates = [24, 25, 29.97, 30]; - const frameRate = frameRates[timecode.type] || 24; // Default to 24 if unknown - const dropFrame = (timecode.type === 2); - - let timecodeString = `${timecode.hours.toString().padStart(2, '0')}:${timecode.minutes.toString().padStart(2, '0')}:${timecode.seconds.toString().padStart(2, '0')} f${timecode.frames.toString().padStart(2, '0')}/${frameRate.toString()}`; - - //console.log("Displaying Timecode:", timecodeString, `(${frameRate} fps${dropFrame ? ' DF' : ''})`); - displayOverlayMessage("Timecode", timecodeString); -} - -function playbackMIDI(msg, unsafe = false, UUID = null) { - if (session.midiIframe) { - pokeIframeAPI("midi-in", msg, UUID); - } - - if (session.midiTimecode && msg && msg.d) { - const data = msg.d; - if (data.length === 2 && data[0] === 241) { - handleQuarterFrame(data[1]); - } else if (data.length >= 10 && data[0] === 240 && data[1] === 127 && data[4] === 1) { - if (data.length === 11) { - handleFullFrame(data.slice(6, -1)); - } else { - handleFullFrame(data.slice(6, 8)); - } - } - } - - if (session.midiIn === false && session.midiRemote === false) { - return; - } else if (session.midiOut === session.midiIn && session.midiRemote === false) { - return; - } - - log("play out"); - - if (session.midiDelay) { - let timestamp = null; - if ("s" in msg) { - timestamp = msg.s; - } else if ("t" in msg) { - timestamp = msg.t; - } - - if (timestamp !== null) { - const timeDelay = session.midiDelay - (Date.now() - timestamp); - if (timeDelay <= 0) { - playOutMidi(msg); - } else { - setupMidiOscillator(() => playOutMidi(msg), 1000 / timeDelay); - } - } else { - playOutMidi(msg); - } - } else { - playOutMidi(msg); - } - - if (unsafe) { - return; - } // I don't know how midi remote works in reverse, so lets ignore it - - if (session.midiRemote == 4) { - if (msg.d[0] == 176) { - midiHotkeysCommand(msg.d[1], msg.d[2]); - } - } else if (session.midiRemote == 1 || session.midiRemote == 2 || session.midiRemote == 3) { - if (msg.d[0] == 156) { - if (msg.d[1] == 33) { - midiHotkeysNote("A1", msg.d[2]); - } else if (msg.d[1] == 55) { - midiHotkeysNote("G3", msg.d[2]); - } else if (msg.d[1] == 57) { - midiHotkeysNote("A3", msg.d[2]); - } else if (msg.d[1] == 59) { - midiHotkeysNote("B3", msg.d[2]); - } else if (msg.d[1] == 60) { - midiHotkeysNote("C4", msg.d[2]); - } else if (msg.d[1] == 62) { - midiHotkeysNote("D4", msg.d[2]); - } else if (msg.d[1] == 64) { - midiHotkeysNote("E4", msg.d[2]); - } else if (msg.d[1] == 65) { - midiHotkeysNote("F4", msg.d[2]); - } else if (msg.d[1] == 67) { - midiHotkeysNote("G4", msg.d[2]); - } else if (msg.d[1] == 69) { - midiHotkeysNote("A4", msg.d[2]); - } else if (msg.d[1] == 43) { - midiHotkeysNote("G2", msg.d[2]); - } else if (msg.d[1] == 35) { - midiHotkeysNote("B1", msg.d[2]); - } else if (msg.d[1] == 36) { - midiHotkeysNote("C2", msg.d[2]); - } else if (msg.d[1] == 38) { - midiHotkeysNote("D2", msg.d[2]); - } else if (msg.d[1] == 40) { - midiHotkeysNote("E2", msg.d[2]); - } else if (msg.d[1] == 41) { - midiHotkeysNote("F2", msg.d[2]); - } else if (msg.d[1] == 24) { - midiHotkeysNote("C1", msg.d[2]); - } - } - } - //var output = WebMidi.getOutputById("123456789"); - //output = WebMidi.getOutputByName("Axiom Pro 25 Ext Out"); - //output = WebMidi.outputs[0]; -} - -function addEventToAll(targets, trigger, callback) { - // js helper - const target = document.querySelectorAll(targets); - var triggers = trigger.split(" "); - for (let i = 0; i < target.length; i++) { - for (let j = 0; j < triggers.length; j++) { - setTimeout( - function (t1, t2) { - t1.addEventListener(t2, function (e) { - callback(e, t1); - }); - }, - 0, - target[i], - triggers[j] - ); - } - } -} - -function insertAfter(newNode, existingNode) { - existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling); -} -addEventToAll(".column", "click", function (e, ele) { - if (ele.classList.contains("skip-animation")) { - return; - } - try { - var bounding_box = ele.getBoundingClientRect(); - } catch (e) { - return; - } - if (!bounding_box) { - errorlog("No bounding box for ele found"); - } - ele.style.top = bounding_box.y + "px"; - ele.style.left = bounding_box.x - 20 + "px"; - ele.classList.add("in-animation"); - ele.classList.remove("pointer"); - ele.classList.remove("rounded"); - - if (document.getElementById("empty-container")) { - getById("empty-container").parentNode.removeChild(getById("empty-container")); - } - var empty = document.createElement("DIV"); - empty.id = "empty-container"; - empty.className = "column"; - ele.parentNode.insertBefore(empty, ele.nextSibling); - const styles = - "\ - @keyframes outlightbox {\ - 0% {\ - height: 100%;\ - width: 100%;\ - top: 0px;\ - left: 0px;\ - }\ - 50% {\ - height: 200px;\ - top: " + - bounding_box.y + - "px;\ - }\ - 100% {\ - height: 200px;\ - width: " + - bounding_box.width + - "px;\ - top: " + - bounding_box.y + - "px;\ - left: " + - bounding_box.x + - "px;\ - }\ - }\ - "; - if (document.getElementById("lightbox-animations")) { - getById("lightbox-animations").innerHTML = styles; - } - document.body.style.overflow = "hidden"; -}); -addEventToAll(".close", "click", function (e, ele) { - cleanupMediaTracks(); - - document.querySelectorAll(".hidden2").forEach(ele2 => { - ele2.classList.remove("hidden2"); - }); - - ele.style.display = "none"; - mapToAll(".container-inner", function (target) { - target.style.display = "none"; - }); - document.body.style.overflow = "auto"; - - // Get the actual position where the element should return to - var emptyContainer = getById("empty-container"); - if (emptyContainer) { - var targetBox = emptyContainer.getBoundingClientRect(); - - // Update the outlightbox animation with the correct target position - const styles = - "\ - @keyframes outlightbox {\ - 0% {\ - height: 100%;\ - width: 100%;\ - top: 0px;\ - left: 0px;\ - }\ - 50% {\ - height: 200px;\ - top: " + - targetBox.top + - "px;\ - }\ - 100% {\ - height: " + targetBox.height + "px;\ - width: " + - targetBox.width + - "px;\ - top: " + - targetBox.top + - "px;\ - left: " + - targetBox.left + - "px;\ - }\ - }\ - "; - - if (document.getElementById("lightbox-animations")) { - getById("lightbox-animations").innerHTML = styles; - } - - // Don't set position here - let the animation handle it - setTimeout(function () { - // just smoothes things out; breathing room to clean up things first. - ele.parentNode.classList.add("out-animation"); - }, 1); - } - e.stopPropagation(); -}); -addEventToAll(".column", "animationend", function (e, ele) { - if (e.animationName == "inlightbox") { - ele.classList.add("skip-animation"); - mapToAll( - ".close", - function (target) { - target.style.display = "block"; - }, - ele - ); - document.querySelectorAll("#header, #miniTaskBarm, #credits, .columnfade").forEach(ele2 => { - if (ele2 !== ele) { - ele2.classList.add("hidden2"); - } - }); - mapToAll( - ".container-inner", - function (target) { - target.style.display = "block"; - }, - ele - ); - } else if (e.animationName == "outlightbox") { - ele.classList.remove("in-animation"); - ele.classList.remove("out-animation"); - ele.classList.remove("skip-animation"); - ele.classList.remove("columnfade"); - ele.classList.add("pointer"); - ele.classList.add("rounded"); - - // Clear all inline styles to fully restore original position - ele.style.top = ""; - ele.style.left = ""; - ele.style.position = ""; - ele.style.width = ""; - ele.style.height = ""; - - // Clear stored position data - delete ele.dataset.originalTop; - delete ele.dataset.originalLeft; - delete ele.dataset.originalWidth; - delete ele.dataset.originalHeight; - - if (document.getElementById("empty-container")) { - getById("empty-container").parentNode.removeChild(getById("empty-container")); - } - if (document.getElementById("lightbox-animations") && getById("lightbox-animations").sheet && getById("lightbox-animations").sheet.cssRules.length > 0) { - getById("lightbox-animations").sheet.deleteRule(0); - } - } -}); -addEventToAll("#audioSource", "mousedown touchend focusin focusout", function (e, ele) { - var state = getById("multiselect-trigger").dataset.state || 0; // Does this return TRU instead?? GAH. #TODO: - if (state == 0) { - getById("multiselect-trigger").dataset.state = 1; - getById("multiselect-trigger").classList.add("open"); - getById("multiselect-trigger").classList.remove("closed"); - mapToAll( - ".chevron", - function (ele) { - ele.classList.remove("bottom"); - }, - (parentElement = getById("multiselect-trigger")) - ); - mapToAll( - ".multiselect-contents", - function (ele) { - ele.style.display = "block"; - mapToAll( - 'input[type="checkbox"]', - function (ele2) { - ele2.parentNode.style.display = "block"; - ele2.style.display = "inline-block"; - }, - ele - ); - }, - (parentElement = getById("multiselect-trigger").parentNode) - ); - } - e.stopPropagation(); - //e.preventDefault(); -}); -addEventToAll("#audioSource3", "mousedown touchend focusin focusout", function (e, ele) { - var state = getById("multiselect-trigger3").dataset.state || 0; // Does this return TRU instead?? GAH. #TODO: - if (state == 0) { - getById("multiselect-trigger3").dataset.state = 1; - getById("multiselect-trigger3").classList.add("open"); - getById("multiselect-trigger3").classList.remove("closed"); - mapToAll( - ".chevron", - function (target) { - target.classList.remove("bottom"); - }, - getById("multiselect-trigger3") - ); - mapToAll( - ".multiselect-contents", - function (target) { - target.style.display = "block"; - }, - getById("multiselect-trigger3").parentNode - ); - mapToAll( - ".multiselect-contents", - function (target) { - mapToAll( - 'input[type="checkbox"]', - function (target2) { - target2.style.display = "inline-block"; - target2.parentNode.style.display = "block"; - }, - target - ); - }, - getById("multiselect-trigger3").parentNode - ); - } - e.stopPropagation(); - //e.preventDefault(); -}); -addEventToAll("#multiselect-trigger", "mousedown touchend focusin focusout", function (e, ele) { - var state = ele.dataset.state || 0; // Does this return TRU instead?? GAH. #TODO: - if (state == 0) { - // open the dropdown - ele.dataset.state = 1; - ele.classList.add("open"); - ele.classList.remove("closed"); - mapToAll( - ".chevron", - function (target) { - target.classList.remove("bottom"); - }, - getById("multiselect-trigger") - ); - mapToAll( - ".multiselect-contents", - function (target) { - target.style.display = "block"; - }, - ele.parentNode - ); - mapToAll( - ".multiselect-contents", - function (target) { - mapToAll( - 'input[type="checkbox"]', - function (target2) { - target2.style.display = "inline-block"; - target2.parentNode.style.display = "block"; - }, - target - ); - }, - ele.parentNode - ); - } else { - // close the dropdown - ele.dataset.state = 0; - ele.classList.add("closed"); - ele.classList.remove("open"); - mapToAll( - ".chevron", - function (target) { - target.classList.add("bottom"); - }, - ele - ); - mapToAll( - ".multiselect-contents", - function (target) { - mapToAll( - 'input[type="checkbox"]', - function (target2) { - target2.style.display = "none"; - if (!target2.checked) { - target2.parentNode.style.display = "none"; - } - }, - target - ); - }, - ele.parentNode - ); - } - e.preventDefault(); - e.stopPropagation(); -}); -addEventToAll("#multiselect-trigger3", "mousedown touchend focusin focusout", function (e, ele) { - var state = ele.dataset.state || 0; // Does this return TRU instead?? GAH. #TODO: - if (state == 0) { - // open the dropdown - ele.dataset.state = 1; - ele.classList.add("open"); - ele.classList.remove("closed"); - mapToAll( - ".chevron", - function (target) { - target.classList.remove("bottom"); - }, - ele - ); - mapToAll( - ".multiselect-contents", - function (target) { - target.style.display = "block"; - }, - ele.parentNode - ); - mapToAll( - ".multiselect-contents", - function (target) { - mapToAll( - 'input[type="checkbox"]', - function (target2) { - target2.style.display = "inline-block"; - target2.parentNode.style.display = "block"; - }, - target - ); - }, - ele.parentNode - ); - } else { - // close the dropdown - ele.dataset.state = 0; - ele.classList.add("closed"); - ele.classList.remove("open"); - mapToAll( - ".chevron", - function (target) { - target.classList.add("bottom"); - }, - ele - ); - mapToAll( - ".multiselect-contents", - function (target) { - mapToAll( - 'input[type="checkbox"]', - function (target2) { - target2.style.display = "none"; - if (!target2.checked) { - target2.parentNode.style.display = "none"; - } - }, - target - ); - }, - ele.parentNode - ); - } - e.preventDefault(); - e.stopPropagation(); -}); - -function getSenders2(UUID) { - var fixedSenders = []; - var isAlt = false; - if (!(UUID in session.pcs)) { - return fixedSenders; - } - if ("realUUID" in session.pcs[UUID]) { - isAlt = true; - UUID = session.pcs[UUID].realUUID; - if (!(UUID in session.pcs)) { - return fixedSenders; - } - } - var senders = session.pcs[UUID].getSenders(); - - if (isAlt) { - senders.forEach(sender => { - if (sender.track && sender.track.id) { - if (sender.track.id in screenshareTracks) { - // I'm not going to change track.kind, since OBS isn't part of this list - fixedSenders.push(sender); - } - } - }); - } else { - senders.forEach(sender => { - if (sender.track && sender.track.id) { - if (!(sender.track.id in screenshareTracks)) { - fixedSenders.push(sender); - } - } - }); - } - - return fixedSenders; -} - -function getReceivers2(UUID) { - var fixedReceivers = []; - var isAlt = false; - var ssTracks = []; - if ("realUUID" in session.rpcs[UUID]) { - isAlt = true; - UUID = session.rpcs[UUID].realUUID; - if (!("screenIndexes" in session.rpcs[UUID])) { - errorlog("this is supposed to be a screen share, but no screen share index was found"); - return; - } - ssTracks = session.rpcs[UUID].screenIndexes; - } else if ("screenIndexes" in session.rpcs[UUID] && session.rpcs[UUID].screenIndexes) { - ssTracks = session.rpcs[UUID].screenIndexes; - } - - if (session.rpcs[UUID] && session.rpcs[UUID].getReceivers) { - var receivers = session.rpcs[UUID].getReceivers(); - } else { - var receivers = []; - } - - try { - if (session.rpcs[UUID].whep && session.rpcs[UUID].whep.getReceivers) { - // used to be "mc", not "whep" - try { - receivers = receivers.concat(session.rpcs[UUID].whep.getReceivers()); - } catch (e) { - errorlog(e); - } - } - } catch (e) { - errorlog(e); - } - - if (isAlt) { - for (var i = 0; i < receivers.length; i++) { - for (var j = 0; j < ssTracks.length; j++) { - if (i == ssTracks[j]) { - fixedReceivers.push(receivers[i]); - break; - } - } - } - } else { - for (var i = 0; i < receivers.length; i++) { - var matched = false; - for (var j = 0; j < ssTracks.length; j++) { - if (i == ssTracks[j]) { - matched = true; - } - } - if (!matched) { - fixedReceivers.push(receivers[i]); - } - } - } - - return fixedReceivers; -} - -function getReceiversMC(UUID) { - var fixedReceivers = []; - var isAlt = false; - var ssTracks = []; - if ("realUUID" in session.rpcs[UUID]) { - isAlt = true; - UUID = session.rpcs[UUID].realUUID; - if (!("screenIndexes" in session.rpcs[UUID])) { - errorlog("this is supposed to be a screen share, but no screen share index was found"); - return; - } - ssTracks = session.rpcs[UUID].screenIndexes; - } else if ("screenIndexes" in session.rpcs[UUID] && session.rpcs[UUID].screenIndexes) { - ssTracks = session.rpcs[UUID].screenIndexes; - } - - receivers = []; - if (session.rpcs[UUID].whep) { - receivers = session.rpcs[UUID].whep.getReceivers(); - } - - if (isAlt) { - for (var i = 0; i < receivers.length; i++) { - for (var j = 0; j < ssTracks.length; j++) { - if (i == ssTracks[j]) { - fixedReceivers.push(receivers[i]); - break; - } - } - } - } else { - for (var i = 0; i < receivers.length; i++) { - var matched = false; - for (var j = 0; j < ssTracks.length; j++) { - if (i == ssTracks[j]) { - matched = true; - } - } - if (!matched) { - fixedReceivers.push(receivers[i]); - } - } - } - return fixedReceivers; -} - -async function createSecondStream2(UUID) { - if (session.pcs[UUID].allowScreenVideo === false && session.pcs[UUID].allowScreenAudio === false) { - return false; - } - if ("realUUID" in session.pcs[UUID]) { - return false; - } // we don't want to attach to an existing screen share obviously - if (!session.screenStream) { - return false; - } - - if (!(UUID + "_screen" in session.pcs)) { - warnlog(UUID + "_screen; new screen link"); - session.pcs[UUID + "_screen"] = {}; - session.pcs[UUID + "_screen"].realUUID = UUID; - session.pcs[UUID + "_screen"].stats = {}; - session.pcs[UUID + "_screen"].sceneDisplay = null; - session.pcs[UUID + "_screen"].sceneMute = null; - session.pcs[UUID + "_screen"].solo = null; - session.pcs[UUID + "_screen"].allowVideo = session.pcs[UUID].allowScreenVideo; - session.pcs[UUID + "_screen"].allowAudio = session.pcs[UUID].allowScreenAudio; - session.pcs[UUID + "_screen"].allowDrawing = session.pcs[UUID].allowDrawing; - if (session.pcs[UUID + "_screen"].allowDrawing) { - if (session.screenShareElement && session.screenShareElement.syncDrawOnVideo) { - session.screenShareElement.syncDrawOnVideo(); - } - } - session.pcs[UUID + "_screen"].layout = null; - session.pcs[UUID + "_screen"].obsState = {}; - session.pcs[UUID + "_screen"].obsState.visibility = null; - session.pcs[UUID + "_screen"].obsState.sourceActive = null; - session.pcs[UUID + "_screen"].obsState.streaming = null; - session.pcs[UUID + "_screen"].obsState.recording = null; - session.pcs[UUID + "_screen"].obsState.virtualcam = null; - session.pcs[UUID + "_screen"].optimizedBitrate = false; - session.pcs[UUID + "_screen"].savedBitrate = false; - session.pcs[UUID + "_screen"].bitrateTimeout = null; - session.pcs[UUID + "_screen"].bitrateTimeoutFirefox = false; - session.pcs[UUID + "_screen"].setAudioBitrate = false; - session.pcs[UUID + "_screen"].setBitrate = false; - session.pcs[UUID + "_screen"].maxBandwidth = null; // based on max available bitrate - session.pcs[UUID + "_screen"].limitAudio = false; - session.pcs[UUID + "_screen"].audioMutedOverride = false; - session.pcs[UUID + "_screen"].enhanceAudio = false; - session.pcs[UUID + "_screen"].meshcast = null; - session.pcs[UUID + "_screen"].UUID = UUID + "_screen"; - session.pcs[UUID + "_screen"].scale = false; - session.pcs[UUID + "_screen"].scaleDueToBitrate = false; - session.pcs[UUID + "_screen"].scaleWidth = false; - session.pcs[UUID + "_screen"].scaleHeight = false; - session.pcs[UUID + "_screen"].scaleSnap = false; - session.pcs[UUID + "_screen"].scaleResolution = false; - session.pcs[UUID + "_screen"].scene = false; - session.pcs[UUID + "_screen"].keyframeRate = false; - session.pcs[UUID + "_screen"].keyframeTimeout = null; - session.pcs[UUID + "_screen"].label = false; - session.pcs[UUID + "_screen"].order = false; - session.pcs[UUID + "_screen"].preferVideoCodec = false; - session.pcs[UUID + "_screen"].startTime = Date.now(); - session.pcs[UUID + "_screen"].needsPublishing = null; - // session.pcs[UUID+"_screen"].rotation = false; I don't think this will ever be used? - - // we will use allowVideo/allowAudio from the main UUID parent - - session.pcs[UUID + "_screen"].getStats = function () { - return new Promise((resolve, reject) => { - resolve([]); - }); - }; - } - - /* if (session.audioContentHint && tracks.length){ - tracks.forEach(trk=>{ - try { - - trk.contentHint = session.audioContentHint; - } catch(e){ - errorlog(e); - } - }); - } */ - - var senders = getSenders2(UUID + "_screen"); - var tracks = session.screenStream.getTracks(); - - for (var i = 0; i < tracks.length; i++) { - var track = tracks[i]; - - try { - if (track.kind === "audio" && session.pcs[UUID + "_screen"].allowAudio === false) { - continue; - } else if (track.kind === "video" && session.pcs[UUID + "_screen"].allowVideo === false) { - continue; - } - } catch (e) { - errorlog(e); - } - - if (session.audioContentHint && track.kind === "audio") { - try { - track.contentHint = session.audioContentHint; // this gets triggered too often I think - } catch (e) { - errorlog(e); - } - } - - if (session.screenshareContentHint && track.kind === "video") { - try { - track.contentHint = session.screenshareContentHint; // this gets triggered too often I think - } catch (e) { - errorlog(e); - } - } else if (session.contentHint && track.kind === "video") { - try { - track.contentHint = session.contentHint; // this gets triggered too often I think - } catch (e) { - errorlog(e); - } - } - - var added = false; - for (var j = 0; j < senders.length; j++) { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - var sender = senders[j]; - if (sender.track && sender.track.kind == track.kind) { - sender.replaceTrack(track); // replace may not be supported by all browsers. eek. - sender.track.enabled = true; - added = true; - break; - } - } - if (!added) { - session.pcs[UUID].addTrack(track, session.screenStream); - } - } - - session.applyIsolatedChat(); - - updateMixer(); -} - -var screenshareTracks = {}; -var firsttime = true; -async function createSecondStream() { - //////////////////////////// &sstype=3 ? - if (session.screenShareState == false) { - // adding a screen - - var video = {}; - - var quality = session.quality_ss; - - if (quality === false) { - quality = 0; // default to 1080p for screen shares - } - - if (session.quality !== false) { - quality = session.quality; - } - if (session.screensharequality !== false) { - quality = session.screensharequality; - } - - if (quality == -1) { - // unlocked capture resolution - } else if (quality == -2) { - video.width = { - ideal: 3840 - }; - video.height = { - ideal: 2160 - }; - } else if (quality == -3) { - video.width = { - ideal: 2560 - }; - video.height = { - ideal: 1440 - }; - } else if (quality == 0) { - video.width = { - ideal: 1920 - }; - video.height = { - ideal: 1080 - }; - } else if (quality == 1) { - video.width = { - ideal: 1280 - }; - video.height = { - ideal: 720 - }; - } else if (quality == 2) { - video.width = { - ideal: 640 - }; - video.height = { - ideal: 360 - }; - } else if (quality >= 3) { - // lowest - video.width = { - ideal: 320 - }; - video.height = { - ideal: 180 - }; - } - - if (session.width) { - video.width = { - ideal: session.width - }; - } - if (session.height) { - video.height = { - ideal: session.height - }; - } - - var constraints = { - // this part is a bit annoying. Do I use the same settings? I can add custom setting controls here later - audio: { - echoCancellation: true, // we want to cancel echo, since this is a secondary stream - autoGainControl: false, - noiseSuppression: false - }, - video: video - //,cursor: {exact: "none"} - }; - - try { - let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); - if (supportedConstraints.cursor) { - if (session.screensharecursor) { - constraints.video.cursor = ["always", "motion"]; - } else { - constraints.video.cursor = "never"; - } - } - if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) { - constraints.audio.suppressLocalAudioPlayback = true; - } - // - if (session.preferCurrentTab) { - constraints.preferCurrentTab = true; - } - if (session.selfBrowserSurface) { - constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include - } - if (session.surfaceSwitching) { - constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include - } - if (session.systemAudio) { - constraints.systemAudio = session.systemAudio; // exclude or include - } - if (session.displaySurface && supportedConstraints.displaySurface) { - constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser - } - } catch (e) { - warnlog("navigator.mediaDevices.getSupportedConstraints() not supported"); - } - - if (session.screenshareAEC === false) { - constraints.audio.echoCancellation = false; - } // we want to keep echo cancellation when doing a secondary screen share, unless explicitly disabled. - - if (session.screenshareAutogain === false) { - constraints.audio.autoGainControl = false; - } else if (session.autoGainControl === true) { - constraints.audio.autoGainControl = true; - } - - if (session.screenshareDenoise === false) { - constraints.audio.noiseSuppression = false; - } else if (session.noiseSuppression === true) { - constraints.audio.noiseSuppression = true; - } - - if (session.voiceIsolation === true) { - constraint.audio.voiceIsolation = true; - } - //if (audio == false) { - // constraints.audio = false; - //} - - var overrideFramerate = false; - - if (session.screensharefps !== false) { - constraints.video.frameRate = { - ideal: session.screensharefps, - max: session.screensharefps - }; - } else if (session.frameRate !== false && session.maxframeRate != false) { - overrideFramerate = session.frameRate; - constraints.video.frameRate = { - ideal: session.maxframeRate, - max: session.maxframeRate - }; - } else if (session.frameRate !== false) { - constraints.video.frameRate = session.frameRate; - } else if (session.maxframeRate != false) { - constraints.video.frameRate = { - ideal: session.maxframeRate, - max: session.maxframeRate - }; - } else { - constraints.video.frameRate = { - ideal: 60 - }; - } - - if (session.screenshareVideoOnly) { - constraints.audio = false; - } - - if (session.forceAspectRatio) { - // await updateCameraConstraints("aspectRatio", session.forceAspectRatio); - if (constraints.video && constraints.video !== true) { - constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) }; - - if (constraints.video.width && !session.width) { - delete constraints.video.width; - } else if (constraints.video.height && !session.height) { - delete constraints.video.height; - } - } - } - - if (constraints.video !== false && Object.keys(constraints.video).length == 0) { - constraints.video = true; - } - - if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - if (!ElectronDesktopCapture) { - if (!session.cleanOutput) { - warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)"); - } - return false; - } - } - - log("sstype3 screen share"); - log(constraints); - - navigator.mediaDevices - .getDisplayMedia(constraints) - .then(async function (stream) { - try { - var constraint = {}; - if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) { - constraint.aspectRatio = parseFloat(session.forceAspectRatio); - } else if (session.forceScreenShareAspectRatio) { - constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio); - } - if (overrideFramerate) { - constraint.frameRate = overrideFramerate; - } - if (Object.keys(constraint).length) { - await stream.getVideoTracks()[0].applyConstraints({ - advanced: [constraint] - }); - log({ - advanced: [constraint] - }); - } - } catch (e) { - errorlog(e); - } - - session.screenShareState = true; - session.screenStream = stream; - if (session.whipPublishScreen && session.whipOutputScreen) { - whipOutScreen(); - } - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - - //if (!session.screenVideoElement){ - // session.screenVideoElement = createVideoElement() - //} - try { - stream.getVideoTracks()[0].onended = function () { - stopSecondScreenshare(); - }; - } catch (e) { - log("No Video selected; screensharing?"); - } - - session.screenStream.getTracks().forEach(function (track) { - screenshareTracks[track.id] = true; // obs isn't included, so no point to check track.kind - }); - for (UUID in session.pcs) { - createSecondStream2(UUID); - } - - if (!firsttime) { - var msg = {}; - msg.screenStopped = false; - session.sendMessage(msg); - } else if (!session.screenShareElement) { - session.screenShareElement = createVideoElement(); - session.screenShareElement.muted = true; - session.screenShareElement.autoplay = true; - session.screenShareElement.controls = session.showControls || false; - - session.screenShareElement.id = "screensharesource"; - session.screenShareElement.dataset.sid = session.streamID + ":s"; - - if (typeof session.volume == "number") { - session.screenShareElement.volume = session.volume; - } else { - session.screenShareElement.volume = 1.0; // play audio automatically - } - session.screenShareElement.classList.add("tile"); - session.screenShareElement.setAttribute("playsinline", ""); - session.screenShareElement.controlTimer = null; - - session.screenShareElement.dataset.menu = "context-menu-video"; - if (!session.cleanOutput) { - session.screenShareElement.classList.add("task"); // this adds the right-click menu - } - createDirectorScreenshareOnlyBox(); - - if (document.getElementById("videoScreenContainer_director")) { - getById("videoScreenContainer_director").appendChild(session.screenShareElement); - } - - session.screenShareElement.onpause = event => { - // prevent things from pausing; human or other - if (!(event.ctrlKey || event.metaKey)) { - log("Video paused; auto playing"); - event.currentTarget - .play() - .then(_ => { - log("playing 10"); - }) - .catch(warnlog); - } - }; - - session.screenShareElement.addEventListener("click", function (e) { - log("click"); - try { - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - var [menu, innerMenu] = statsMenuCreator(); - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu, true); - printMyStats(innerMenu, true); - e.stopPropagation(); - return false; - } - } catch (e) { - errorlog(e); - } - }); - - session.screenShareElement.touchTimeOut = null; - session.screenShareElement.touchLastTap = 0; - session.screenShareElement.touchCount = 0; - - session.screenShareElement.addEventListener("touchend", function (event) { - if (session.disableMouseEvents) { - return; - } - log("touched"); - //document.ontouchup = null; - //document.onmouseup = null; - document.onmousemove = null; - document.ontouchmove = null; - var currentTime = new Date().getTime(); - var tapLength = currentTime - session.screenShareElement.touchLastTap; - clearTimeout(session.screenShareElement.touchTimeOut); - if (tapLength < 500 && tapLength > 0) { - /// - log("double touched"); - session.screenShareElement.touchCount += 1; - event.preventDefault(); - if (session.screenShareElement.touchCount < 5) { - session.screenShareElement.touchLastTap = currentTime; - return false; - } - session.screenShareElement.touchLastTap = 0; - session.screenShareElement.touchCount = 0; - var [menu, innerMenu] = statsMenuCreator(); - menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu, true); - printMyStats(innerMenu, true); - event.stopPropagation(); - return false; - ////// - } else { - session.screenShareElement.touchCount = 1; - session.screenShareElement.touchLastTap = currentTime; - session.screenShareElement.touchTimeOut = setTimeout( - function (vv) { - clearTimeout(vv.touchTimeOut); - vv.touchLastTap = 0; - vv.touchCount = 0; - }, - 5000, - session.screenShareElement - ); - } - }); - } - - firsttime = false; - - session.screenShareElement.srcObject = session.screenStream; - - getById("screensharebutton").classList.add("green"); - getById("screensharebutton").ariaPressed = "true"; - getById("screensharebutton").title = getTranslation("stop-screen-sharing"); - - getById("screenshare2button").classList.add("green"); - getById("screenshare2button").ariaPressed = "true"; - getById("screenshare2button").title = getTranslation("stop-screen-sharing"); - - getById("screenshare3button").classList.add("green"); - getById("screenshare3button").ariaPressed = "true"; - getById("screenshare3button").title = getTranslation("stop-screen-sharing"); - - if (session.autorecord || session.autorecordlocal) { - log("AUTO RECORD START SSTYPE3"); - setTimeout( - function (s) { - if (!session.screenStream) { - return; - } - try { - var ele = document.getElementById("recordLocalScreenbutton"); - if (ele) { - ele.classList.add("red"); - ele.classList.remove("hidden"); - if (!ele.vid) { - var v = createVideoElement(); - v.muted = true; - v.srcObject = s; - ele.vid = v; - } - if (ele.vid.recorder || ele.vid.recording) { - ele.vid.recorder.stop(); - ele.classList.remove("red"); - ele.classList.add("hidden"); - ele.vid = null; - } else { - var videoKbps = session.recordDefault; - if (session.recordLocal !== false) { - videoKbps = session.recordLocal; - } - recordLocalVideo(null, videoKbps, ele.vid); - } - } - } catch (e) { - errorlog(e); - } - }, - 2000, - session.screenStream - ); - } - - setTimeout(function () { - updateMixer(); - }, 100); - - setTimeout(function () { - updateMixer(); - }, 1000); - }) - .catch(function (err) { - errorlog(err); - }); - } else { - // removing a screen - stopSecondScreenshare(); - } -} -function recordLocalScreenStopRecord() { - var ele = document.getElementById("recordLocalScreenbutton"); - if (ele) { - try { - ele.classList.remove("red"); - ele.classList.add("hidden"); - if (ele.vid) { - if (ele.vid.recorder || ele.vid.recording) { - ele.vid.recorder.stop(); - } - ele.vid = null; - } - } catch (e) { - errorlog(e); - } - } -} -function stopSecondScreenshare() { - var msg = {}; - msg.screenStopped = true; - session.sendMessage(msg); - - for (const peerUUID in session.pcs) { - if (!session.pcs.hasOwnProperty(peerUUID)) { - continue; - } - const peer = session.pcs[peerUUID]; - if (peer && "whipScreen" in peer && peer.whipScreen !== false) { - peer.whipScreen = null; - } - } - - var ele = document.getElementById("recordLocalScreenbutton"); - if (ele) { - try { - ele.classList.remove("red"); - ele.classList.add("hidden"); - if (ele.vid) { - if (ele.vid.recorder || ele.vid.recording) { - ele.vid.recorder.stop(); - } - ele.vid = null; - } - } catch (e) { - errorlog(e); - } - } - if (session.screenStream) { - session.screenStream.getTracks().forEach(function (track) { - // previous video track; saving it. Must remove the track at some point. - for (UUID in session.pcs) { - if (!("realUUID" in session.pcs[UUID])) { - continue; - } // not a screen share, so skip - var senders = getSenders2(UUID); - senders.forEach(sender => { - // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? - if (sender.track && sender.track.kind == "video") { - sender.track.enabled = false; - } - }); - } - if (track.id in screenshareTracks) { - // obs isn't included, so no point to check track.kind - log("remove track 2"); - session.screenStream.removeTrack(track); - track.stop(); - screenshareTracks[track.id] = false; - } - }); - } - if (document.getElementById("container_screen_director")) { - document.getElementById("container_screen_director") - } - - session.screenStream = false; - session.screenShareState = false; - pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); - - if (session.whipOutScreen) { - try { - session.whipOutScreen.getSenders().forEach(sender => { - try { sender.track && sender.track.stop && sender.track.stop(); } catch (e) { } - }); - } catch (e) { } - try { - session.whipOutScreen.close(); - } catch (e) { } - session.whipOutScreen = null; - } - if (session.whipoutScreenSettings) { - session.whipoutScreenSettings.started = false; - } - - getById("screensharebutton").classList.remove("green"); - getById("screensharebutton").ariaPressed = "false"; - getById("screensharebutton").title = getTranslation("share-a-screen"); - - getById("screenshare2button").classList.remove("green"); - getById("screenshare2button").ariaPressed = "false"; - getById("screenshare2button").title = getTranslation("share-a-screen"); - - getById("screenshare3button").classList.remove("green"); - getById("screenshare3button").ariaPressed = "false"; - getById("screenshare3button").title = getTranslation("share-a-screen"); - - if (document.getElementById("screensharesource")) { - document.getElementById("screensharesource").load() - } - - setTimeout(function () { - updateMixer(); - }, 100); - - setTimeout(function () { - updateMixer(); - }, 1000); -} - -function enableFullscreenZoom() { - const content = document.getElementById('gridlayout'); - let lastScrollPosition = { x: 0, y: 0 }; - content.style.overflow = "visible"; - content.style.transformOrigin = "center center"; // Set transform origin to center - - // Add styles for the container to allow centering - content.style.position = "relative"; - content.parentElement.style.display = "flex"; - content.parentElement.style.justifyContent = "center"; - content.parentElement.style.alignItems = "center"; - content.parentElement.style.minHeight = "100vh"; - - content.parentNode.insertAdjacentHTML('afterbegin', - '
    \ - \ -
    '); - - const viewport = document.querySelector('meta[name="viewport"]'); - - viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=4.0, user-scalable=yes'; - if (!viewport) { - const meta = document.createElement('meta'); - meta.name = 'viewport'; - meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=4.0, user-scalable=yes'; - document.head.appendChild(meta); - } - - const slider = document.getElementById('zoomSlider2'); - slider.addEventListener('input', (e) => { - const scale = e.target.value / 100; - - // Calculate the center point before scaling - const rect = content.getBoundingClientRect(); - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - - // Apply the transform with translate to maintain center point - content.style.transform = `scale(${scale})`; - - // Calculate scroll position to keep center point - const newWidth = rect.width * scale; - const newHeight = rect.height * scale; - const newLeft = centerX - newWidth / 2; - const newTop = centerY - newHeight / 2; - - window.scrollTo( - window.scrollX + (newLeft - rect.left), - window.scrollY + (newTop - rect.top) - ); - }); -} - -// Auth Access Control Functions -let currentRoomSettings = null; - -async function loadRoomAccessSettings() { - if (!session.authMode || !session.roomid || !window.vdoAuth) return; - - try { - // Get room settings - const response = await fetch(`${AUTH_SERVICE_URL}/api/room/access`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${session.authToken}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ room: session.roomid }) - }); - - if (response.ok) { - const roomInfo = await response.json(); - if (roomInfo.isOwner) { - // Get detailed room settings - const settingsResponse = await fetch(`${AUTH_SERVICE_URL}/api/room/settings/${session.realRoomId || session.roomid}`, { - headers: { - 'Authorization': `Bearer ${session.authToken}` - } - }); - - if (settingsResponse.ok) { - currentRoomSettings = await settingsResponse.json(); - - // Update UI with current settings - const accessMode = currentRoomSettings.accessMode || 'public'; - document.querySelector(`input[name="roomAccessMode"][value="${accessMode}"]`).checked = true; - updateRoomAccessMode(accessMode); - - // Load allowlist - if (currentRoomSettings.allowlist && currentRoomSettings.allowlist.length > 0) { - displayAllowlist(currentRoomSettings.allowlist); - } - - // Load pending access requests - loadAccessRequests(); - } - } - } - } catch (e) { - console.error('Failed to load room settings:', e); - } -} - -function updateRoomAccessMode(mode) { - // Show/hide allowlist section based on mode - if (mode === 'allowlist') { - getById('allowlistSection').style.display = 'block'; - getById('accessRequestsSection').style.display = 'block'; - } else { - getById('allowlistSection').style.display = 'none'; - getById('accessRequestsSection').style.display = 'none'; - } - - // Update room settings on server - if (currentRoomSettings && window.vdoAuth) { - window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, { - accessMode: mode - }); - } -} - -function addToAllowlist() { - const input = getById('allowlistInput'); - const value = input.value.trim(); - - if (!value) return; - - // Validate format - if (!value.startsWith('@') && !value.startsWith('email:')) { - alert('Please enter a username (starting with @) or email pattern (starting with email:)'); - return; - } - - // Add to current allowlist - if (!currentRoomSettings) { - currentRoomSettings = { allowlist: [] }; - } - - if (!currentRoomSettings.allowlist.includes(value)) { - currentRoomSettings.allowlist.push(value); - - // Update server - if (window.vdoAuth) { - window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, { - allowlist: currentRoomSettings.allowlist - }); - } - - // Update display - displayAllowlist(currentRoomSettings.allowlist); - - // Clear input - input.value = ''; - } -} - -function displayAllowlist(allowlist) { - const display = getById('allowlistDisplay'); - display.innerHTML = ''; - - allowlist.forEach(entry => { - const item = document.createElement('div'); - item.style.cssText = 'padding: 5px; margin: 2px 0; background: #f0f0f0; border-radius: 3px; display: flex; justify-content: space-between; align-items: center;'; - - const label = document.createElement('span'); - label.textContent = entry; - - const removeBtn = document.createElement('button'); - removeBtn.textContent = 'Remove'; - removeBtn.style.cssText = 'padding: 2px 8px; font-size: 12px;'; - removeBtn.onclick = () => removeFromAllowlist(entry); - - item.appendChild(label); - item.appendChild(removeBtn); - display.appendChild(item); - }); -} - -function removeFromAllowlist(entry) { - if (!currentRoomSettings || !currentRoomSettings.allowlist) return; - - const index = currentRoomSettings.allowlist.indexOf(entry); - if (index > -1) { - currentRoomSettings.allowlist.splice(index, 1); - - // Update server - if (window.vdoAuth) { - window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, { - allowlist: currentRoomSettings.allowlist - }); - } - - // Update display - displayAllowlist(currentRoomSettings.allowlist); - } -} - -async function loadAccessRequests() { - if (!session.authMode || !session.roomid || !window.vdoAuth) return; - - try { - const requests = await window.vdoAuth.getRoomAccessRequests(session.realRoomId || session.roomid); - displayAccessRequests(requests); - } catch (e) { - console.error('Failed to load access requests:', e); - } -} - -function displayAccessRequests(requests) { - const list = getById('accessRequestsList'); - list.innerHTML = ''; - - if (requests.length === 0) { - list.innerHTML = '
    No pending requests
    '; - return; - } - - requests.forEach(request => { - const item = document.createElement('div'); - item.style.cssText = 'padding: 10px; margin: 5px 0; background: #f9f9f9; border: 1px solid #ddd; border-radius: 5px;'; - - const info = document.createElement('div'); - const header = document.createElement('div'); - header.style.cssText = 'display: flex; align-items: center; margin-bottom: 5px;'; - - if (request.avatar) { - const avatarImg = document.createElement('img'); - avatarImg.src = request.avatar; - avatarImg.alt = ''; - avatarImg.style.cssText = 'width: 30px; height: 30px; border-radius: 50%; margin-right: 10px;'; - header.appendChild(avatarImg); - } - - const textWrap = document.createElement('div'); - const nameEl = document.createElement('strong'); - nameEl.textContent = request.displayName || ''; - textWrap.appendChild(nameEl); - - if (request.userHandle) { - const handleEl = document.createElement('span'); - handleEl.style.cssText = 'color: #666; margin-left: 5px;'; - handleEl.textContent = request.userHandle; - textWrap.appendChild(handleEl); - } - - header.appendChild(textWrap); - info.appendChild(header); - - const meta = document.createElement('div'); - meta.style.cssText = 'color: #999; font-size: 12px;'; - const metaParts = []; - - if (request.provider) { - metaParts.push(request.provider); - } - - if (request.requestedAt) { - const requestedDate = new Date(request.requestedAt); - if (!Number.isNaN(requestedDate.getTime())) { - metaParts.push(requestedDate.toLocaleString()); - } - } - - meta.textContent = metaParts.join(' • '); - info.appendChild(meta); - - const actions = document.createElement('div'); - actions.style.cssText = 'margin-top: 8px; display: flex; gap: 10px;'; - - const approveBtn = document.createElement('button'); - approveBtn.textContent = 'Approve'; - approveBtn.style.cssText = 'padding: 5px 15px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer;'; - approveBtn.onclick = () => handleAccessRequest(request.userId, 'approve'); - - const denyBtn = document.createElement('button'); - denyBtn.textContent = 'Deny'; - denyBtn.style.cssText = 'padding: 5px 15px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer;'; - denyBtn.onclick = () => handleAccessRequest(request.userId, 'deny'); - - actions.appendChild(approveBtn); - actions.appendChild(denyBtn); - - item.appendChild(info); - item.appendChild(actions); - list.appendChild(item); - }); -} - -async function handleAccessRequest(userId, action) { - if (!window.vdoAuth) return; - - try { - const success = await window.vdoAuth.handleAccessRequest(session.realRoomId || session.roomid, userId, action); - if (success) { - // Reload access requests - loadAccessRequests(); - - // Reload allowlist if approved - if (action === 'approve') { - loadRoomAccessSettings(); - } - } - } catch (e) { - console.error('Failed to handle access request:', e); - } -} + + if (contentType && contentType.indexOf("application/sdp") === 0 && body) { + try { + const answer = { type: "answer", sdp: body }; + await pc.setRemoteDescription(answer); + } catch (e) { + errorlog(e); + } + } + + if (!session.whipoutScreenSettings) { + session.whipoutScreenSettings = { type: "whep", url: whepUrl, token: session.streamID + "_s", media: "screen", started: false }; + } else { + session.whipoutScreenSettings.url = whepUrl; + } + if (!session.whipoutScreenSettings.token) { + session.whipoutScreenSettings.token = session.streamID + "_s"; + } + session.whipoutScreenSettings.media = "screen"; + session.whipoutScreenSettings.started = Date.now(); + + broadcastWhepSettings("screen"); + return true; +} + +function whipClient() { + // publish to whip.vdo.ninja with obs, to use. experimental + if (!session.whipView) { + return; + } + warnlog("WHIP Client started"); + + var socket = null; + var connecting = false; + var failedCount = 0; + + function connect() { + clearTimeout(connecting); + if (socket) { + if (socket.readyState === socket.OPEN) { + return; + } + try { + socket.close(); + } catch (e) { } + } + log("Trying to load whip websocket..."); + + socket = new WebSocket(session.whipServerURL); + + socket.onclose = function () { + failedCount += 1; + clearTimeout(connecting); + connecting = setTimeout(function () { + connect(); + }, 100 * (failedCount - 1)); + }; + + socket.onerror = function (e) { + console.error(e); + failedCount += 1; + clearTimeout(connecting); + connecting = setTimeout(function () { + connect(); + }, 100 * failedCount); + }; + + socket.onopen = function () { + failedCount = 0; + try { + var settings = {}; + socket.send(JSON.stringify({ join: session.whipView })); + } catch (e) { + connecting = setTimeout(function () { + connect(); + }, 1); + } + }; + + socket.addEventListener("message", async function (event) { + if (event.data) { + var data = JSON.parse(event.data); + + if ("sdp" in data) { + try { + var resp = await processWhipIn(data); + } catch (e) { + var resp = e && (e.message || e.toString()); + } + if (resp) { + var ret = {}; + var get = data.get; + data = {}; + if (get) { + data.get = get; + data.result = resp; + ret.callback = data; + socket.send(JSON.stringify(ret)); + } + } + } else if (data.type === "candidate") { + if (data.candidate && data.streamID) { + await handleIncomingIceCandidate(data); + } + } else if (data.type == "delete") { + warnlog("WHIP publisher is actively disconnecting"); + // session.closeRPC(i, true); + var ret = {}; + var get = data.get; + data = {}; + if (get) { + data.get = get; + data.result = "OK"; + ret.callback = data; + socket.send(JSON.stringify(ret)); + } + } else { + warnlog("Unsupported incoming data"); + } + } + }); + } + connect(); +} +async function processWhipIn(data) { + // LISTEN FOR REMOTE WHIP (from OBS?) + var msg = {}; + msg.description = {}; + msg.description.type = "offer"; + msg.description.sdp = data.sdp; + var UUID = session.generateRandomString(25); // fake + msg.UUID = UUID; + + if (session.forceNoAudioWhipIn || session.forceNoVideoWhipIn) { + try { + log(msg.description.sdp + ""); + msg.description.sdp = CodecsHandler.modifySdp(msg.description.sdp, session.forceNoAudioWhipIn, session.forceNoVideoWhipIn); + } catch (e) { + errorlog(e); + } + } + + if (!msg.description.sdp.includes("a=group:BUNDLE")) { // handling of bundle-only media lines; gstreamer's whipsink 1.25 for example + try { + const sdpLines = msg.description.sdp.split('\r\n'); + const bundleLines = sdpLines.filter(line => line.includes('a=mid:')); + if (bundleLines.length > 0) { + const mids = bundleLines.map(line => line.split(':')[1]); + sdpLines.splice(1, 0, `a=group:BUNDLE ${mids.join(' ')}`); + msg.description.sdp = sdpLines.join('\r\n'); + } + } catch (e) { + errorlog(e); + } + } + + if (data.streamID) { + msg.streamID = data.streamID; + } else { + msg.streamID = session.generateRandomString(15); // fake + } + + log("setupIncoming"); + await session.setupIncoming(msg); // could end up setting up the peer the wrong way. + + session.rpcs[UUID].whip = true; + var callback = null; + var promise = new Promise((resolve, reject) => { + callback = resolve; + }); + session.rpcs[UUID].whipCallback = callback; + + var callback2 = null; + var promise2 = new Promise((resolve, reject) => { + callback2 = resolve; + }); + session.rpcs[UUID].whipCallback2 = callback2; + + log("CONNECT PEER"); + session.connectPeer(msg); + log("CONNECT PEER DONE"); + + log("ICE BUNDLE PROMISE"); + setTimeout( + function (UUID) { + if (session.rpcs[UUID].whipCallback2) { + session.rpcs[UUID].whipCallback2([...session.rpcs[UUID].iceBundle]); + clearTimeout(session.rpcs[UUID].iceTimer); + session.rpcs[UUID].iceTimer = null; + session.rpcs[UUID].iceBundle = []; + session.rpcs[UUID].whipCallback2 = null; + } + }, + session.whepWait, + UUID + ); + + var iceBundle = await promise2; // waiting for ICE GATHER COMPLETE; default 2 second. change with &whipwait=2000 + + clearTimeout(session.rpcs[UUID].iceTimer); + session.rpcs[UUID].iceTimer = null; + session.rpcs[UUID].whipCallback2 = null; + + log("ICE BUNDLE DONE"); + log(iceBundle); + + await promise; + session.rpcs[UUID].whipCallback = null; + + sdpAnswer = session.rpcs[UUID].localDescription.sdp; + + if (session.localNetworkOnly) { + sdpAnswer = filterSDPLAN(sdpAnswer); + } + if (session.stunOnly) { // or whatever flag you want to use + sdpAnswer = filterStunOnly(sdpAnswer); + } + + //iceBundle.forEach(ice => { // not needed, since the localDescription has it embedded already, since we waited + // sdpAnswer += `a=${ice.candidate}\r\n`; + //}); + + /* + if (true){ // this code tries to force the TURN server into use, but it's not working that I can see. + + const sdpLines = sdpAnswer.split('\r\n'); + const modifiedLines = []; + let mediaSection = 0; + let candidateAdded = false; + let audioPort = null; + + for (let line of sdpLines) { + if (line.startsWith('m=')) { + mediaSection++; + if (mediaSection === 1) { + // Extract audio port + audioPort = line.split(' ')[1]; + } else if (mediaSection === 2 && audioPort) { + // Set video port to match audio port + line = `m=video ${audioPort} UDP/TLS/RTP/SAVPF 96`; + } + } + + if (line.startsWith('c=')) { + line = `c=IN IP4 51.222.12.223`; + } + + if (line.startsWith('a=candidate:') && !candidateAdded) { + line = `a=candidate:1 1 UDP 2 51.222.12.223 3478 typ relay raddr 0.0.0.0 rport 0`; + candidateAdded = true; + } + + modifiedLines.push(line); + } + + return modifiedLines.join('\r\n'); + } + */ + + return sdpAnswer; // return SDP answer for the remote WHIP request +} +async function handleIncomingIceCandidate(data) { + const UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === data.streamID); + if (UUID && session.rpcs[UUID]) { + try { + await session.rpcs[UUID].addIceCandidate(new RTCIceCandidate(data.candidate)); + log("Added incoming ICE candidate for stream: " + data.streamID); + } catch (e) { + errorlog("Error adding incoming ICE candidate: ", e); + } + } else { + warnlog("Received ICE candidate for unknown stream: " + data.streamID); + } +} +function processSDPFromServer(sdp) { + // not the description package; just the sdp + try { + if (session.mono && Firefox) { + // chrome defaults to mono already, but we can force Firefox mono if needed + sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 0 }, true); + } else if (Firefox) { + // Let Firefox be Firefox.. else it might break the server. mediamtx err 400 response if mono + } else if (session.stereo && session.stereo == 4) { + // pro audio only when viewing streams + sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 2 }, true); + } else if (session.stereo && !session.mono && session.stereo != 3) { + sdp = CodecsHandler.setOpusAttributes(sdp, { stereo: 1 }, true); + } + } catch (e) { + errorlog(e); + } + return sdp; +} + +function filterIceLAN(candidate) { + + if (typeof candidate === 'string') { + return filterSDPLAN(candidate); + } + if (!candidate) { return candidate; } + + try { + let candidateString = candidate.candidate; + let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/; + let isMDNSHostname = candidateString.includes(".local"); + + if (!(candidateString.includes("typ host") && (privateIPPattern.test(candidateString) || isMDNSHostname))) { + log("🟧 dropped candidate due to not being a LAN candidate"); + return false; + } + log("🟢 candidate allowed since host-type and has a LAN-IP or is a .local hostname"); + } catch (e) { + errorlog(e); + } + return true; +} +function filterSDPLAN(sdp) { + try { + return sdp + .split("\n") + .filter(line => { + if (line.startsWith("a=candidate:")) { + let parts = line.split(" "); + let type = parts[7]; // The 'typ' field in the candidate string + let address = parts[4]; + let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/; + let isMDNSHostname = address.endsWith(".local"); + log("🟥 dropped candidate from SDP due to not being a LAN candidate"); + return type === "host" && (privateIPPattern.test(address) || isMDNSHostname); + } + return true; + }) + .join("\n"); + } catch (e) { + errorlog(e); + return sdp; + } +} + +function filterStunOnlySDP(sdpString) { + try { + const lines = sdpString.split('\r\n'); + const filteredLines = lines.filter(line => { + // If it's a candidate line + if (line.startsWith('a=candidate:')) { + // Only keep STUN candidates + return line.includes(' typ srflx '); + } + + // Keep everything that's not a candidate line + return true; + }); + + // Process the filtered SDP + let processedLines = []; + let currentMedia = null; + let stunPort = null; + let stunAddress = null; + + // First pass - find STUN candidate details + for (const line of filteredLines) { + if (line.includes(' typ srflx ')) { + const parts = line.split(' '); + for (let i = 0; i < parts.length; i++) { + if (parts[i] === 'typ' && parts[i + 1] === 'srflx') { + stunPort = parts[5]; + stunAddress = parts[4]; + break; + } + } + } + } + + // Second pass - build the SDP with STUN info + for (const line of filteredLines) { + if (line.startsWith('m=')) { + currentMedia = line.split(' ')[0].substr(2); + if (stunPort && (currentMedia === 'audio' || currentMedia === 'video')) { + // Use STUN port for media lines + const parts = line.split(' '); + parts[1] = stunPort; + processedLines.push(parts.join(' ')); + } else { + processedLines.push(line); + } + } else if (line.startsWith('c=') && stunAddress) { + // Use STUN address for connection lines + processedLines.push(`c=IN IP4 ${stunAddress}`); + } else { + processedLines.push(line); + } + } + + return processedLines.join('\r\n'); + } catch (e) { + errorlog(e); + return sdpString; // Return original on error + } +} + +// If you need to modify the individual candidate filter as well: +function filterStunOnly(candidate) { + if (typeof candidate === 'string') { + return filterStunOnlySDP(candidate); + } + if (!candidate) { return candidate; } + console.log(candidate); + try { + let candidateString = candidate.candidate; + + // Only allow STUN candidates + if (!candidateString.includes("typ srflx")) { + log("🟧 Dropped non-STUN candidate"); + return false; + } + + // Make sure it's not containing private IP ranges + let privateIPPattern = /(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))[0-9\.]*/; + if (privateIPPattern.test(candidateString)) { + log("🟧 Dropped STUN candidate with private IP"); + return false; + } + + log("🟢 Allowed STUN candidate"); + return true; + } catch (e) { + errorlog(e); + return false; + } +} + +/** + * IPv6 Preference Handling + * + * These functions help prefer IPv4 over IPv6 for WebRTC ICE candidates. + * This is useful for networks where IPv6 connectivity is flaky (e.g., firewalls + * that drop UDP on IPv6, or unstable IPv6 routing) while IPv4 works reliably. + * + * Default behavior: IPv4 candidates are prioritized (sent first in bundles) + * With &ipv6=0 or &preferipv4: IPv6 candidates are dropped entirely if IPv4 exists + * + * IPv6-only networks continue to work: if no IPv4 candidates exist, IPv6 is used. + */ + +/** + * Detects if an ICE candidate uses IPv6. + * IPv6 addresses contain colons, while IPv4 uses dots. + * Also handles mDNS hostnames (.local) which are treated as non-IPv6. + * + * @param {RTCIceCandidate|string} candidate - The ICE candidate to check + * @returns {boolean} true if the candidate is IPv6, false otherwise + */ +function isIpv6Candidate(candidate) { + if (!candidate) return false; + + try { + var candidateString = (typeof candidate === 'string') + ? candidate + : (candidate.candidate || ''); + + if (!candidateString) return false; + + // Parse the candidate string to extract the address + // Format: "candidate:...
    ..." + var parts = candidateString.split(' '); + if (parts.length < 5) return false; + + var address = parts[4]; + + // mDNS hostnames (.local) are not IPv6 + if (address && address.endsWith('.local')) { + return false; + } + + // IPv6 addresses contain colons, IPv4 uses dots only + // Examples: + // IPv4: "192.168.1.1" + // IPv6: "2001:db8::1", "::1", "fe80::1%eth0" + if (address && address.includes(':')) { + return true; + } + + return false; + } catch (e) { + errorlog("isIpv6Candidate error:", e); + return false; + } +} + +/** + * Filters IPv6 candidates from an SDP string. + * Used when session.disableIpv6 is true and IPv4 candidates exist. + * + * @param {string} sdp - The SDP string to filter + * @param {boolean} hasIpv4 - Whether IPv4 candidates exist in this SDP + * @returns {string} The filtered SDP string + */ +function filterSdpIpv6(sdp, hasIpv4) { + if (!sdp || typeof sdp !== 'string') return sdp; + + // If no IPv4 exists, we must keep IPv6 for connectivity + if (!hasIpv4) { + log("🟡 IPv6 kept in SDP: no IPv4 candidates available (IPv6-only network)"); + return sdp; + } + + try { + var lines = sdp.split('\n'); + var filteredLines = lines.filter(function(line) { + if (line.startsWith('a=candidate:')) { + var parts = line.split(' '); + if (parts.length >= 5) { + var address = parts[4]; + // Drop IPv6 candidates (contain colons, not .local) + if (address && address.includes(':') && !address.endsWith('.local')) { + log("🟧 Dropped IPv6 candidate from SDP: " + address); + return false; + } + } + } + return true; + }); + return filteredLines.join('\n'); + } catch (e) { + errorlog("filterSdpIpv6 error:", e); + return sdp; + } +} + +/** + * Checks if an SDP string contains any IPv4 candidates. + * Used to determine if we can safely filter out IPv6. + * + * @param {string} sdp - The SDP string to check + * @returns {boolean} true if IPv4 candidates exist + */ +function sdpHasIpv4Candidates(sdp) { + if (!sdp || typeof sdp !== 'string') return false; + + try { + var lines = sdp.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (line.startsWith('a=candidate:')) { + var parts = line.split(' '); + if (parts.length >= 5) { + var address = parts[4]; + // IPv4: contains dots, no colons, not .local + if (address && !address.includes(':') && !address.endsWith('.local')) { + return true; + } + // mDNS (.local) counts as non-IPv6, so treat as potential IPv4 + if (address && address.endsWith('.local')) { + return true; + } + } + } + } + } catch (e) { + errorlog("sdpHasIpv4Candidates error:", e); + } + return false; +} + +/** + * Reorders ICE candidates to prioritize IPv4 over IPv6. + * IPv4 candidates are moved to the front of the array. + * This helps ensure IPv4 is tried first when both are available. + * + * @param {Array} candidates - Array of RTCIceCandidate objects + * @returns {Array} Reordered array with IPv4 candidates first + */ +function reorderCandidatesIpv4First(candidates) { + if (!candidates || !Array.isArray(candidates) || candidates.length === 0) { + return candidates; + } + + try { + var ipv4Candidates = []; + var ipv6Candidates = []; + + for (var i = 0; i < candidates.length; i++) { + if (isIpv6Candidate(candidates[i])) { + ipv6Candidates.push(candidates[i]); + } else { + ipv4Candidates.push(candidates[i]); + } + } + + // Return IPv4 first, then IPv6 + return ipv4Candidates.concat(ipv6Candidates); + } catch (e) { + errorlog("reorderCandidatesIpv4First error:", e); + return candidates; + } +} + +/** + * Filters out IPv6 candidates from an array if IPv4 candidates exist. + * Used when session.disableIpv6 is true. + * + * @param {Array} candidates - Array of RTCIceCandidate objects + * @returns {Object} { filtered: Array, hasIpv4: boolean, droppedIpv6: number } + */ +function filterIpv6FromCandidates(candidates) { + var result = { + filtered: [], + hasIpv4: false, + droppedIpv6: 0 + }; + + if (!candidates || !Array.isArray(candidates)) { + return result; + } + + var ipv4Candidates = []; + var ipv6Candidates = []; + + for (var i = 0; i < candidates.length; i++) { + if (isIpv6Candidate(candidates[i])) { + ipv6Candidates.push(candidates[i]); + } else { + ipv4Candidates.push(candidates[i]); + } + } + + result.hasIpv4 = ipv4Candidates.length > 0; + + if (result.hasIpv4) { + // We have IPv4, so we can drop IPv6 + result.filtered = ipv4Candidates; + result.droppedIpv6 = ipv6Candidates.length; + if (ipv6Candidates.length > 0) { + log("🟧 Dropped " + ipv6Candidates.length + " IPv6 candidate(s) since IPv4 is available"); + } + } else { + // No IPv4, must use IPv6 for connectivity + result.filtered = ipv6Candidates; + if (ipv6Candidates.length > 0) { + log("🟡 Using " + ipv6Candidates.length + " IPv6 candidate(s): no IPv4 available (IPv6-only network)"); + } + } + + return result; +} + +/** + * Filters IPv6 candidates from an RTCSessionDescription if needed. + * Used when sending SDP offers/answers to filter embedded candidates. + * + * @param {RTCSessionDescription} description - The description to filter + * @returns {RTCSessionDescription|Object} Filtered description (new object if filtered, original if not) + */ +function filterDescriptionIpv6(description) { + if (!description || !description.sdp) return description; + + // Only filter if disableIpv6 is set + if (!session.disableIpv6) return description; + + try { + var sdp = description.sdp; + + // Check if there are IPv4 candidates - if not, keep IPv6 for connectivity + if (!sdpHasIpv4Candidates(sdp)) { + log("🟡 IPv6 kept in SDP description: no IPv4 candidates (IPv6-only network)"); + return description; + } + + // Filter IPv6 from the SDP + var filteredSdp = filterSdpIpv6(sdp, true); + + // Return new description object with filtered SDP + return { + type: description.type, + sdp: filteredSdp + }; + } catch (e) { + errorlog("filterDescriptionIpv6 error:", e); + return description; + } +} + +async function whepIn(whepInput = false, whepInputToken = false, UUID = false) { + // PLAY WHEP; will continually bring it in, and retry continuously + var candidates = []; + var responseLocation = false; + var acceptPatch = false; + var eTag = false; + var icePwd = false; + var iceUfrag = false; + // var reconnect = null; + //var maxRetries = 5; + //var delay = 2000; + + if (!UUID) { + UUID = "whep_" + session.generateRandomString(25); // fake + } + + whepInput = whepInput || session.whepInput; + if (!whepInput) { + errorlog("no whepInput"); + return; + } + whepInputToken = whepInputToken || session.whepInputToken; + + async function whepConnect() { + //return new Promise((resolve, reject) => { + try { + if (!(UUID in session.rpcs)) { + session.rpcs[UUID] = {}; + } + ensureViewerRpcDefaults(UUID); + + if (!session.configuration) { + await chooseBestTURN(); + } + + if (session.encodedInsertableStreams) { + // most servers won't support this + session.configuration.encodedInsertableStreams = true; + } + + var config = { ...session.configuration }; + + // if (whepInput.includes("cloudflare")){ + // config.iceTransportPolicy = "relay"; // oof. Doesn't work with Cloudflare without this? REVISIT (Update: I guess they fixed it? Oct 23rd its working without it) + // } + + try { + session.rpcs[UUID].whep = new RTCPeerConnection(config); + } catch (err) { + errorlog(err); + if (!session.cleanOutput) { + warnUser("An RTC error occured"); + } + } + + var video = true; + var audio = true; + + if (session.novideo !== false && !session.novideo.includes(session.rpcs[UUID].streamID)) { + video = false; + } else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.video) { + video = false; + } + if (session.noaudio !== false && !session.noaudio.includes(session.rpcs[UUID].streamID)) { + audio = false; + } else if (session.excludeaudio && session.excludeaudio.includes(session.rpcs[UUID].streamID)) { + audio = false; + } else if (session.rpcs[UUID].settings && !session.rpcs[UUID].settings.audio) { + audio = false; + } + + if (!audio && !video) { + errorlog("We will not request the whep source as no audio or video is requested"); + return; + } + + if (video) { + disableQualityDirector(UUID); + } + + if (!session.manual || !session.director) { + window.onresize = updateMixer; + window.onorientationchange = function () { + setTimeout(updateMixer, 200); + }; + } + + try { + if (video) { + session.rpcs[UUID].whep.addTransceiver("video", { direction: "recvonly" }); + } + if (audio) { + session.rpcs[UUID].whep.addTransceiver("audio", { direction: "recvonly" }); + } + } catch (e) { + errorlog(e); + } + + session.rpcs[UUID].whep.ontrack = function (event) { + warnlog("TRACK INBOUND!"); + warnlog(event); + session.onTrack(event, UUID); + // maxRetries = 5; // reset allowed reconnection limit + let track = null; + if (event.streams && event.streams[0]) { + try { + let newStream = event.streams[0]; + track = newStream.getVideoTracks()[0]; + } catch (e) { } + } else if (event.track && event.track.kind && (event.track.kind == "video")) { + track = event.track + } + if (track) { + log(track); + setTimeout(function (track, UUID) { + if (session.rpcs[UUID] && track && track.id) { + if (session.rpcs[UUID].stats && session.rpcs[UUID].stats[track.id] && ("keyFramesRequested_pli" in session.rpcs[UUID].stats[track.id])) { + // TODO ?? maybe not,b ut console.log("I need to request a keyframe. keyFramesRequested_pli: "+session.rpcs[UUID].stats[track.id].keyFramesRequested_pli); + } + } + }, 6100, track, UUID); // 3 seconds for stats to update + 2 second for keyframe to trigger + 1100 for delay + } + + // IF track is a video.. + // wait to see if a keyframe is received within 2 seconds + // if not keyframe, request one. + // session.whipOutKeyframeOnNewViewer + // session.rpcs[UUID].stats[trackID].keyFramesRequested_pli = stat.pliCount || 0; + /* if ("pliCount" in stat) { + data.stats.total_pli_count = stat.pliCount; + } + + if ("keyFramesEncoded" in stat) { + data.stats.total_key_frames_encoded = stat.keyFramesEncoded; + } */ + }; + } catch (err) { + errorlog(err); + if (!session.cleanOutput) { + warnUser("An RTC error occured"); + } + } + + session.rpcs[UUID].whep.onnegotiationneeded = requestStream; // bug: https://groups.google.com/forum/#!topic/discuss-webrtc/3-TmyjQ2SeE + + session.rpcs[UUID].whep.oniceconnectionstatechange = function () { + if (session.rpcs[UUID] && session.rpcs[UUID].whep && (session.rpcs[UUID].whep.iceConnectionState === 'disconnected' || + session.rpcs[UUID].whep.iceConnectionState === 'failed')) { + console.warn("ICE connection failed or disconnected"); + retryWhepConnection(UUID); + } + }; + + session.rpcs[UUID].whep.onconnectionstatechange = function () { + if (session.rpcs[UUID] && session.rpcs[UUID].whep && session.rpcs[UUID].whep.connectionState === 'failed') { + console.warn("Whep connection failed"); + retryWhepConnection(UUID); + } + }; + + session.rpcs[UUID].whep.onicecandidate = function (event) { + if (!session.rpcs[UUID] || !session.rpcs[UUID].whep) { + return; + } + + if (event.candidate == null) { + warnlog("END OF ICE CANDIDATES"); + if (session.rpcs[UUID].whep.iceCompletedCallback) { + session.rpcs[UUID].whep.iceCompletedCallback(); + } + return; + } else if (eTag && icePwd && iceUfrag && acceptPatch && acceptPatch == "application/trickle-ice-sdpfrag" && event.candidate && responseLocation && !session.rpcs[UUID].whep.iceCompletedCallback) { + // "left over" candidates not sent with the SDP offer + log("Send patch request with ice candidate"); + + try { + if (session.localNetworkOnly) { + if (!filterIceLAN(event.candidate)) { + return; + } + } + if (session.stunOnly) { // or whatever flag you want to use + if (!filterStunOnly(event.candidate)) { + return; + } + } + } catch (e) { + errorlog(e); + } + + if (event.candidate.candidate) { + let patchCandidate = + "a=ice-ufrag:" + + iceUfrag + + "\r\n" + // <== what a mess.. https://datatracker.ietf.org/doc/html/draft-murillo-whep + "a=ice-pwd:" + + icePwd + + "\r\n" + + "m=audio 9 RTP/AVP 0\r\n" + + "a=mid:0\r\n" + + "a=" + + event.candidate.candidate + + "\r\n" + + "a=end-of-candidates\r\n"; + + // a=ice-ufrag:EsAw + // a=ice-pwd:P2uYro0UCOQ4zxjKXaWCBui1 + // m=audio RTP/AVP 0 + // a=mid:0 + // a=candidate:1387637174 1 udp 2122260223 192.0.2.1 61764 typ host generation 0 ufrag EsAw network-id 1 + // a=candidate:3471623853 1 udp 2122194687 198.51.100.1 61765 typ host generation 0 ufrag EsAw network-id 2 + // a=candidate:473322822 1 tcp 1518280447 192.0.2.1 9 typ host tcptype active generation 0 ufrag EsAw network-id 1 + // a=candidate:2154773085 1 tcp 1518214911 198.51.100.2 9 typ host tcptype active generation 0 ufrag EsAw network-id 2 + // a=end-of-candidates + + // If-Match: "38sdf4fdsf54:EsAw" + ajax(patchCandidate, "trickle-ice-sdpfrag", false, { "if-match": eTag }); + } + } else { + try { + if (session.localNetworkOnly) { + if (!filterIceLAN(event.candidate)) { + return; + } + } + if (session.stunOnly) { // or whatever flag you want to use + if (!filterStunOnly(event.candidate)) { + return; + } + } + } catch (e) { + errorlog(e); + } + + candidates.push(event.candidate); // send later if I can? + } + //log(event.candidate); + }; + + log("onnegotiationneeded event setup"); + } + + function retryWhepConnection(UUID) { + if (!session.rpcs[UUID]) { + log("Session closed, stopping retry attempts"); + return; + } + + if (session.rpcs[UUID].suppressReconnect) { + session.rpcs[UUID].reconnecting = false; + return; + } + + const parentUUID = session.rpcs[UUID].realUUID || false; + if (parentUUID && session.rpcs[parentUUID] && session.rpcs[parentUUID].screenShareState === false) { + session.rpcs[UUID].reconnecting = false; + return; + } + + if (session.rpcs[UUID].reconnecting) { + return; + } + session.rpcs[UUID].reconnecting = true; + + const maxRetries = 5; + const initialDelay = 2000; + const maxDelay = 20000; + + let currentRetry = 0; + let currentDelay = initialDelay; + + function attemptReconnect() { + if (!session.rpcs[UUID]) { + log("Session closed during retry, stopping attempts"); + return; + } + + if (session.rpcs[UUID].suppressReconnect) { + session.rpcs[UUID].reconnecting = false; + return; + } + + const parentUUID = session.rpcs[UUID].realUUID || false; + if (parentUUID && session.rpcs[parentUUID] && session.rpcs[parentUUID].screenShareState === false) { + session.rpcs[UUID].reconnecting = false; + return; + } + + if (session.rpcs[UUID].whep && + (session.rpcs[UUID].whep.connectionState === 'connected' || + session.rpcs[UUID].whep.iceConnectionState === 'connected' || + session.rpcs[UUID].whep.iceConnectionState === 'completed')) { + log("WHEP connection is already established. No need to reconnect."); + session.rpcs[UUID].reconnecting = false; + return; + } + + log(`Attempting WHEP reconnection (attempt ${currentRetry + 1}/${maxRetries})`); + + if (session.rpcs[UUID].whep && session.rpcs[UUID].whep.close) { + session.rpcs[UUID].whep.close(); + } + + try { + if (session.rpcs[UUID].videoElement && "recorder" in session.rpcs[UUID].videoElement) { + session.rpcs[UUID].videoElement.recorder.stop(); + } + } catch (e) { + warnlog(e); + } + + try { + if (session.rpcs[UUID].streamSrc) { + session.rpcs[UUID].streamSrc.getTracks().forEach(function (track) { + track.stop(); + log("Track stopped"); + }); + session.rpcs[UUID].streamSrc = null; + } + } catch (e) { } + + try { + if (!session.rpcs[UUID].UUID && document.getElementById("container_" + UUID)) { + getById("container_" + UUID).parentNode.removeChild(getById("container_" + UUID)); + updateLockedElements(); + } + } catch (e) { + warnlog(e); + } + + try { + if (session.rpcs[UUID].videoElement) { + session.rpcs[UUID].videoElement.remove(); + session.rpcs[UUID].videoElement = null; + } + if (session.rpcs[UUID].canvas) { + session.rpcs[UUID].canvas.remove(); + } + if (session.rpcs[UUID].imageElement) { + session.rpcs[UUID].imageElement.remove(); + } + if ("eventPlayActive" in session.rpcs[UUID]) { + clearInterval(session.rpcs[UUID].eventPlayActive); + } + + if (!session.director || session.switchMode) { + setTimeout(function () { + updateMixer(); + }, 1); + } + } catch (e) { + warnlog(e); + } + + whepConnect(UUID).then(() => { + log("WHEP reconnection successful"); + session.rpcs[UUID].reconnecting = false; + }).catch(error => { + warnlog("WHEP reconnection failed:", error); + currentRetry++; + if (currentRetry < maxRetries) { + currentDelay = Math.min(currentDelay * 2, maxDelay); + log(`Retrying in ${currentDelay}ms`); + setTimeout(attemptReconnect, currentDelay); + } else { + log("Max retries reached, stopping reconnection attempts"); + session.rpcs[UUID].reconnecting = false; + // Implement user feedback here + } + }); + } + + attemptReconnect(); + } + + var requestingStream = false; + function requestStream(event) { + if (requestingStream) { + log(event); + errorlog("onnegotiationneeded again?"); + return; + } + requestingStream = true; + warnlog("ON NEGO NEEDED"); + warnlog(event); + + try { + session.rpcs[UUID].whep + .createOffer() + .then(async function (offer) { + if (session.localNetworkOnly) { + offer.sdp = filterSDPLAN(offer.sdp); + } + if (session.stunOnly) { // or whatever flag you want to use + offer.sdp = filterStunOnly(offer.sdp); + } + + offer.sdp = processSDPFromServer(offer.sdp); + //offer.sdp = CodecsHandler.setOpusAttributes(offer.sdp, {'stereo': 1}); + return session.rpcs[UUID].whep.setLocalDescription(offer); + }) + .then(async function () { + //log(session.rpcs[UUID].whep.localDescription); + + try { + if (session.whepWait) { + log("Waiting for ice candidates to collect. At least 300ms recommended; at most 30-seconds."); + let startTime = Date.now(); + const { promise, resolve } = sleepCancellable(session.whepWait); // 2000ms default; I want to give the ICE / STUN / TURN time to collect. + session.rpcs[UUID].whep.iceCompletedCallback = resolve; // Can complete earlier if possible. + await promise; // pausing for a moment; until all collected or timed out + log("Finished waiting for ice candidates. Waited " + (Date.now() - startTime) / 1000 + "-seconds"); + delete session.rpcs[UUID].whep.iceCompletedCallback; + } + } catch (e) { + errorlog(e); + } + + // candidates = []; // clear collected candidates so far, as they are part of the localDescription's footer probably + var sdp = session.rpcs[UUID].whep.localDescription.sdp; + sdp = processSDPFromServer(sdp); + + //description.sdp = processSDPFromServer(description.sdp); // cause server to not send + if (sdp.includes("sendrecv")) { + errorlog("Should not include sendrecv"); + sdp = sdp.replace("a=sendrecv", "a=recvonly"); + sdp = sdp.replace("v=sendrecv", "v=recvonly"); + } + ajax(sdp, "sdp"); + }) + .catch(function (err) { + requestingStream = false; + }); + } catch (e) { + requestingStream = false; + errorlog(e); + } + } + + function ajax(dataPayload, type, callback = false, headers = false) { + // https://datatracker.ietf.org/doc/html/draft-murillo-whep + //log(dataPayload); + try { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function () { + if (this.readyState == 4 && (this.status == 200 || this.status == 201)) { + try { + // 200 not in spec (meant to be an options response), but I want to be flexible + let headers = xhttp.getAllResponseHeaders(); + var contentType = false; + if (headers.indexOf("content-type") >= 0) { + contentType = this.getResponseHeader("content-type"); + } + if (headers.indexOf("location") >= 0) { + responseLocation = this.getResponseHeader("location"); + } + if (headers.indexOf("accept-patch") >= 0) { + acceptPatch = this.getResponseHeader("accept-patch"); + } + if (headers.indexOf("etag") >= 0) { + eTag = this.getResponseHeader("etag"); + } + if (responseLocation && !(responseLocation.startsWith("http://") || responseLocation.startsWith("https://"))) { + let requestURL = new URL(whepInput); // Replace 'yourRequestURL' with the URL you posted to. + let protocol = requestURL.protocol; + let hostname = requestURL.hostname; + let port = requestURL.port || (protocol === "https:" ? "443" : "80"); // Default port based on protocol + responseLocation = `${protocol}//${hostname}:${port}${responseLocation}`; + } + if (contentType && contentType.startsWith("application/sdp")) { + var description = {}; + description.sdp = this.responseText; + description.type = "answer"; + warnlog("Processing answer:"); + iceUfrag = description.sdp.match(/a=ice-ufrag:(.*)\r\n/); + if (iceUfrag) { + iceUfrag = iceUfrag[1]; + } + icePwd = description.sdp.match(/a=ice-pwd:(.*)\r\n/); + if (icePwd) { + icePwd = icePwd[1]; + } + if (session.localNetworkOnly) { + description.sdp = filterSDPLAN(description.sdp); + } + if (session.stunOnly) { // or whatever flag you want to use + description.sdp = filterStunOnly(description.sdp); + } + description.sdp = processSDPFromServer(description.sdp); // setup stereo/mono + session.rpcs[UUID].whep + .setRemoteDescription(description) + .then(function () { + warnlog("SHOULD BE CONNECTED?"); + requestingStream = false; + }) + .catch(function (e) { + log(e); + requestingStream = false; + }); + // the request is done, but lets handle any old ice candidates + if (eTag && icePwd && iceUfrag && acceptPatch && acceptPatch == "application/trickle-ice-sdpfrag" && candidates.length && responseLocation && !session.rpcs[UUID].whep.iceCompletedCallback) { + // "left over" candidates not sent with the SDP offer + log("Send patch request with ice candidates"); + let patchCandidates = + "a=ice-ufrag:" + + iceUfrag + + "\r\n" + // <== what a mess.. https://datatracker.ietf.org/doc/html/draft-murillo-whep + "a=ice-pwd:" + + icePwd + + "\r\n" + + "m=audio 9 RTP/AVP 0\r\n" + // if I leave out the port (9), then MediaMTX breaks, but this is not in the draft spec as linked above + "a=mid:0\r\n"; + candidates.forEach(candidate => { + patchCandidates += "a=" + candidate.candidate + "\r\n"; + }); + candidates = []; + patchCandidates += "a=end-of-candidates\r\n"; + // + // If-Match: "38sdf4fdsf54:EsAw" + ajax(patchCandidates, "trickle-ice-sdpfrag", false, { "if-match": eTag }); + } else { + warnlog("Trickling candidates via PATCH requests not supported it seems"); + } + } else if (contentType == "application/error") { + if (!session.cleanOutput) { + warnUser("Unknown WHEP playback error"); + } + } else if (callback) { + callback(); + } + } catch (e) { + requestingStream = false; + } + } else if (this.readyState == 4 && this.status == 204) { + requestingStream = false; + if (type == "trickle-ice-sdpfrag") { + // patch candidate request accepted? + } else { + // not in spec? + } + } else if (this.readyState == 4) { + requestingStream = false; + // console.warn(this.status, this.readyState); + if (this.status == 432) { + } else if (this.status == 405) { + // GET, HEAD or PUT not allowed atm + } else if (this.status == 501) { + } else if (this.status == 412) { + // etag trickle did not match + } + if (!this.status || this.status >= 400) { + if (type === "sdp") { + retryWhepConnection(UUID); + } + } + } + }; + if (type === "trickle-ice-sdpfrag") { + if (responseLocation) { + xhttp.open("PATCH", responseLocation, true); + } else { + xhttp.open("PATCH", whepInput, true); // cause who knows. worth trying? + } + } else { + xhttp.open("POST", whepInput, true); + } + xhttp.setRequestHeader("Content-Type", "application/" + type); + + if (whepInputToken) { + xhttp.setRequestHeader("Authorization", "Bearer " + whepInputToken); // spam it on every request; fail safe + } + + if (headers) { + Object.keys(headers).forEach(key => { + xhttp.setRequestHeader(key, headers[key]); + }); + } + xhttp.onerror = function (e) { + errorlog(e); + requestingStream = false; + if (!session.cleanOutput) { + if (whepInput.startsWith("https://")) { + if (location.protocol !== "https:") { + warnUser("WHEP playback failed.\n\nThe website needs to be loaded via https (ssl) to access media devices."); + } else if ("isSecureContext" in window && window.isSecureContext === false) { + warnUser("WHEP playback failed.\n\nThe website may have assets loaded in an insecure context."); + } else { + retryWhepConnection(UUID); + } + } else { + // vdo.ninja itself is secure + if (location.protocol === "https:") { + if (location.hostname == "vdo.ninja") { + warnUser("WHEP playback failed.\n\nThe WHEP URL needs to be using https if from an SSL-enabled website.\n\nPerhaps try using http://insecure.vdo.ninja instead.", false, false); + } else { + warnUser("WHEP playback failed.\n\nThe WHEP URL needs to be using https if from an SSL-enabled website."); + } + } else if ("isSecureContext" in window && window.isSecureContext === false) { + warnUser("WHEP playback failed.\n\nThe website may have assets loaded in a secure context."); + } else { + retryWhepConnection(UUID); + } + } + } + }; + + xhttp.send(dataPayload); + } catch (e) { + requestingStream = false; + errorlog(e); + } + } + + whepConnect(); + return UUID; +} +//////// +function whepOut() { + // publish to whep.vdo.ninja with obs, to use. experimental + if (!session.whepHost) { + return; + } + warnlog("WHEP Client started"); + + var socket = null; + var connecting = false; + var failedCount = 0; + + function connect() { + clearTimeout(connecting); + if (socket) { + if (socket.readyState === socket.OPEN) { + return; + } + try { + socket.close(); + } catch (e) { } + } + log("Trying to load whep websocket..."); + + socket = new WebSocket("wss://whep.vdo.ninja"); + + socket.onclose = function () { + failedCount += 1; + clearTimeout(connecting); + connecting = setTimeout(function () { + connect(); + }, 100 * (failedCount - 1)); + }; + + socket.onerror = function (e) { + console.error(e); + failedCount += 1; + clearTimeout(connecting); + connecting = setTimeout(function () { + connect(); + }, 100 * failedCount); + }; + + socket.onopen = function () { + failedCount = 0; + try { + var settings = {}; + socket.send(JSON.stringify({ join: session.whepHost })); + } catch (e) { + connecting = setTimeout(function () { + connect(); + }, 1); + } + }; + + socket.addEventListener("message", async function (event) { + if (event.data) { + log(event.data); + var data = JSON.parse(event.data); + + if ("sdp" in data) { + var resp = await processWHEPout(data); + if (resp) { + var ret = {}; + var get = data.get; + data = {}; + if (get) { + data.get = get; + data.result = resp; + ret.callback = data; + log(ret); + socket.send(JSON.stringify(ret)); + } + } + } else if (data.type == "delete") { + warnlog("WHIP Client is actively disconnecting"); + // session.closeRPC(i, true); + var ret = {}; + var get = data.get; + data = {}; + if (get) { + data.get = get; + data.result = "OK"; + ret.callback = data; + log(ret); + socket.send(JSON.stringify(ret)); + } + } + } + }); + } + connect(); +} + +async function processWHEPout(data) { + // LISTEN FOR REMOTE WHIP + + var description = {}; + description.type = "offer"; + + var isGstreamer = false; + description.sdp = data.sdp.replace(/a=rtpmap:111 OPUS\/48000\r\n/g, "a=rtpmap:111 opus/48000/2\r\n"); // gstreamer fix + if (description.sdp !== data.sdp) { + isGstreamer = true; // ugh. i'll need to revisit when gstreamer/whepsrc improves. + } + + var UUID = session.generateRandomString(25) + "_whepout"; // client side made up; just needs to be unique is all; + + if (UUID in session.pcs) { + UUID = session.generateRandomString(25) + "_whepout"; // ha, pretty pointless. + } + + try { + if (!session.configuration) { + await chooseBestTURN(); + } + if (session.encodedInsertableStreams) { + // most servers won't support this + session.configuration.encodedInsertableStreams = true; + } + var config = { ...session.configuration }; + + session.pcs[UUID] = new RTCPeerConnection(config); + + session.pcs[UUID].onicecandidate = event => { + if (event.candidate) { + // Handle ICE candidate.. or not. + } + // try { + // if (session.localNetworkOnly) { + //if (session.localNetworkOnly){ + // if (!filterIceLAN(event.candidate)){ + // return; + // } + // } + // } + // } catch(e) {errorlog(e);} + }; + + if (session.localNetworkOnly) { + description.sdp = filterSDPLAN(description.sdp); + } + if (session.stunOnly) { // or whatever flag you want to use + description.sdp = filterStunOnly(description.sdp); + } + + try { + await session.pcs[UUID].setRemoteDescription(description); + } catch (e) { + errorlog(e); + errorlog("If you are seeing this error, the browser likely isn't able to match what you are requesting. Compare a normal browser-create SDP with what you are offering."); + } + + const transceivers = session.pcs[UUID].getTransceivers(); + + transceivers.forEach(transceiver => { + const direction = transceiver.currentDirection || transceiver.direction; + // Set to sendonly or sendrecv if not already set and if there is a local track to send + if (direction !== "sendrecv" && direction !== "sendonly") { + transceiver.direction = "sendonly"; // or 'sendrecv' if you also want to receive + } else { + return; + } + // Assuming you have a track to send + session.videoElement.srcObject.getAudioTracks().forEach(track => { + if (transceiver.direction.includes("send")) { + if (transceiver.sender.track && transceiver.sender.track.kind != "audio") { + return; + } else if (transceiver.receiver.track && transceiver.receiver.track.kind != "audio") { + return; + } + + transceiver.sender + .replaceTrack(track) + .then(() => { + log(`Added track: ${track.kind}`); + }) + .catch(errorlog); + } + }); + + session.videoElement.srcObject.getVideoTracks().forEach(track => { + if (transceiver.direction.includes("send")) { + if (transceiver.sender.track && transceiver.sender.track.kind != "video") { + return; + } else if (transceiver.receiver.track && transceiver.receiver.track.kind != "video") { + return; + } + + transceiver.sender + .replaceTrack(track) + .then(() => { + log(`Added track: ${track.kind}`); + }) + .catch(errorlog); + } + }); + }); + + //await sleep(300); // give it time to collect ice candidates. too lazy to do a promise callback right now + + const localDescription = await session.pcs[UUID].createAnswer(); + + if (session.localNetworkOnly) { + localDescription.sdp = filterSDPLAN(localDescription.sdp); + } + + if (session.stunOnly) { // or whatever flag you want to use + localDescription.sdp = filterStunOnly(localDescription.sdp); + } + + await session.pcs[UUID].setLocalDescription(localDescription); + + await sleep(session.whepWait); + + if (isGstreamer) { + return session.pcs[UUID].localDescription.sdp.replace("a=rtpmap:111 opus/48000/2\r\n", "a=rtpmap:111 OPUS/48000\r\n"); // not sure if this makes sense tho. + } + + return session.pcs[UUID].localDescription.sdp; + //} + } catch (error) { + console.error("Error setting up the peer connection:", error); + } +} +///// +function pokePostAPI(action, data, streamID) { + var msg = {}; + msg.update = {}; + msg.update.streamID = streamID || session.streamID || null; + msg.update.action = action; + msg.update.value = data; + + try { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function () { + if (this.readyState == 4 && (this.status == 200 || this.status == 201)) { + log("good"); + } else { + warnlog("post api didn't work?"); + } + }; + xhttp.open("POST", session.postApi, true); + xhttp.setRequestHeader("Content-type", "application/json"); + + xhttp.onerror = function (e) { + errorlog(e); + }; + xhttp.send(JSON.stringify(msg)); + } catch (e) { + errorlog(e); + } +} + +var queuedSendingAPIMsgs = []; +function pokeAPI(action, data, streamID = null) { + if (session.postApi) { + pokePostAPI(action, data, streamID); + } + + if (!session.api) { + return; + } + + if (session.apiSocket) { + try { + var msg = {}; + msg.update = {}; + msg.update.streamID = streamID || session.streamID || null; + msg.update.action = action; + msg.update.value = data; + session.apiSocket.send(JSON.stringify(msg)); + } catch (e) { + errorlog(e); + } + } else if (session.apiSocket !== null) { + queuedSendingAPIMsgs.push([action, data, streamID]); + if (queuedSendingAPIMsgs.length > 20) { + queuedSendingAPIMsgs.shift(); + } + } +} + +function pokeDiscord(action, data = {}) { + if (!session || !session.discordHook) { return; } + const { streamID, label, ses, hangup, startTime } = data; + if (hangup && !session.discordHookSensitive) return; + const duration = Math.floor((Date.now() - (startTime || 0)) / 1000); + const message = { + embeds: [{ + title: "Incoming stream disconnected unexpectedly and it didn't reconnect in 60s", + color: 0xFF0000, + fields: [ + { name: "Incoming Stream ID", value: streamID || "Unknown", inline: true }, + { name: "Duration of connection", value: `${duration}s`, inline: true } + ], + timestamp: new Date().toISOString() + }] + }; + if (label) message.embeds[0].fields.push({ name: "Viewer", value: label, inline: true }); + if (session.label) message.embeds[0].fields.push({ name: "Publisher", value: session.label, inline: true }); + setTimeout(() => { + // Check if stream is reconnected by looking through all RPCs + const isReconnected = Object.values(session.rpcs || {}).some(rpc => rpc.streamID === streamID); + if (!isReconnected) { + fetch(session.discordHook, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(message) + }).catch(err => console.error('Discord webhook failed:', err)); + } + }, 60000); +} + +function getGuestTarget(type, id) { + var element = document.querySelector('[data-sid="' + id + '"][data-action-type="' + type + '"], [data-sid="' + id + '"] [data-action-type="' + type + '"]'); // data-sid="P5MQpia" + if (!element) { + return getRightOrderedElement('[data--u-u-i-d] [data-action-type="' + type + '"]', id); + } + return element; +} + +function getGuestTargetScene(scene, id) { + var element = document.querySelector('[data-action-type="addToScene"][data-scene="' + scene + '"][data-sid="' + id + '"], [data-sid="' + id + '"] [data-action-type="addToScene"][data-scene="' + scene + '"]'); // data-sid="P5MQpia" + if (!element) { + return getRightOrderedElement('[data-action-type="addToScene"][data-scene="' + scene + '"][data--u-u-i-d]', id); + } + return element; +} +function getGuestTargetGroup(group, id) { + var element = document.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"][data-sid="' + id + '"], [data-sid="' + id + '"] [data-action-type="toggle-group"][data-group="' + group + '"]'); // data-sid="P5MQpia" + if (!element) { + return getRightOrderedElement('[data-action-type="toggle-group"][data-group="' + group + '"][data--u-u-i-d]', id); + } + return element; +} + +async function targetGuest(target, action, value = null, value2 = null) { + if (target) { + if ((target == (parseInt(target) + "")) && (target < 100)) { + target -= 1; + } + } else { + target = 1; + } + warnlog("target " + target); + warnlog("action " + action); + warnlog("value " + value); + if ((action == 0) || (action == "forward") || (action == "transfer")) { + var element = getGuestTarget("forward", target); + if (element) { + return await directMigrate(element, true, value); // if value is set, it will auto transfer the guest to that room. + } else { + return false; + } + } else if ((action == 1) || (action == "addScene")) { + var scene = 1; + if (value == "null" || value == null || value == "toggle") { + scene = 1; + } else if (value !== true && value !== false) { + scene = value; + } + var element = getGuestTargetScene(scene, target); // oscid/action/target/value 1/1/scene + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directEnable(element, true); // false or true return + } + } else if (action == 2 || action == "muteScene") { + var element = getGuestTarget("mute-scene", target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directMute(element, true); // false/true + } + } else if (action == 3 || action == "mic" || action == "audio") { + var element = getGuestTarget("mute-guest", target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return remoteMute(element, true); // false/true + } + } else if (action == 4 || action == "hangup") { + var element = getGuestTarget("hangup", target); + if (element) { + return directHangup(element, true); // false or true; false if confirmed no + } + } else if (action == 5 || action == "soloChat" || action == "soloTalk") { + // see soloChatBidirectional action=9 for two-way + var element = getGuestTarget("solo-chat", target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return session.toggleSoloChat(element.dataset.UUID); + } + } else if (action == 6 || action == "speaker") { + var element = getGuestTarget("toggle-remote-speaker", target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return remoteSpeakerMute(element); + } + } else if (action == 7 || action == "display") { + var element = getGuestTarget("toggle-remote-display", target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return remoteDisplayMute(element); + } + } else if (action == 8 || action == "group") { + if (value == "null" || value == null) { + value = 1; + } + var element = getGuestTargetGroup(value, target); + if (element) { + return changeGroup(element, null, value); + } + } else if (action == 9 || action == "soloChatBidirectional" || action == "soloTalkBidirectional") { + var element = getGuestTarget("solo-chat", target); + if (element) { + var ctrl = {}; + ctrl.ctrlKey = true; + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return session.toggleSoloChat(element.dataset.UUID, ctrl); + } + } else if (action == 10 || action == "video" || action == "camera") { + var element = getGuestTarget("mute-video-guest", target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return remoteMuteVideo(element, true); // false/true + } + } else if (action == 12 || action == "addScene2") { + var element = getGuestTargetScene(2, target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directEnable(element, true); + } + } else if (action == 13 || action == "addScene3") { + var element = getGuestTargetScene(3, target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directEnable(element, true); + } + } else if (action == 14 || action == "addScene4") { + var element = getGuestTargetScene(4, target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directEnable(element, true); + } + } else if (action == 15 || action == "addScene5") { + var element = getGuestTargetScene(5, target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directEnable(element, true); + } + } else if (action == 16 || action == "addScene6") { + var element = getGuestTargetScene(6, target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directEnable(element, true); + } + } else if (action == 17 || action == "addScene7") { + var element = getGuestTargetScene(7, target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directEnable(element, true); + } + } else if (action == 18 || action == "addScene8") { + var element = getGuestTargetScene(8, target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return directEnable(element, true); + } + } else if (action == 19 || action == "forceKeyframe") { + var element = getGuestTarget("force-keyframe", target); + if (element) { + return requestKeyframeScene(element); + } + } else if (action == 20 || action == "soloVideo") { + var element = getGuestTarget("solo-video", target); + if (element) { + if (value === true) { + element.value = 1; + } else if (value === false) { + element.value = 0; + } + return requestInfocus(element); + } + } else if (action == 21 || action == "sendChat") { + var element = getGuestTarget("solo-video", target); // just something that probably exists. + if (element) { + return sendChat(value, element.dataset.UUID); + } + } else if (action == "pgm" || action == "channel") { + var element = getGuestTarget("isolate-channel", target); // just something that probably exists. + if (element) { + return directIsolateChannel(element.dataset.UUID, (parseInt(value) || null)); + } + } else if (action == 22 || action == "sendDirectorChat") { + var element = getGuestTarget("solo-video", target); // just something that probably exists. + if (element) { + return sendChat(value, element.dataset.UUID, true); + } + } else if (action == "sendPinnedDirectorChat") { + var element = getGuestTarget("solo-video", target); // just something that probably exists. + if (element) { + return sendChat(value, element.dataset.UUID, 2); + } + } else if (action == 27 || action == "volume") { + var element = getGuestTarget("volume", target); + if (element) { + element.value = parseInt(value) || 0; + return remoteVolume(element); + } + } else if ((action == 28) || (action == "setslot")) { + var element = getGuestTarget("setslot", target); + if (element) { + return setSlot(element, value); + } else { + return false; + } + } else if (action == 29 || action == "mixorder") { + var element = getGuestTarget("order-down", target); + if (element) { + if (value === true) { + changeOrder(+1, element.dataset.UUID); + } else if (value === false) { + changeOrder(-1, element.dataset.UUID); + } else { + changeOrder(value, element.dataset.UUID); + } + return true; + } else { + return false; + } + } else if (action == "requestResolution") { // director's preview or scene preview or s/e; not capture resolution + var element = getGuestTarget("solo-video", target); // just need to find the guest + if (element) { + let resolution = value.split("x"); + if (resolution.length == 2) { + session.requestResolution(element.dataset.UUID, parseInt(resolution[0]), parseInt(resolution[1])); + return true; + } else { + return "Failed. Must be WIDTHxHEIGHT"; + } + + } + return false; + } else if (action == "setWidth") { // actual capture resolution ; director only + var element = getGuestTarget("solo-video", target); // just need to find the guest + if (element) { + requestVideoHack("width", parseInt(value), element.dataset.UUID); + return true; + } + return false; + } else if (action == "setHeight") { + var element = getGuestTarget("solo-video", target); // just need to find the guest + if (element) { + requestVideoHack("height", parseInt(value), element.dataset.UUID); + return true; + } + return false; + } else if (action == "setAspectRatio") { + var element = getGuestTarget("solo-video", target); // just need to find the guest + if (element) { + requestVideoHack("aspectRatio", parseFloat(value), element.dataset.UUID); + return true; + } + return false; + } else if (action == "requestAspectRatio") { + var element = getGuestTarget("solo-video", target); // just need to find the guest + if (element) { + let maxDimension = parseInt(value2) || 1920; + let aspectRatio = 16 / 9; // default + + // Parse aspect ratio + if (value) { + if (value.includes(":")) { + let parts = value.split(":"); + aspectRatio = parseFloat(parts[0]) / parseFloat(parts[1]); + } else { + aspectRatio = parseFloat(value); + } + } + + // Calculate dimensions + let width, height; + if (aspectRatio >= 1) { + width = maxDimension; + height = Math.round(maxDimension / aspectRatio); + } else { + height = maxDimension; + width = Math.round(maxDimension * aspectRatio); + } + + session.requestResolution(element.dataset.UUID, width, height); + return true; + } + return false; + } else if (action == "startRoomTimer") { + var element = getGuestTarget("create-timer", target); + if (element) { + element.value = 0; + return directTimer(element, false, value); + } + } else if (action == "pauseRoomTimer") { + var element = getGuestTarget("create-timer", target); + if (element) { + if (element.value == 3) { + return directTimer(element, { ctrlKey: true }); + } else { + return directTimer(element, { ctrlKey: true }); + } + } + } else if (action == "stopRoomTimer") { + var element = getGuestTarget("create-timer", target); + if (element) { + element.value = 1; + return directTimer(element); + } + } else if (Commands[action]) { + try { + return Commands[action](value, target); + } catch (e) { + errorlog(e); + } + } + return false; +} + +function oscClient() { + // api.vdo.ninja api OSC (websocket / https API hotkey support). The iFrame API method provides greater customization. + if (!session.api) { + return; + } + warnlog("oscClient started"); + + var socket = null; + var connecting = false; + var failedCount = 0; + + function connect() { + clearTimeout(connecting); + if (socket) { + if (socket.readyState === socket.OPEN) { + return; + } + try { + socket.close(); + } catch (e) { } + } + socket = new WebSocket(session.apiserver); + + socket.onclose = function () { + session.apiSocket = false; + failedCount += 1; + clearTimeout(connecting); + connecting = setTimeout(function () { + connect(); + }, 100 * (failedCount - 1)); + }; + + socket.onerror = function () { + failedCount += 1; + clearTimeout(connecting); + connecting = setTimeout(function () { + connect(); + }, 100 * failedCount); + }; + + socket.onopen = function () { + failedCount = 0; + try { + socket.send(JSON.stringify({ join: session.api })); + session.apiSocket = socket; + if (queuedSendingAPIMsgs.length) { + queuedSendingAPIMsgs.forEach(msg => { + pokeAPI(msg[0], msg[1], msg[2]); + }); + queuedSendingAPIMsgs = []; + } + pokeAPI("details", getDetailedState(session.streamID)); + } catch (e) { + connecting = setTimeout(function () { + connect(); + }, 1); + } + }; + + socket.addEventListener("message", async function (event) { + if (event.data) { + var data = JSON.parse(event.data); + + if ("msg" in data) { + data = data.msg; + } + + if ("value" in data) { + if ("action" in data && data.action == "layout") { + data.value = safelyDecodeValue(data.value); + } + } + + var resp = await processMessage(data); + if (resp !== null) { + var ret = {}; + data.result = resp; + ret.callback = data; + log(ret); + socket.send(JSON.stringify(ret)); + } + } + }); + } + connect(); +} + +function safelyDecodeValue(value) { // since the layout can be a number, json, or base64 encoded json + if (Number.isInteger(Number(value))) { + return parseInt(value, 10); + } + try { + return JSON.parse(value); // Try parsing as JSON + } catch (e) { + } + try { + const decodedValue = atob(value); // Try decoding as base64 instead + try { + return JSON.parse(decodedValue); // Try parsing as JSON now that we did b64 decoding + } catch (e) { + return decodedValue; + } + } catch (e) { + return value; + } +} + +function setupCommands() { + var commands = {}; + + commands.raisehand = function (value = null, value2 = null) { + return raisehand(); + }; + commands.togglehand = function (value = null, value2 = null) { + return raisehand(); + }; + commands.togglescreenshare = function (value = null, value2 = null) { + toggleScreenShare(); + return session.screenShareState; + }; + commands.chat = function (value = null, value2 = null) { + toggleChat(value); + return session.chat; + }; + commands.speaker = function (value = null, value2 = null) { + if (value === true) { + // unmute + session.speakerMuted = false; // set + toggleSpeakerMute(true); // apply + } else if (value === false) { + // mute + session.speakerMuted = true; // set + toggleSpeakerMute(true); // apply + } else if (value === "toggle") { + // toggle + toggleSpeakerMute(); + } + return session.speakerMuted; + }; // mute speaker + commands.mic = function (value = null, value2 = null) { + if (value === true) { + // unmute + session.muted = false; // set + log(session.muted); + toggleMute(true); // apply + } else if (value === false) { + // mute + session.muted = true; // set + log(session.muted); + toggleMute(true); // apply + } else if (value === "toggle") { + // toggle + toggleMute(); + } + return session.muted; + }; + commands.camera = function (value = null, value2 = null) { + if (value === true) { + // unmute + session.videoMuted = false; // set + log(session.videoMuted); + toggleVideoMute(true); // apply + } else if (value === false) { + // mute + session.videoMuted = true; // set + log(session.videoMuted); + toggleVideoMute(true); // apply + } else if (value === "toggle") { + // toggle + toggleVideoMute(); + } + return session.videoMuted; + }; + commands.video = function (value = null, value2 = null) { + if (value === true) { + // unmute + session.videoMuted = false; // set + log(session.videoMuted); + toggleVideoMute(true); // apply + } else if (value === false) { + // mute + session.videoMuted = true; // set + log(session.videoMuted); + toggleVideoMute(true); // apply + } else if (value === "toggle") { + // toggle + toggleVideoMute(); + } + return session.videoMuted; + }; + commands.hangup = function (value = null, value2 = null) { + hangup(); + return true; + }; + commands.bitrate = function (value = null, value2 = null) { + if (value === false) { + value = 0; + } else if (value === true) { + value = -1; + } else { + value = parseInt(value) || 0; + } + for (var i in session.rpcs) { + try { + session.requestRateLimit(value, i); + } catch (e) { + errorlog(e); + } + } + return value; + }; + + commands.requestStats = function (value = null, value2 = null) { + var myStats = { ...session.stats }; + + myStats.streamID = session.streamID; + + if (session.whipOut && session.whipOut.stats) { + myStats.whipStats = session.whipOut.stats; + } + if (session.whepIn && session.whepIn.stats) { + myStats.whepStats = session.whepIn.stats; + } + + myStats.pcs = {}; + myStats.rpcs = {}; + + for (var uuid in session.pcs) { + myStats.pcs[uuid] = session.pcs[uuid].stats; + } + for (var uuid in session.rpcs) { + myStats.rpcs[uuid] = session.rpcs[uuid].stats; + myStats.rpcs[uuid].streamID = session.rpcs[uuid].streamID; + } + + return myStats; + }; + + commands.getDetails = function (value = null, value2 = null) { + return getDetailedState(value); + }; + + commands.getStats = function (value = null, value2 = null) { + return getQuickStats(value); + }; + + commands.getGuestList = function (value = null, value2 = null) { + return getGuestList(); + }; + + commands.reload = function (value = null, value2 = null) { + reloadRequested(); + return true; + }; + commands.volume = function (value = null, value2 = null) { + if (value === false) { + value = 0; + } else if (value === true) { + value = 100; + } else { + value = parseInt(value) || 0; + } + value = parseFloat(value / 100); + for (var i in session.rpcs) { + try { + session.rpcs[i].videoElement.volume = parseFloat(value); + } catch (e) { + errorlog(e); + } + } + return value; + }; + + commands.forceKeyframe = function (value = null, value2 = null) { + return session.forcePLI(); + }; + + commands.panning = function (value = null, value2 = null) { + if (value === false) { + value = 90; + } else if (value === true) { + value = 90; + } else { + value = parseInt(value); + } + for (var uuid in session.rpcs) { + try { + adjustPan(uuid, value); // &panning needs to be added to enable. playback only; not mic out. + } catch (e) { + errorlog(e); + } + } + return value; + }; + + commands.record = function (value = null, value2 = null) { + if (!session.videoElement) { + return; + } + + if (value === false) { + // mute + if ("recording" in session.videoElement) { + recordLocalVideo("stop"); + } + } else if (value === true) { + if ("recording" in session.videoElement) { + // already recording + } else { + recordLocalVideo("start"); + } + } + return value; + }; + + commands.group = function (value = null, value2 = null) { + if (value && value !== "null") { + return changeGroupDirectorAPI(value); + } + return false; + }; + + commands.joinGroup = function (value = null, value2 = null) { + if (value && value !== "null") { + return changeGroupDirectorAPI(value, true); + } + return false; + }; + + commands.leaveGroup = function (value = null, value2 = null) { + if (value && value !== "null") { + return changeGroupDirectorAPI(value, false); + } + return false; + }; + + commands.viewGroup = function (value = null, value2 = null) { + if (value && value !== "null") { + return changeGroupViewDirectorAPI(value); + } + return false; + }; + + commands.joinViewGroup = function (value = null, value2 = null) { + if (value && value !== "null") { + return changeGroupViewDirectorAPI(value, true); + } + return false; + }; + + commands.leaveViewGroup = function (value = null, value2 = null) { + if (value && value !== "null") { + return changeGroupViewDirectorAPI(value, false); + } + return false; + }; + + commands.sendChat = function (value = null, value2 = null) { + sendChat(value); + // sendChatMessage // this would add it to the chat message + return true; + }; + + commands.sendChatMessage = function (value = null, value2 = null) { + sendChatMessage(value); + return true; + }; + + commands.showChatOverlay = function (value = null, value2 = null) { + getChatMessage(value, false, false, true); + return true; + }; + + commands.startRoomTimer = function (value = null, value2 = null) { + getById("globalTimerDirectorToggle").value = 0; // reset + directRoomTimer(getById("globalTimerDirectorToggle"), false, value); + return true; + }; + + commands.pauseRoomTimer = function (value = null, value2 = null) { + if (getById("globalTimerDirectorToggle").value == 3) { + directRoomTimer(getById("globalTimerDirectorToggle"), { ctrlKey: true }, value); + } else { + directRoomTimer(getById("globalTimerDirectorToggle"), { ctrlKey: true }, value); + } + return true; + }; + + commands.stopRoomTimer = function (value = null, value2 = null) { + getById("globalTimerDirectorToggle").value = 1; // pause + directRoomTimer(getById("globalTimerDirectorToggle"), false, value); + return true; + }; + + commands.tallylight = function (value = null, value2 = null) { + if (value == "onair") { + session.tallyOverride = 1; + } else if (value == "active") { + session.tallyOverride = 2; + } else if (value == "standby") { + session.tallyOverride = 3; + } else if (value == "false") { + session.tallyOverride = false; + } else if (value == "off") { + session.tallyOverride = false; + } else if (value) { + session.tallyOverride = parseInt(value) || 0; + } else { + session.tallyOverride = 0; + } + applySceneState(); + return true; + }; + + commands.prevSlide = function (value = null, value2 = null) { + var data = {}; + data.d = [176, 110, 10]; + playbackMIDI(data); + return true; + }; + + commands.nextSlide = function (value = null, value2 = null) { + var data = {}; + data.d = [176, 110, 11]; + playbackMIDI(data); + return true; + }; + + commands.zoom = function (value = null, value2 = null) { + if (value !== null) { + const zoomValue = parseFloat(value); + const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; + session.remoteZoom(zoomValue, isAbsolute); + return { zoom: zoomValue, absolute: isAbsolute }; + } + return false; + }; + + commands.focus = function (value = null, value2 = null) { + if (value !== null) { + const focusValue = parseFloat(value); + const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; + session.remoteFocus(focusValue, isAbsolute); + return { focus: focusValue, absolute: isAbsolute }; + } + return false; + }; + + commands.pan = function (value = null, value2 = null) { + if (value !== null) { + const panValue = parseFloat(value); + const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; + session.remotePan(panValue, isAbsolute); + return { pan: panValue, absolute: isAbsolute }; + } + return false; + }; + + commands.tilt = function (value = null, value2 = null) { + if (value !== null) { + const tiltValue = parseFloat(value); + const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; + session.remoteTilt(tiltValue, isAbsolute); + return { tilt: tiltValue, absolute: isAbsolute }; + } + return false; + }; + + commands.exposure = function (value = null, value2 = null) { + if (value !== null) { + const exposureValue = parseFloat(value); + const isAbsolute = value2 === true || value2 === "true" || value2 === "abs"; + session.remoteExposure(exposureValue, isAbsolute); + return { exposure: exposureValue, absolute: isAbsolute }; + } + return false; + }; + + commands.soloVideo = function (value = null, value2 = null) { + var element = getById("highlightDirector"); + if (value && value == "toggle") { + return requestInfocus(element); + } else if (value && value !== "null") { + return requestInfocus(element, null, true); + } else if (value && value === "null") { + return requestInfocus(element); + } else { + return requestInfocus(element, null, false); + } + return false; + }; + commands.highlight = function (value = null, value2 = null) { + return commands.soloVideo(value, value2); + }; + + commands.activeSpeaker = function (value = null, value2 = null) { + var res = {}; + res.previous = session.activeSpeaker; + if ((value && (value == "toggle" || value == "null")) || (value === null)) { + res.action = "toggle"; + if (session.activeSpeaker) { + session.activeSpeaker = false; + } else { + session.activeSpeaker = urlParams.get("activespeaker") || urlParams.get("speakerview") || urlParams.get("sas") || 1; + } + } else if (value && parseInt(value)) { + session.activeSpeaker = parseInt(value) || 1; + } else { + session.activeSpeaker = false; + } + if (session.activeSpeaker && !session.activeSpeakerInterval) { + if (!session.audioEffects) { + session.audioEffects = true; + for (var UUID in session.rpcs) { + updateIncomingAudioElement(UUID); + } + } + activeSpeaker(false); + session.activeSpeakerInterval = setInterval(function () { + activeSpeaker(false); + }, 100); + } else { + updateMixer(); + } + res.current = session.activeSpeaker; + res.value = value; + return res; + }; + + commands.setBufferDelay = function (value = null, value2 = null) { + let delay = parseInt(value) || 0; + + if (!value2) { + session.buffer = delay; + } else if (value2 === "*") { + for (var uuid in session.rpcs) { + session.rpcs[uuid].buffer = delay; + playoutdelay(uuid); + document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + uuid + '"] input[data-buffer-value]').forEach(ele => { + ele.value = delay; + }); + return true; + } + } else if (value2 in session.rpcs) { + session.rpcs[value2].buffer = delay; + playoutdelay(value2); + document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + value2 + '"] input[data-buffer-value]').forEach(ele => { + ele.value = delay; + }); + return true; + } else if (value2) { + let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === value2); + if (session.rpcs[UUID]) { + session.rpcs[UUID].buffer = delay; + playoutdelay(UUID); + document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + UUID + '"] input[data-buffer-value]').forEach(ele => { + ele.value = delay; + }); + return true; + } else { + errorlog("The stream ID specified does not exist: " + value2); + } + } + return false; + }; + + commands.layout = function (value = null, value2 = null) { + let response = {}; + response.input = value; + + try { + if (parseInt(value) == value) { + log(value); + value = parseInt(value); + if (value == 0) { + value = false; // Auto mixer + } else { + value -= 1; // Adjust index to match layouts array (1-based to 0-based) + } + response.index = value; + } else if (checkType(value) === "Array") { + log(value); + session.layout_array = value; + if (session.layout_array) { + session.layout = combinedLayout(session.layout_array); + } + updateMixer(); + + if (session.director) { + issueLayout("0"); + response.issued = true; + response.scene = "0"; + pokeIframeAPI("layout-updated", value); + } + + response.type = 1; + updateMixer(); + response.combined_layout = session.layout; + var temp = previousDebug; + previousDebug = response + return { response: response, previous: temp } + } else if (checkType(value) === "Object") { + log(value); + session.layout = value; + if (session.director) { + issueLayout("0"); + response.issued = true; + response.scene = "0"; + pokeIframeAPI("layout-updated", session.layout); + } + response.type = 2; + updateMixer(); + response.layout = session.layout; + var temp = previousDebug; + previousDebug = response + return { response: response, previous: temp } + } + + if (value === null) { + session.layout = false; + pokeIframeAPI("layout-updated", session.layout); + pokeIframeAPI("layout-index", 0); + if (session.director) { + issueLayout("0"); + response.issued = true; + response.scene = "0"; + + } + response.type = 3; + response.layout = session.layout; + updateMixer(); + var temp = previousDebug; + previousDebug = response + return { response: response, previous: temp } + } else if (value === false) { + session.layout = false; + pokeIframeAPI("layout-updated", session.layout); + pokeIframeAPI("layout-index", 0); + if (session.director) { + issueLayout("0"); + response.issued = true; + response.scene = "0"; + } + response.layout = session.layout; + response.type = 3; + updateMixer(); + var temp = previousDebug; + previousDebug = response + return { response: response, previous: temp } + } else if (session.layouts && session.layouts[value]) { + try { + log(session.layouts); + var temp = session.layouts[value]; + response.type = 4; + if (session.director) { + var combined = {}; + for (var i = 0; i < temp.length; i++) { + if (!temp[i]) continue; + let streamID = null; + // First check if there's a slot assigned + if ("slot" in temp[i]) { + const slotNumber = parseInt(temp[i].slot) + 1; + streamID = session.currentSlots[slotNumber]; + } + // If no stream found via slot, check defaultStreamID + if (!streamID && temp[i].defaultStreamID) { + // Check if this defaultStreamID is connected and not assigned to another slot + let isConnected = false; + let isAlreadyAssigned = false; + for (let j in session.rpcs) { + if (session.rpcs[j].streamID === temp[i].defaultStreamID) { + isConnected = true; + // Check if this stream is assigned to any slot + for (let slot in session.currentSlots) { + if (session.currentSlots[slot] === temp[i].defaultStreamID) { + isAlreadyAssigned = true; + break; + } + } + break; + } + } + if (isConnected && !isAlreadyAssigned) { + streamID = temp[i].defaultStreamID; + } + } + // If we found a streamID, use it, otherwise add to empty slot + if (streamID) { + combined[streamID] = temp[i]; + } else { + if (!combined[""]) combined[""] = []; + combined[""].push(temp[i]); + } + } + session.layout = combined; + log("issuing layout:"); + log(session.layout); + issueLayout("0"); + response.issued = true; + response.scene = "0"; + response.combined_layout = session.layout; + pokeIframeAPI("layout-updated", session.layout); + pokeIframeAPI("layout-index", value + 1); + } + } catch (e) { + errorlog(e); + response.error = e.message + } + updateMixer(); + temp = previousDebug; + previousDebug = response + return { response: response, previous: temp } + } else { + errorlog("no layout found"); + log(session.layouts); + var temp = previousDebug; + previousDebug = response + return { response: response, previous: temp } + } + } catch (e) { + response.error = e.message + errorlog(e); + } + var temp = previousDebug; + previousDebug = response + return { response: response, previous: temp } + }; + + commands.width = function (value = null, value2 = null) { + // affects LOCAL camera width + let width = value ? parseInt(value) : null; + if (width) { + updateCameraConstraints("width", width, false, false); + return true; + } + return false; + }; + + commands.height = function (value = null, value2 = null) { + // affects LOCAL camera height + let height = value ? parseInt(value) : null; + if (height) { + updateCameraConstraints("height", height, false, false); + return true; + } + return false; + }; + + commands.aspectRatio = function (value = null, value2 = null) { + // affects LOCAL camera aspect ratio + if (!value) return false; + + let aspectRatio; + if (typeof value === 'string' && value.includes(":")) { + let parts = value.split(":"); + aspectRatio = parseFloat(parts[0]) / parseFloat(parts[1]); + } else { + aspectRatio = parseFloat(value); + } + + if (aspectRatio && !isNaN(aspectRatio)) { + updateCameraConstraints("aspectRatio", aspectRatio, false, false); + return true; + } + return false; + }; + + commands.videoConstraint = function (value = null, value2 = null) { + // Generic video constraint setter for LOCAL camera + // Usage: action=videoConstraint&value=CONSTRAINT_NAME&value2=CONSTRAINT_VALUE + if (!value || value2 === null || value2 === undefined) return false; + + // Parse value2 based on common types + let constraintValue = value2; + + // Handle boolean strings + if (value2 === "true") { + constraintValue = true; + } else if (value2 === "false") { + constraintValue = false; + } else if (value2 == parseFloat(value2)) { + // Handle numeric values + constraintValue = parseFloat(value2); + } + + // Special handling for aspectRatio with colon notation + if (value === "aspectRatio" && typeof value2 === 'string' && value2.includes(":")) { + let parts = value2.split(":"); + constraintValue = parseFloat(parts[0]) / parseFloat(parts[1]); + } + + // Apply the constraint + updateCameraConstraints(value, constraintValue, false, false); + return true; + }; + + return commands; +} +var Commands = setupCommands(); + +var previousDebug = {}; + +function checkType(value) { + if (Array.isArray(value)) { + return 'Array'; + } else if (typeof value === 'object' && value !== null) { + return 'Object'; + } else { + return 'Neither an Array nor an Object'; + } +} +async function processMessage(data) { + // api.vdo.ninja/apikey/action/value + try { + warnlog(data); + if ("target" in data && data.target !== "null" && data.target !== null) { + if ("action" in data) { + if ("value" in data && data.value !== "null" && data.value !== null) { + return await targetGuest(data.target, data.action, data.value, data.value2 || null); + } else { + return await targetGuest(data.target, data.action, null); + } + } + } else if ("action" in data && data.action !== "null" && data.action !== null) { + if (data.action in Commands) { + if ("value" in data && data.value !== "null" && data.value !== null) { + if (data.value == "true") { + data.value = true; + } else if (data.value == "false") { + data.value = false; + } + return Commands[data.action](data.value, data.value2 || null); + } else { + return Commands[data.action](); + } + } + } + } catch (e) { + errorlog(e); + } + return null; +} + +function midiHotkeysNote(note, velocity = false) { + if (session.midiHotkeys == 1) { + if (note == "G3") { + // open and close the chat window + toggleChat(); + return session.chat; + } else if (note == "A3") { + // mute your audio output + toggleMute(); + return session.muted; + } else if (note == "B3") { + // mute your video output + toggleVideoMute(); + return session.videoMuted; + } else if (note == "C4") { + // enable / disable screenshare + toggleScreenShare(); + return session.screenShareState; + } else if (note == "D4") { + // completely kill your connection/session + hangup(); + return true; + } else if (note == "E4") { + // raise your hand; director sees this + raisehand(); + return raisehand(); + } else if (note == "F4") { + // start/stop local recording + return recordLocalVideoToggle(); + } else if (note == "G4") { + // Director Enables their Audio output + press2talk(true); + return true; + } else if (note == "A4") { + // Director cut's their audio/video output + hangup2(); + return true; + } else if (note == "B4") { + // toggle speaker + toggleSpeakerMute(); + return session.speakerMuted; + } + } else if (session.midiHotkeys == 2) { + if (note == "G1") { + // open and close the chat window + toggleChat(); + } else if (note == "A1") { + // mute your audio output + toggleMute(); + } else if (note == "B1") { + // mute your video output + toggleVideoMute(); + } else if (note == "C2") { + // enable / disable screenshare + toggleScreenShare(); + } else if (note == "D2") { + // completely kill your connection/session + hangup(); + } else if (note == "E2") { + // raise your hand; director sees this + raisehand(); + } else if (note == "F2") { + // start/stop local recording + recordLocalVideoToggle(); + } else if (note == "G2") { + // Director Enables their Audio output + press2talk(true); + } else if (note == "A2") { + // Director cut's their audio/video output + hangup2(); + } else if (note == "B2") { + // toggle speaker + toggleSpeakerMute(); + } + } else if (session.midiHotkeys == 3) { + if (note == "C1") { + if (velocity == "0") { + // open and close the chat window + toggleChat(); + } else if (velocity == "1") { + // mute your audio output + toggleMute(); + } else if (velocity == "2") { + // mute your video output + toggleVideoMute(); + } else if (velocity == "3") { + // enable / disable screenshare + toggleScreenShare(); + } else if (velocity == "4") { + // completely kill your connection/session + hangup(); + } else if (velocity == "5") { + // raise your hand; director sees this + raisehand(); + } else if (velocity == "6") { + // start/stop local recording + recordLocalVideoToggle(); + } else if (velocity == "7") { + // Director Enables their Audio output + press2talk(true); + } else if (velocity == "8") { + // Director cut's their audio/video output + hangup2(); + } else if (velocity == "9") { + // toggle speaker + toggleSpeakerMute(); + } + } + } + /* if (velocity !== false && typeof velocity !== "undefined") { + // Get integer value of velocity + const velocityValue = parseInt(velocity); + + // Check if valid MIDI velocity (0-127) + if (!isNaN(velocityValue) && velocityValue >= 0 && velocityValue <= 127) { + // Camera control MIDI commands using Channel 1, various CC numbers + if (note == "C5") { + // Zoom - scale 0-127 to percentage or use relative value + const normalizedValue = velocityValue / 127; // 0 to 1 range + session.remoteZoom(normalizedValue, true); // absolute value + return { zoom: normalizedValue, absolute: true }; + } else if (note == "D5") { + // Focus - scale 0-127 to focus value + const normalizedValue = velocityValue / 127; // 0 to 1 range + session.remoteFocus(normalizedValue); + return { focus: normalizedValue }; + } else if (note == "E5") { + // Pan - scale 0-127 to pan value + const normalizedValue = (velocityValue - 64) / 64; // -1 to 1 range + session.remotePan(normalizedValue); + return { pan: normalizedValue }; + } else if (note == "F5") { + // Tilt - scale 0-127 to tilt value + const normalizedValue = (velocityValue - 64) / 64; // -1 to 1 range + session.remoteTilt(normalizedValue); + return { tilt: normalizedValue }; + } else if (note == "G5") { + // Exposure - scale 0-127 to exposure value + const normalizedValue = velocityValue / 127; // 0 to 1 range + session.remoteExposure(normalizedValue); + return { exposure: normalizedValue }; + } + } + } */ +} + +function getRightOrderedElement(selector, guestslot, UUID = false) { + var elements = getById("guestFeeds").children; + if (!UUID) { + for (var i = 0; i < elements.length; i++) { + try { + UUID = elements[i].UUID; + var lock = parseInt(document.getElementById("position_" + UUID).dataset.locked); + if (lock && lock == guestslot + 1) { + return elements[i].querySelector(selector) || false; + } + } catch (e) { } + } + } + + if (elements[guestslot]) { + return elements[guestslot].querySelector(selector) || false; + } else { + return false; + } +} + +function midiHotkeysCommand_offset(command, value, offset = 1) { + for (var i = 0; i < 9; i++) { + if (command == offset + i) { + var ele = getRightOrderedElement('[data-action-type="mute-guest"][data--u-u-i-d]', command - offset); + if (ele) { + remoteMute(ele, true); + } + } + } +} + +function midiHotkeysCommand(command, value) { + if (command == 110) { + // Existing controls 0-8, 10-11 remain unchanged + if (value == 0) { + toggleChat(); + } else if (value == 1) { + toggleMute(); + } else if (value == 2) { + toggleVideoMute(); + } else if (value == 3) { + toggleScreenShare(); + } else if (value == 4) { + hangup(); + } else if (value == 5) { + raisehand(); + } else if (value == 6) { + recordLocalVideoToggle(); + } else if (value == 7) { + press2talk(true); + } else if (value == 8) { + hangup2(); + } + // 10 & 11 reserved for PPT slides + + // Camera controls - relative adjustments + else if (value == 20) { + // Zoom in (relative +10%) + Commands.zoom(0.1); + } else if (value == 21) { + // Zoom out (relative -10%) + Commands.zoom(-0.1); + } else if (value == 22) { + // Pan left (relative -10%) + Commands.pan(-0.1); + } else if (value == 23) { + // Pan right (relative +10%) + Commands.pan(0.1); + } else if (value == 24) { + // Tilt up (relative +10%) + Commands.tilt(0.1); + } else if (value == 25) { + // Tilt down (relative -10%) + Commands.tilt(-0.1); + } else if (value == 26) { + // Exposure increase (relative +10%) + Commands.exposure(0.1); + } else if (value == 27) { + // Exposure decrease (relative -10%) + Commands.exposure(-0.1); + } else if (value == 28) { + // Focus near (relative -10%) + Commands.focus(-0.1); + } else if (value == 29) { + // Focus far (relative +10%) + Commands.focus(0.1); + } + + // Camera presets - absolute positions + else if (value == 30) { + // Camera preset 1: Center position + Commands.zoom(1.0, "abs"); + Commands.pan(0, "abs"); + Commands.tilt(0, "abs"); + } else if (value == 31) { + // Camera preset 2: Wide shot + Commands.zoom(0.5, "abs"); + Commands.pan(0, "abs"); + Commands.tilt(0, "abs"); + } else if (value == 32) { + // Camera preset 3: Close-up + Commands.zoom(2.0, "abs"); + Commands.pan(0, "abs"); + Commands.tilt(0, "abs"); + } + + } else if (command > 110) { + // Existing guest slot controls remain unchanged + var guestslot = command - 111; + if (value == 0) { + var ele = getRightOrderedElement('[data-action-type="forward"][data--u-u-i-d]', guestslot); + if (ele) { + directMigrate(ele, true); + } + } else if (value == 1) { + var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="1"][data--u-u-i-d]', guestslot); + if (ele) { + directEnable(ele, true); + } + } else if (value == 2) { + var ele = getRightOrderedElement('[data-action-type="mute-scene"][data--u-u-i-d]', guestslot); + if (ele) { + directMute(ele, true); + } + } else if (value == 3) { + var ele = getRightOrderedElement('[data-action-type="mute-guest"][data--u-u-i-d]', guestslot); + if (ele) { + remoteMute(ele, true); + } + } else if (value == 4) { + var ele = getRightOrderedElement('[data-action-type="hangup"][data--u-u-i-d]', guestslot); + if (ele) { + directHangup(ele, true); + } + } else if (value == 5) { + var ele = getRightOrderedElement('[data-action-type="solo-chat"][data--u-u-i-d]', guestslot); + if (ele) { + session.toggleSoloChat(ele.dataset.UUID); + } + } else if (value == 6) { + var ele = getRightOrderedElement('[data-action-type="toggle-remote-speaker"][data--u-u-i-d]', guestslot); + if (ele) { + remoteSpeakerMute(ele); + } + } else if (value == 7) { + var ele = getRightOrderedElement('[data-action-type="toggle-remote-display"][data--u-u-i-d]', guestslot); + if (ele) { + remoteDisplayMute(ele); + } + } else if (value == 8) { + var ele = getRightOrderedElement('[data-action-type="force-keyframe"][data--u-u-i-d]', guestslot); + if (ele) { + requestKeyframeScene(ele); + } + } else if (value == 12) { + var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="2"][data--u-u-i-d]', guestslot); + if (ele) { + directEnable(ele, true); + } + } else if (value == 13) { + var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="3"][data--u-u-i-d]', guestslot); + if (ele) { + directEnable(ele, true); + } + } else if (value == 14) { + var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="4"][data--u-u-i-d]', guestslot); + if (ele) { + directEnable(ele, true); + } + } else if (value == 15) { + var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="5"][data--u-u-i-d]', guestslot); + if (ele) { + directEnable(ele, true); + } + } else if (value == 16) { + var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="6"][data--u-u-i-d]', guestslot); + if (ele) { + directEnable(ele, true); + } + } else if (value == 17) { + var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="7"][data--u-u-i-d]', guestslot); + if (ele) { + directEnable(ele, true); + } + } else if (value == 18) { + var ele = getRightOrderedElement('[data-action-type="addToScene"][data-scene="8"][data--u-u-i-d]', guestslot); + if (ele) { + directEnable(ele, true); + } + } else if (value >= 27) { + var ele = getRightOrderedElement('[data-action-type="volume"][data--u-u-i-d]', guestslot); + if (ele) { + var audioGain = parseInt(value - 27) || 0; + if (audioGain < 0) { + audioGain = 0; + } + + ele.value = 200; + for (var i = 1; i <= 200; i++) { + if (volumeLUT[i] >= audioGain) { + ele.value = i; + break; + } + } + remoteVolume(ele); + } + } + } + + // Additional MIDI CC commands for finer camera control (80-89) + else if (command >= 80 && command <= 89) { + // Map MIDI CC values (0-127) to camera control values + const normalizedValue = value / 127; // 0 to 1 range + + if (command == 80) { + // CC80: Zoom absolute (0-127 maps to 0-2x zoom) + Commands.zoom(normalizedValue * 2, "abs"); + } else if (command == 81) { + // CC81: Pan absolute (0-127 maps to -1 to +1) + Commands.pan((normalizedValue * 2) - 1, "abs"); + } else if (command == 82) { + // CC82: Tilt absolute (0-127 maps to -1 to +1) + Commands.tilt((normalizedValue * 2) - 1, "abs"); + } else if (command == 83) { + // CC83: Exposure absolute (0-127 maps to 0-1) + Commands.exposure(normalizedValue, "abs"); + } else if (command == 84) { + // CC84: Focus absolute (0-127 maps to 0-1) + Commands.focus(normalizedValue, "abs"); + } + } +} + +session.createResourceChannel = function (UUID) { + if (!session.pcs[UUID] || !session.pcs[UUID].allowResources) return; + + const channel = session.pcs[UUID].createDataChannel("resources", { + ordered: true, + maxRetransmits: 30 + }); + + session.pcs[UUID].resourceChannel = channel; + if (session.pcs[UUID] && !session.pcs[UUID].resourceQueue) { // Initialize if not exists + session.pcs[UUID].resourceQueue = [...session.resources]; + } + session.pcs[UUID].processingResource = false; + + session.pcs[UUID].resourceChannel.onopen = () => { + log("Resource channel opened"); + if (session.pcs[UUID].resourceQueue && session.pcs[UUID].resourceQueue.length > 0) { + session.processResourceQueue(UUID); // Process any queued items + } + }; + + session.pcs[UUID].resourceChannel.onclose = () => { + warnlog("Resource channel closed"); + if (session.pcs[UUID]) { + session.pcs[UUID].processingResource = false; // Reset processing state + } + }; + + session.pcs[UUID].resourceChannel.onerror = (e) => { + warnlog("Resource channel error"); + if (session.pcs[UUID]) { + session.pcs[UUID].processingResource = false; // Reset processing state on error + } + }; +}; +async function handleImageUpload(file, field) { + const buffer = await file.arrayBuffer(); + const resource = { + type: field.templateName, // Changed from field.type to field.templateName + id: field.id, + data: buffer, + metadata: { + filename: file.name, + size: file.size, + type: field.type, + label: field.type, + filetype: file.type, + id: field.id, + templateName: field.templateName + } + }; + + for (var UUID in session.pcs) { + if (session.pcs[UUID].allowResources) { + session.sendResource(resource, UUID); + } + } + session.resources.push(resource); +} +session.sendResource = async function (resource, UUID) { + // Resource format: { type: "avatar"|"overlay"|"qr"|"icon", data: ArrayBuffer, metadata: Object } + if (!session.pcs[UUID]) return false; + + if (!session.pcs[UUID].resourceQueue) { + session.pcs[UUID].resourceQueue = [...session.resources]; // Initialize queue if not exists + } + + // Queue the resource + session.pcs[UUID].resourceQueue.push(resource); + + // If channel exists and is open, process queue + if (session.pcs[UUID].resourceChannel && session.pcs[UUID].resourceChannel.readyState === "open") { + if (!session.pcs[UUID].processingResource) { + session.processResourceQueue(UUID); + } + } else if (!session.pcs[UUID].resourceChannel && session.pcs[UUID].allowResources) { + // If channel doesn't exist but resources are allowed, create it + session.createResourceChannel(UUID); + } + + return true; +}; + +session.processResourceQueue = async function (UUID) { + if (!session.pcs[UUID] || !session.pcs[UUID].resourceChannel || session.pcs[UUID].processingResource) return; + + const channel = session.pcs[UUID].resourceChannel; + const queue = session.pcs[UUID].resourceQueue; + + if (queue.length === 0) return; + + session.pcs[UUID].processingResource = true; + + try { + const resource = queue[0]; + const chunkSize = 16384; // 16KB chunks + const totalChunks = Math.ceil(resource.data.byteLength / chunkSize); + + log(resource); + + channel.send(JSON.stringify(resource.metadata)); + log("sending.."); + await new Promise(r => setTimeout(r, 100)); // Small delay to ensure metadata is processed + + for (let i = 0; i < totalChunks; i++) { + const start = i * chunkSize; + const end = Math.min(start + chunkSize, resource.data.byteLength); + const chunk = resource.data.slice(start, end); + + channel.send(chunk); + + // Add small delay between chunks to prevent flooding + await new Promise(r => setTimeout(r, 50)); + } + log("sent."); + queue.shift(); + + } catch (e) { + errorlog(e); + } + + session.pcs[UUID].processingResource = false; + + if (queue.length > 0) { + setTimeout(() => session.processResourceQueue(UUID), 100); + } +}; + +session.recieveResourcesChannel = async function (UUID, channel) { + if (!session.allowResources) { + warnlog("Someone is trying to send resources despite you asking not to.."); + return; + } + log("Created resource channel"); + + session.rpcs[UUID].resourceChannel = channel; + session.rpcs[UUID].resourceChannel.binaryType = "arraybuffer"; + + let metadata = null; + let receivedChunks = []; + let receivedSize = 0; + + channel.onmessage = async (e) => { // Fixed: Using channel parameter instead of nested property + try { + if (typeof e.data === "string") { + const data = JSON.parse(e.data); + log(data); + if (data.templateName) { + metadata = data; + receivedChunks = []; + receivedSize = 0; + } + } else { + if (!metadata) return; + + receivedChunks.push(e.data); + receivedSize += e.data.byteLength; + + if (receivedSize >= metadata.size) { + const completeBuffer = await new Blob(receivedChunks).arrayBuffer(); + session.processReceivedResource(UUID, completeBuffer, metadata); + + receivedChunks = []; + receivedSize = 0; + } + } + } catch (error) { + errorlog(error); + } + }; + + channel.onopen = e => { + log("Opened resource channel"); + }; +}; + +session.processReceivedResource = function (UUID, buffer, data) { + if (!session.rpcs[UUID]) return; + + const createObjectURL = (buffer, type) => { + const blob = new Blob([buffer], { type: type || "image/png" }); + return URL.createObjectURL(blob); + }; + + try { + log(data); + + session.rpcs[UUID].meta[data.templateName] = data; + + if (session.rpcs[UUID].meta[data.templateName].value) { + URL.revokeObjectURL(session.rpcs[UUID].meta[data.templateName].value); + } + + session.rpcs[UUID].meta[data.templateName].value = createObjectURL(buffer, data.type); + log(session.rpcs[UUID].meta[data.templateName].value); + updateMixer(); + } catch (error) { + errorlog(error); + } +}; +function sendRawMIDI(input, UUID = false, streamID = false) { + var msg = {}; + msg.midi = {}; + msg.midi.d = Array.from(input.data); // Convert to regular array + msg.midi.s = input.timestamp || Date.now(); + if (input.message && input.message.channel) { + msg.midi.c = input.message.channel; + } else if (input && input.channel) { + msg.midi.c = input.channel; + } + + if (UUID && session.pcs[UUID] && session.pcs[UUID].allowMIDI) { + session.sendMessage(msg, UUID); + } else if (UUID && session.rpcs[UUID] && session.rpcs[UUID].allowMIDI) { + session.sendRequest(msg, UUID); + } else if (streamID) { + for (var UID in session.rpcs) { + if (session.rpcs[UID].allowMIDI && session.rpcs[UID].streamID === streamID) { + // specific to gstreamer code aplication + session.sendRequest(msg, UID); + return; // only one stream ID should match + } + } + } else { + var list = []; + for (var UID in session.pcs) { + if (session.pcs[UID].allowMIDI) { + if (session.sendMessage(msg, UID)) { + list.push(UID); + } + } + } + for (var UID in session.rpcs) { + if (session.rpcs[UID].allowMIDI) { + // specific to gstreamer code aplication + if (!list.includes(UID)) { + session.sendRequest(msg, UID); + } + } + } + } +} + +function sendMIDINote(note, on = true, channel = 1, uuid = null) { + // MIDI Note On status byte: 144 + (channel - 1) + // MIDI Note Off status byte: 128 + (channel - 1) + const statusByte = on ? (144 + (channel - 1)) : (128 + (channel - 1)); + const velocity = on ? 127 : 0; // 127 for note on, 0 for note off + + // Convert note names like "C1", "D3" to MIDI note numbers + let noteNumber; + if (typeof note === "string") { + const noteName = note.slice(0, -1); + const octave = parseInt(note.slice(-1)); + const noteValues = { + "C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3, + "E": 4, "F": 5, "F#": 6, "Gb": 6, "G": 7, + "G#": 8, "Ab": 8, "A": 9, "A#": 10, "Bb": 10, "B": 11 + }; + + // C1 is MIDI note 24, each octave is 12 notes + noteNumber = 24 + (octave - 1) * 12 + noteValues[noteName]; + } else { + noteNumber = note; + } + + // Create MIDI message and send it + const data = {}; + data.data = [statusByte, noteNumber, velocity]; + sendRawMIDI(data, uuid); + + return { note: noteNumber, status: statusByte, velocity: velocity }; +} +function buttonMIDI(ele, state = null) { + const note = ele.dataset.midiNote; + const uuid = ele.dataset.uuid || null; + const isToggleMode = ele.dataset.midiMode === 'toggle'; + + // Handle state tracking similar to changeGroup function + let newState; + let changed = false; + + if (state === true) { + // Explicit true state requested + if (!ele.classList.contains("pressed") || CtrlPressed) { + changed = true; + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } + newState = true; + } else if (state === false) { + // Explicit false state requested + if (ele.classList.contains("pressed") || CtrlPressed) { + changed = true; + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } + newState = false; + } else if (CtrlPressed) { + newState = ele.classList.contains("pressed"); + changed = true; + } else { + // Toggle current state + newState = !ele.classList.contains("pressed"); + changed = true; + + if (newState) { + ele.classList.add("pressed"); + ele.ariaPressed = "true"; + } else { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + } + } + + // Only send MIDI if state actually changed + if (changed) { + if (isToggleMode) { + // Dual channel toggle + sendMIDINote(note, true, newState ? 1 : 2, uuid); + } else { + // Single channel note on/off + const channel = parseInt(ele.dataset.midiChannel || "1"); + sendMIDINote(note, newState, channel, uuid); + + // Only auto-unpress non-toggle buttons + if (newState) { + setTimeout(() => { + ele.classList.remove("pressed"); + ele.ariaPressed = "false"; + }, 120); + } + } + + // Sync director state if available + if (typeof syncDirectorState === 'function') { + syncDirectorState(ele); + } + } + + return newState; +} + +let currentOscillatorIdMidi = 0; + +function setupMidiOscillator(callbackFunction, frameRate, timeOne = null, thisOscillatorId = null) { + if (!thisOscillatorId) { + thisOscillatorId = ++currentOscillatorIdMidi; + } else if (currentOscillatorIdMidi !== thisOscillatorId) { + return false; + } + + if (!session.audioCtx) { + session.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + + let oscillator = session.audioCtx.createOscillator(); + let silence = session.audioCtx.createGain(); + silence.gain.value = 0; + oscillator.connect(silence); + silence.connect(session.audioCtx.destination); + + if (!timeOne) { + timeOne = session.audioCtx.currentTime; + } + + oscillator.onended = () => { + oscillator.disconnect(); + silence.disconnect(); + if (currentOscillatorIdMidi === thisOscillatorId) { + let timeTwo = session.audioCtx.currentTime; + if (typeof callbackFunction === "function") { + callbackFunction(); + } + setupMidiOscillator(callbackFunction, frameRate, timeTwo, thisOscillatorId); + } + }; + + oscillator.start(timeOne); + oscillator.stop(timeOne + 1 / frameRate); + + return function (check = false) { + if (check && currentOscillatorIdMidi !== thisOscillatorId) { + return true; + } else if (check) { + return false; + } + if (currentOscillatorIdMidi === thisOscillatorId) { + currentOscillatorIdMidi++; + } + return false; + }; +} + +function playOutMidi(msg) { + if (session.midiIn === true || session.midiIn == parseInt(session.midiIn)) { + if ("d" in msg) { + const outputs = session.midiIn === true ? WebMidi.outputs : [WebMidi.outputs[parseInt(session.midiIn) - 1]]; + + outputs.forEach(output => { + try { + if ("c" in msg) { + output.channels[msg.c].send(msg.d); + } else if (msg.d) { + output.send(msg.d); + } + } catch (e) { + errorlog(e); + } + }); + } + } +} + +function displayOverlayMessage(label, msg) { + if (!(session.cleanOutput && session.cleanish == false)) { + var textOverlay = getById("overlayMsgs"); + if (textOverlay) { + textOverlay.innerText = msg + textOverlay.style.display = "block"; + var showtime = msg.length * 200 + 3000; + if (showtime > 8000) { + showtime = 8000; + } + setTimeout( + function (ele) { + try { + ele.parentNode.removeChild(ele); + } catch (e) { } + }, + showtime, + spanOverlay + ); + } + } +} + +let lastMTCValues = { hours: 0, minutes: 0, seconds: 0, frames: 0, type: 0 }; +let mtcCounter = 0; +let lastDisplayedTime = 0; +function handleQuarterFrame(data) { + const mtcType = (data >> 4) & 0x7; + const mtcValue = data & 0xF; + + //console.log(`Quarter Frame: Type ${mtcType}, Value ${mtcValue}`); + + switch (mtcType) { + case 0: lastMTCValues.frames = mtcValue; break; + case 1: lastMTCValues.frames |= (mtcValue << 4); break; + case 2: lastMTCValues.seconds = mtcValue; break; + case 3: lastMTCValues.seconds |= (mtcValue << 4); break; + case 4: lastMTCValues.minutes = mtcValue; break; + case 5: lastMTCValues.minutes |= (mtcValue << 4); break; + case 6: lastMTCValues.hours = mtcValue; break; + case 7: + lastMTCValues.hours |= (mtcValue & 0x1) << 4; + lastMTCValues.type = (mtcValue >> 1) & 0x3; + break; + } + + mtcCounter++; + if (mtcCounter === 8) { + mtcCounter = 0; + displayTimecode(lastMTCValues); + } +} + +function handleFullFrame(data) { + const hours = data[0]; + const minutes = data[1]; + const seconds = data[2]; + const frames = data[3]; + // The frame rate information might be encoded differently or not present + // We'll need to determine how to extract this information from your specific implementation + const type = 0; // Default to 24 fps for now, adjust as needed + + lastMTCValues = { hours, minutes, seconds, frames, type }; + displayTimecode(lastMTCValues); + mtcCounter = 0; // Reset counter after full frame +} + +function displayTimecode(timecode) { + const frameRates = [24, 25, 29.97, 30]; + const frameRate = frameRates[timecode.type] || 24; // Default to 24 if unknown + const dropFrame = (timecode.type === 2); + + let timecodeString = `${timecode.hours.toString().padStart(2, '0')}:${timecode.minutes.toString().padStart(2, '0')}:${timecode.seconds.toString().padStart(2, '0')} f${timecode.frames.toString().padStart(2, '0')}/${frameRate.toString()}`; + + //console.log("Displaying Timecode:", timecodeString, `(${frameRate} fps${dropFrame ? ' DF' : ''})`); + displayOverlayMessage("Timecode", timecodeString); +} + +function playbackMIDI(msg, unsafe = false, UUID = null) { + if (session.midiIframe) { + pokeIframeAPI("midi-in", msg, UUID); + } + + if (session.midiTimecode && msg && msg.d) { + const data = msg.d; + if (data.length === 2 && data[0] === 241) { + handleQuarterFrame(data[1]); + } else if (data.length >= 10 && data[0] === 240 && data[1] === 127 && data[4] === 1) { + if (data.length === 11) { + handleFullFrame(data.slice(6, -1)); + } else { + handleFullFrame(data.slice(6, 8)); + } + } + } + + if (session.midiIn === false && session.midiRemote === false) { + return; + } else if (session.midiOut === session.midiIn && session.midiRemote === false) { + return; + } + + log("play out"); + + if (session.midiDelay) { + let timestamp = null; + if ("s" in msg) { + timestamp = msg.s; + } else if ("t" in msg) { + timestamp = msg.t; + } + + if (timestamp !== null) { + const timeDelay = session.midiDelay - (Date.now() - timestamp); + if (timeDelay <= 0) { + playOutMidi(msg); + } else { + setupMidiOscillator(() => playOutMidi(msg), 1000 / timeDelay); + } + } else { + playOutMidi(msg); + } + } else { + playOutMidi(msg); + } + + if (unsafe) { + return; + } // I don't know how midi remote works in reverse, so lets ignore it + + if (session.midiRemote == 4) { + if (msg.d[0] == 176) { + midiHotkeysCommand(msg.d[1], msg.d[2]); + } + } else if (session.midiRemote == 1 || session.midiRemote == 2 || session.midiRemote == 3) { + if (msg.d[0] == 156) { + if (msg.d[1] == 33) { + midiHotkeysNote("A1", msg.d[2]); + } else if (msg.d[1] == 55) { + midiHotkeysNote("G3", msg.d[2]); + } else if (msg.d[1] == 57) { + midiHotkeysNote("A3", msg.d[2]); + } else if (msg.d[1] == 59) { + midiHotkeysNote("B3", msg.d[2]); + } else if (msg.d[1] == 60) { + midiHotkeysNote("C4", msg.d[2]); + } else if (msg.d[1] == 62) { + midiHotkeysNote("D4", msg.d[2]); + } else if (msg.d[1] == 64) { + midiHotkeysNote("E4", msg.d[2]); + } else if (msg.d[1] == 65) { + midiHotkeysNote("F4", msg.d[2]); + } else if (msg.d[1] == 67) { + midiHotkeysNote("G4", msg.d[2]); + } else if (msg.d[1] == 69) { + midiHotkeysNote("A4", msg.d[2]); + } else if (msg.d[1] == 43) { + midiHotkeysNote("G2", msg.d[2]); + } else if (msg.d[1] == 35) { + midiHotkeysNote("B1", msg.d[2]); + } else if (msg.d[1] == 36) { + midiHotkeysNote("C2", msg.d[2]); + } else if (msg.d[1] == 38) { + midiHotkeysNote("D2", msg.d[2]); + } else if (msg.d[1] == 40) { + midiHotkeysNote("E2", msg.d[2]); + } else if (msg.d[1] == 41) { + midiHotkeysNote("F2", msg.d[2]); + } else if (msg.d[1] == 24) { + midiHotkeysNote("C1", msg.d[2]); + } + } + } + //var output = WebMidi.getOutputById("123456789"); + //output = WebMidi.getOutputByName("Axiom Pro 25 Ext Out"); + //output = WebMidi.outputs[0]; +} + +function addEventToAll(targets, trigger, callback) { + // js helper + const target = document.querySelectorAll(targets); + var triggers = trigger.split(" "); + for (let i = 0; i < target.length; i++) { + for (let j = 0; j < triggers.length; j++) { + setTimeout( + function (t1, t2) { + t1.addEventListener(t2, function (e) { + callback(e, t1); + }); + }, + 0, + target[i], + triggers[j] + ); + } + } +} + +function insertAfter(newNode, existingNode) { + existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling); +} +addEventToAll(".column", "click", function (e, ele) { + if (ele.classList.contains("skip-animation")) { + return; + } + try { + var bounding_box = ele.getBoundingClientRect(); + } catch (e) { + return; + } + if (!bounding_box) { + errorlog("No bounding box for ele found"); + } + ele.style.top = bounding_box.y + "px"; + ele.style.left = bounding_box.x - 20 + "px"; + ele.classList.add("in-animation"); + ele.classList.remove("pointer"); + ele.classList.remove("rounded"); + + if (document.getElementById("empty-container")) { + getById("empty-container").parentNode.removeChild(getById("empty-container")); + } + var empty = document.createElement("DIV"); + empty.id = "empty-container"; + empty.className = "column"; + ele.parentNode.insertBefore(empty, ele.nextSibling); + const styles = + "\ + @keyframes outlightbox {\ + 0% {\ + height: 100%;\ + width: 100%;\ + top: 0px;\ + left: 0px;\ + }\ + 50% {\ + height: 200px;\ + top: " + + bounding_box.y + + "px;\ + }\ + 100% {\ + height: 200px;\ + width: " + + bounding_box.width + + "px;\ + top: " + + bounding_box.y + + "px;\ + left: " + + bounding_box.x + + "px;\ + }\ + }\ + "; + if (document.getElementById("lightbox-animations")) { + getById("lightbox-animations").innerHTML = styles; + } + document.body.style.overflow = "hidden"; +}); +addEventToAll(".close", "click", function (e, ele) { + cleanupMediaTracks(); + + document.querySelectorAll(".hidden2").forEach(ele2 => { + ele2.classList.remove("hidden2"); + }); + + ele.style.display = "none"; + mapToAll(".container-inner", function (target) { + target.style.display = "none"; + }); + document.body.style.overflow = "auto"; + + // Get the actual position where the element should return to + var emptyContainer = getById("empty-container"); + if (emptyContainer) { + var targetBox = emptyContainer.getBoundingClientRect(); + + // Update the outlightbox animation with the correct target position + const styles = + "\ + @keyframes outlightbox {\ + 0% {\ + height: 100%;\ + width: 100%;\ + top: 0px;\ + left: 0px;\ + }\ + 50% {\ + height: 200px;\ + top: " + + targetBox.top + + "px;\ + }\ + 100% {\ + height: " + targetBox.height + "px;\ + width: " + + targetBox.width + + "px;\ + top: " + + targetBox.top + + "px;\ + left: " + + targetBox.left + + "px;\ + }\ + }\ + "; + + if (document.getElementById("lightbox-animations")) { + getById("lightbox-animations").innerHTML = styles; + } + + // Don't set position here - let the animation handle it + setTimeout(function () { + // just smoothes things out; breathing room to clean up things first. + ele.parentNode.classList.add("out-animation"); + }, 1); + } + e.stopPropagation(); +}); +addEventToAll(".column", "animationend", function (e, ele) { + if (e.animationName == "inlightbox") { + ele.classList.add("skip-animation"); + mapToAll( + ".close", + function (target) { + target.style.display = "block"; + }, + ele + ); + document.querySelectorAll("#header, #miniTaskBarm, #credits, .columnfade").forEach(ele2 => { + if (ele2 !== ele) { + ele2.classList.add("hidden2"); + } + }); + mapToAll( + ".container-inner", + function (target) { + target.style.display = "block"; + }, + ele + ); + } else if (e.animationName == "outlightbox") { + ele.classList.remove("in-animation"); + ele.classList.remove("out-animation"); + ele.classList.remove("skip-animation"); + ele.classList.remove("columnfade"); + ele.classList.add("pointer"); + ele.classList.add("rounded"); + + // Clear all inline styles to fully restore original position + ele.style.top = ""; + ele.style.left = ""; + ele.style.position = ""; + ele.style.width = ""; + ele.style.height = ""; + + // Clear stored position data + delete ele.dataset.originalTop; + delete ele.dataset.originalLeft; + delete ele.dataset.originalWidth; + delete ele.dataset.originalHeight; + + if (document.getElementById("empty-container")) { + getById("empty-container").parentNode.removeChild(getById("empty-container")); + } + if (document.getElementById("lightbox-animations") && getById("lightbox-animations").sheet && getById("lightbox-animations").sheet.cssRules.length > 0) { + getById("lightbox-animations").sheet.deleteRule(0); + } + } +}); +addEventToAll("#audioSource", "mousedown touchend focusin focusout", function (e, ele) { + var state = getById("multiselect-trigger").dataset.state || 0; // Does this return TRU instead?? GAH. #TODO: + if (state == 0) { + getById("multiselect-trigger").dataset.state = 1; + getById("multiselect-trigger").classList.add("open"); + getById("multiselect-trigger").classList.remove("closed"); + mapToAll( + ".chevron", + function (ele) { + ele.classList.remove("bottom"); + }, + (parentElement = getById("multiselect-trigger")) + ); + mapToAll( + ".multiselect-contents", + function (ele) { + ele.style.display = "block"; + mapToAll( + 'input[type="checkbox"]', + function (ele2) { + ele2.parentNode.style.display = "block"; + ele2.style.display = "inline-block"; + }, + ele + ); + }, + (parentElement = getById("multiselect-trigger").parentNode) + ); + } + e.stopPropagation(); + //e.preventDefault(); +}); +addEventToAll("#audioSource3", "mousedown touchend focusin focusout", function (e, ele) { + var state = getById("multiselect-trigger3").dataset.state || 0; // Does this return TRU instead?? GAH. #TODO: + if (state == 0) { + getById("multiselect-trigger3").dataset.state = 1; + getById("multiselect-trigger3").classList.add("open"); + getById("multiselect-trigger3").classList.remove("closed"); + mapToAll( + ".chevron", + function (target) { + target.classList.remove("bottom"); + }, + getById("multiselect-trigger3") + ); + mapToAll( + ".multiselect-contents", + function (target) { + target.style.display = "block"; + }, + getById("multiselect-trigger3").parentNode + ); + mapToAll( + ".multiselect-contents", + function (target) { + mapToAll( + 'input[type="checkbox"]', + function (target2) { + target2.style.display = "inline-block"; + target2.parentNode.style.display = "block"; + }, + target + ); + }, + getById("multiselect-trigger3").parentNode + ); + } + e.stopPropagation(); + //e.preventDefault(); +}); +addEventToAll("#multiselect-trigger", "mousedown touchend focusin focusout", function (e, ele) { + var state = ele.dataset.state || 0; // Does this return TRU instead?? GAH. #TODO: + if (state == 0) { + // open the dropdown + ele.dataset.state = 1; + ele.classList.add("open"); + ele.classList.remove("closed"); + mapToAll( + ".chevron", + function (target) { + target.classList.remove("bottom"); + }, + getById("multiselect-trigger") + ); + mapToAll( + ".multiselect-contents", + function (target) { + target.style.display = "block"; + }, + ele.parentNode + ); + mapToAll( + ".multiselect-contents", + function (target) { + mapToAll( + 'input[type="checkbox"]', + function (target2) { + target2.style.display = "inline-block"; + target2.parentNode.style.display = "block"; + }, + target + ); + }, + ele.parentNode + ); + } else { + // close the dropdown + ele.dataset.state = 0; + ele.classList.add("closed"); + ele.classList.remove("open"); + mapToAll( + ".chevron", + function (target) { + target.classList.add("bottom"); + }, + ele + ); + mapToAll( + ".multiselect-contents", + function (target) { + mapToAll( + 'input[type="checkbox"]', + function (target2) { + target2.style.display = "none"; + if (!target2.checked) { + target2.parentNode.style.display = "none"; + } + }, + target + ); + }, + ele.parentNode + ); + } + e.preventDefault(); + e.stopPropagation(); +}); +addEventToAll("#multiselect-trigger3", "mousedown touchend focusin focusout", function (e, ele) { + var state = ele.dataset.state || 0; // Does this return TRU instead?? GAH. #TODO: + if (state == 0) { + // open the dropdown + ele.dataset.state = 1; + ele.classList.add("open"); + ele.classList.remove("closed"); + mapToAll( + ".chevron", + function (target) { + target.classList.remove("bottom"); + }, + ele + ); + mapToAll( + ".multiselect-contents", + function (target) { + target.style.display = "block"; + }, + ele.parentNode + ); + mapToAll( + ".multiselect-contents", + function (target) { + mapToAll( + 'input[type="checkbox"]', + function (target2) { + target2.style.display = "inline-block"; + target2.parentNode.style.display = "block"; + }, + target + ); + }, + ele.parentNode + ); + } else { + // close the dropdown + ele.dataset.state = 0; + ele.classList.add("closed"); + ele.classList.remove("open"); + mapToAll( + ".chevron", + function (target) { + target.classList.add("bottom"); + }, + ele + ); + mapToAll( + ".multiselect-contents", + function (target) { + mapToAll( + 'input[type="checkbox"]', + function (target2) { + target2.style.display = "none"; + if (!target2.checked) { + target2.parentNode.style.display = "none"; + } + }, + target + ); + }, + ele.parentNode + ); + } + e.preventDefault(); + e.stopPropagation(); +}); + +function getSenders2(UUID) { + var fixedSenders = []; + var isAlt = false; + if (!(UUID in session.pcs)) { + return fixedSenders; + } + if ("realUUID" in session.pcs[UUID]) { + isAlt = true; + UUID = session.pcs[UUID].realUUID; + if (!(UUID in session.pcs)) { + return fixedSenders; + } + } + var senders = session.pcs[UUID].getSenders(); + + if (isAlt) { + senders.forEach(sender => { + if (sender.track && sender.track.id) { + if (sender.track.id in screenshareTracks) { + // I'm not going to change track.kind, since OBS isn't part of this list + fixedSenders.push(sender); + } + } + }); + } else { + senders.forEach(sender => { + if (sender.track && sender.track.id) { + if (!(sender.track.id in screenshareTracks)) { + fixedSenders.push(sender); + } + } + }); + } + + return fixedSenders; +} + +function getReceivers2(UUID) { + var fixedReceivers = []; + var isAlt = false; + var ssTracks = []; + if ("realUUID" in session.rpcs[UUID]) { + isAlt = true; + UUID = session.rpcs[UUID].realUUID; + if (!("screenIndexes" in session.rpcs[UUID])) { + errorlog("this is supposed to be a screen share, but no screen share index was found"); + return; + } + ssTracks = session.rpcs[UUID].screenIndexes; + } else if ("screenIndexes" in session.rpcs[UUID] && session.rpcs[UUID].screenIndexes) { + ssTracks = session.rpcs[UUID].screenIndexes; + } + + if (session.rpcs[UUID] && session.rpcs[UUID].getReceivers) { + var receivers = session.rpcs[UUID].getReceivers(); + } else { + var receivers = []; + } + + try { + if (session.rpcs[UUID].whep && session.rpcs[UUID].whep.getReceivers) { + // used to be "mc", not "whep" + try { + receivers = receivers.concat(session.rpcs[UUID].whep.getReceivers()); + } catch (e) { + errorlog(e); + } + } + } catch (e) { + errorlog(e); + } + + if (isAlt) { + for (var i = 0; i < receivers.length; i++) { + for (var j = 0; j < ssTracks.length; j++) { + if (i == ssTracks[j]) { + fixedReceivers.push(receivers[i]); + break; + } + } + } + } else { + for (var i = 0; i < receivers.length; i++) { + var matched = false; + for (var j = 0; j < ssTracks.length; j++) { + if (i == ssTracks[j]) { + matched = true; + } + } + if (!matched) { + fixedReceivers.push(receivers[i]); + } + } + } + + return fixedReceivers; +} + +function getReceiversMC(UUID) { + var fixedReceivers = []; + var isAlt = false; + var ssTracks = []; + if ("realUUID" in session.rpcs[UUID]) { + isAlt = true; + UUID = session.rpcs[UUID].realUUID; + if (!("screenIndexes" in session.rpcs[UUID])) { + errorlog("this is supposed to be a screen share, but no screen share index was found"); + return; + } + ssTracks = session.rpcs[UUID].screenIndexes; + } else if ("screenIndexes" in session.rpcs[UUID] && session.rpcs[UUID].screenIndexes) { + ssTracks = session.rpcs[UUID].screenIndexes; + } + + receivers = []; + if (session.rpcs[UUID].whep) { + receivers = session.rpcs[UUID].whep.getReceivers(); + } + + if (isAlt) { + for (var i = 0; i < receivers.length; i++) { + for (var j = 0; j < ssTracks.length; j++) { + if (i == ssTracks[j]) { + fixedReceivers.push(receivers[i]); + break; + } + } + } + } else { + for (var i = 0; i < receivers.length; i++) { + var matched = false; + for (var j = 0; j < ssTracks.length; j++) { + if (i == ssTracks[j]) { + matched = true; + } + } + if (!matched) { + fixedReceivers.push(receivers[i]); + } + } + } + return fixedReceivers; +} + +async function createSecondStream2(UUID) { + if (session.pcs[UUID].allowScreenVideo === false && session.pcs[UUID].allowScreenAudio === false) { + return false; + } + if ("realUUID" in session.pcs[UUID]) { + return false; + } // we don't want to attach to an existing screen share obviously + if (!session.screenStream) { + return false; + } + + if (!(UUID + "_screen" in session.pcs)) { + warnlog(UUID + "_screen; new screen link"); + session.pcs[UUID + "_screen"] = {}; + session.pcs[UUID + "_screen"].realUUID = UUID; + session.pcs[UUID + "_screen"].stats = {}; + session.pcs[UUID + "_screen"].sceneDisplay = null; + session.pcs[UUID + "_screen"].sceneMute = null; + session.pcs[UUID + "_screen"].solo = null; + session.pcs[UUID + "_screen"].allowVideo = session.pcs[UUID].allowScreenVideo; + session.pcs[UUID + "_screen"].allowAudio = session.pcs[UUID].allowScreenAudio; + session.pcs[UUID + "_screen"].allowDrawing = session.pcs[UUID].allowDrawing; + if (session.pcs[UUID + "_screen"].allowDrawing) { + if (session.screenShareElement && session.screenShareElement.syncDrawOnVideo) { + session.screenShareElement.syncDrawOnVideo(); + } + } + session.pcs[UUID + "_screen"].layout = null; + session.pcs[UUID + "_screen"].obsState = {}; + session.pcs[UUID + "_screen"].obsState.visibility = null; + session.pcs[UUID + "_screen"].obsState.sourceActive = null; + session.pcs[UUID + "_screen"].obsState.streaming = null; + session.pcs[UUID + "_screen"].obsState.recording = null; + session.pcs[UUID + "_screen"].obsState.virtualcam = null; + session.pcs[UUID + "_screen"].optimizedBitrate = false; + session.pcs[UUID + "_screen"].savedBitrate = false; + session.pcs[UUID + "_screen"].bitrateTimeout = null; + session.pcs[UUID + "_screen"].bitrateTimeoutFirefox = false; + session.pcs[UUID + "_screen"].setAudioBitrate = false; + session.pcs[UUID + "_screen"].setBitrate = false; + session.pcs[UUID + "_screen"].maxBandwidth = null; // based on max available bitrate + session.pcs[UUID + "_screen"].limitAudio = false; + session.pcs[UUID + "_screen"].audioMutedOverride = false; + session.pcs[UUID + "_screen"].enhanceAudio = false; + session.pcs[UUID + "_screen"].meshcast = null; + session.pcs[UUID + "_screen"].UUID = UUID + "_screen"; + session.pcs[UUID + "_screen"].scale = false; + session.pcs[UUID + "_screen"].scaleDueToBitrate = false; + session.pcs[UUID + "_screen"].scaleWidth = false; + session.pcs[UUID + "_screen"].scaleHeight = false; + session.pcs[UUID + "_screen"].scaleSnap = false; + session.pcs[UUID + "_screen"].scaleResolution = false; + session.pcs[UUID + "_screen"].scene = false; + session.pcs[UUID + "_screen"].keyframeRate = false; + session.pcs[UUID + "_screen"].keyframeTimeout = null; + session.pcs[UUID + "_screen"].label = false; + session.pcs[UUID + "_screen"].order = false; + session.pcs[UUID + "_screen"].preferVideoCodec = false; + session.pcs[UUID + "_screen"].startTime = Date.now(); + session.pcs[UUID + "_screen"].needsPublishing = null; + // session.pcs[UUID+"_screen"].rotation = false; I don't think this will ever be used? + + // we will use allowVideo/allowAudio from the main UUID parent + + session.pcs[UUID + "_screen"].getStats = function () { + return new Promise((resolve, reject) => { + resolve([]); + }); + }; + } + + /* if (session.audioContentHint && tracks.length){ + tracks.forEach(trk=>{ + try { + + trk.contentHint = session.audioContentHint; + } catch(e){ + errorlog(e); + } + }); + } */ + + var senders = getSenders2(UUID + "_screen"); + var tracks = session.screenStream.getTracks(); + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + + try { + if (track.kind === "audio" && session.pcs[UUID + "_screen"].allowAudio === false) { + continue; + } else if (track.kind === "video" && session.pcs[UUID + "_screen"].allowVideo === false) { + continue; + } + } catch (e) { + errorlog(e); + } + + if (session.audioContentHint && track.kind === "audio") { + try { + track.contentHint = session.audioContentHint; // this gets triggered too often I think + } catch (e) { + errorlog(e); + } + } + + if (session.screenshareContentHint && track.kind === "video") { + try { + track.contentHint = session.screenshareContentHint; // this gets triggered too often I think + } catch (e) { + errorlog(e); + } + } else if (session.contentHint && track.kind === "video") { + try { + track.contentHint = session.contentHint; // this gets triggered too often I think + } catch (e) { + errorlog(e); + } + } + + var added = false; + for (var j = 0; j < senders.length; j++) { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + var sender = senders[j]; + if (sender.track && sender.track.kind == track.kind) { + sender.replaceTrack(track); // replace may not be supported by all browsers. eek. + sender.track.enabled = true; + added = true; + break; + } + } + if (!added) { + session.pcs[UUID].addTrack(track, session.screenStream); + } + } + + session.applyIsolatedChat(); + + updateMixer(); +} + +var screenshareTracks = {}; +var firsttime = true; +async function createSecondStream() { + //////////////////////////// &sstype=3 ? + if (session.screenShareState == false) { + // adding a screen + + var video = {}; + + var quality = session.quality_ss; + + if (quality === false) { + quality = 0; // default to 1080p for screen shares + } + + if (session.quality !== false) { + quality = session.quality; + } + if (session.screensharequality !== false) { + quality = session.screensharequality; + } + + if (quality == -1) { + // unlocked capture resolution + } else if (quality == -2) { + video.width = { + ideal: 3840 + }; + video.height = { + ideal: 2160 + }; + } else if (quality == -3) { + video.width = { + ideal: 2560 + }; + video.height = { + ideal: 1440 + }; + } else if (quality == 0) { + video.width = { + ideal: 1920 + }; + video.height = { + ideal: 1080 + }; + } else if (quality == 1) { + video.width = { + ideal: 1280 + }; + video.height = { + ideal: 720 + }; + } else if (quality == 2) { + video.width = { + ideal: 640 + }; + video.height = { + ideal: 360 + }; + } else if (quality >= 3) { + // lowest + video.width = { + ideal: 320 + }; + video.height = { + ideal: 180 + }; + } + + if (session.width) { + video.width = { + ideal: session.width + }; + } + if (session.height) { + video.height = { + ideal: session.height + }; + } + + var constraints = { + // this part is a bit annoying. Do I use the same settings? I can add custom setting controls here later + audio: { + echoCancellation: true, // we want to cancel echo, since this is a secondary stream + autoGainControl: false, + noiseSuppression: false + }, + video: video + //,cursor: {exact: "none"} + }; + + try { + let supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + if (supportedConstraints.cursor) { + if (session.screensharecursor) { + constraints.video.cursor = ["always", "motion"]; + } else { + constraints.video.cursor = "never"; + } + } + if (session.suppressLocalAudioPlayback && supportedConstraints.suppressLocalAudioPlayback) { + constraints.audio.suppressLocalAudioPlayback = true; + } + // + if (session.preferCurrentTab) { + constraints.preferCurrentTab = true; + } + if (session.selfBrowserSurface) { + constraints.selfBrowserSurface = session.selfBrowserSurface; // exclude or include + } + if (session.surfaceSwitching) { + constraints.surfaceSwitching = session.surfaceSwitching; // exclude or include + } + if (session.systemAudio) { + constraints.systemAudio = session.systemAudio; // exclude or include + } + if (session.displaySurface && supportedConstraints.displaySurface) { + constraints.video.displaySurface = session.displaySurface; // monitor, window, or browser + } + } catch (e) { + warnlog("navigator.mediaDevices.getSupportedConstraints() not supported"); + } + + if (session.screenshareAEC === false) { + constraints.audio.echoCancellation = false; + } // we want to keep echo cancellation when doing a secondary screen share, unless explicitly disabled. + + if (session.screenshareAutogain === false) { + constraints.audio.autoGainControl = false; + } else if (session.autoGainControl === true) { + constraints.audio.autoGainControl = true; + } + + if (session.screenshareDenoise === false) { + constraints.audio.noiseSuppression = false; + } else if (session.noiseSuppression === true) { + constraints.audio.noiseSuppression = true; + } + + if (session.voiceIsolation === true) { + constraint.audio.voiceIsolation = true; + } + //if (audio == false) { + // constraints.audio = false; + //} + + var overrideFramerate = false; + + if (session.screensharefps !== false) { + constraints.video.frameRate = { + ideal: session.screensharefps, + max: session.screensharefps + }; + } else if (session.frameRate !== false && session.maxframeRate != false) { + overrideFramerate = session.frameRate; + constraints.video.frameRate = { + ideal: session.maxframeRate, + max: session.maxframeRate + }; + } else if (session.frameRate !== false) { + constraints.video.frameRate = session.frameRate; + } else if (session.maxframeRate != false) { + constraints.video.frameRate = { + ideal: session.maxframeRate, + max: session.maxframeRate + }; + } else { + constraints.video.frameRate = { + ideal: 60 + }; + } + + if (session.screenshareVideoOnly) { + constraints.audio = false; + } + + if (session.forceAspectRatio) { + // await updateCameraConstraints("aspectRatio", session.forceAspectRatio); + if (constraints.video && constraints.video !== true) { + constraints.video.aspectRatio = { ideal: parseFloat(session.forceAspectRatio) }; + + if (constraints.video.width && !session.width) { + delete constraints.video.width; + } else if (constraints.video.height && !session.height) { + delete constraints.video.height; + } + } + } + + if (constraints.video !== false && Object.keys(constraints.video).length == 0) { + constraints.video = true; + } + + if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + if (!ElectronDesktopCapture) { + if (!session.cleanOutput) { + warnUser("Enable Elevated Privileges to allow screen-sharing. (right click this window to see that option)"); + } + return false; + } + } + + log("sstype3 screen share"); + log(constraints); + + navigator.mediaDevices + .getDisplayMedia(constraints) + .then(async function (stream) { + try { + var constraint = {}; + if (session.forceAspectRatio && session.forceScreenShareAspectRatio === null) { + constraint.aspectRatio = parseFloat(session.forceAspectRatio); + } else if (session.forceScreenShareAspectRatio) { + constraint.aspectRatio = parseFloat(session.forceScreenShareAspectRatio); + } + if (overrideFramerate) { + constraint.frameRate = overrideFramerate; + } + if (Object.keys(constraint).length) { + await stream.getVideoTracks()[0].applyConstraints({ + advanced: [constraint] + }); + log({ + advanced: [constraint] + }); + } + } catch (e) { + errorlog(e); + } + + session.screenShareState = true; + session.screenStream = stream; + if (session.whipPublishScreen && session.whipOutputScreen) { + whipOutScreen(); + } + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + + //if (!session.screenVideoElement){ + // session.screenVideoElement = createVideoElement() + //} + try { + stream.getVideoTracks()[0].onended = function () { + stopSecondScreenshare(); + }; + } catch (e) { + log("No Video selected; screensharing?"); + } + + session.screenStream.getTracks().forEach(function (track) { + screenshareTracks[track.id] = true; // obs isn't included, so no point to check track.kind + }); + for (UUID in session.pcs) { + createSecondStream2(UUID); + } + + if (!firsttime) { + var msg = {}; + msg.screenStopped = false; + session.sendMessage(msg); + } else if (!session.screenShareElement) { + session.screenShareElement = createVideoElement(); + session.screenShareElement.muted = true; + session.screenShareElement.autoplay = true; + session.screenShareElement.controls = session.showControls || false; + + session.screenShareElement.id = "screensharesource"; + session.screenShareElement.dataset.sid = session.streamID + ":s"; + + if (typeof session.volume == "number") { + session.screenShareElement.volume = session.volume; + } else { + session.screenShareElement.volume = 1.0; // play audio automatically + } + session.screenShareElement.classList.add("tile"); + session.screenShareElement.setAttribute("playsinline", ""); + session.screenShareElement.controlTimer = null; + + session.screenShareElement.dataset.menu = "context-menu-video"; + if (!session.cleanOutput) { + session.screenShareElement.classList.add("task"); // this adds the right-click menu + } + createDirectorScreenshareOnlyBox(); + + if (document.getElementById("videoScreenContainer_director")) { + getById("videoScreenContainer_director").appendChild(session.screenShareElement); + } + + session.screenShareElement.onpause = event => { + // prevent things from pausing; human or other + if (!(event.ctrlKey || event.metaKey)) { + log("Video paused; auto playing"); + event.currentTarget + .play() + .then(_ => { + log("playing 10"); + }) + .catch(warnlog); + } + }; + + session.screenShareElement.addEventListener("click", function (e) { + log("click"); + try { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + var [menu, innerMenu] = statsMenuCreator(); + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu, true); + printMyStats(innerMenu, true); + e.stopPropagation(); + return false; + } + } catch (e) { + errorlog(e); + } + }); + + session.screenShareElement.touchTimeOut = null; + session.screenShareElement.touchLastTap = 0; + session.screenShareElement.touchCount = 0; + + session.screenShareElement.addEventListener("touchend", function (event) { + if (session.disableMouseEvents) { + return; + } + log("touched"); + //document.ontouchup = null; + //document.onmouseup = null; + document.onmousemove = null; + document.ontouchmove = null; + var currentTime = new Date().getTime(); + var tapLength = currentTime - session.screenShareElement.touchLastTap; + clearTimeout(session.screenShareElement.touchTimeOut); + if (tapLength < 500 && tapLength > 0) { + /// + log("double touched"); + session.screenShareElement.touchCount += 1; + event.preventDefault(); + if (session.screenShareElement.touchCount < 5) { + session.screenShareElement.touchLastTap = currentTime; + return false; + } + session.screenShareElement.touchLastTap = 0; + session.screenShareElement.touchCount = 0; + var [menu, innerMenu] = statsMenuCreator(); + menu.interval = setInterval(printMyStats, session.statsInterval, innerMenu, true); + printMyStats(innerMenu, true); + event.stopPropagation(); + return false; + ////// + } else { + session.screenShareElement.touchCount = 1; + session.screenShareElement.touchLastTap = currentTime; + session.screenShareElement.touchTimeOut = setTimeout( + function (vv) { + clearTimeout(vv.touchTimeOut); + vv.touchLastTap = 0; + vv.touchCount = 0; + }, + 5000, + session.screenShareElement + ); + } + }); + } + + firsttime = false; + + session.screenShareElement.srcObject = session.screenStream; + + getById("screensharebutton").classList.add("green"); + getById("screensharebutton").ariaPressed = "true"; + getById("screensharebutton").title = getTranslation("stop-screen-sharing"); + + getById("screenshare2button").classList.add("green"); + getById("screenshare2button").ariaPressed = "true"; + getById("screenshare2button").title = getTranslation("stop-screen-sharing"); + + getById("screenshare3button").classList.add("green"); + getById("screenshare3button").ariaPressed = "true"; + getById("screenshare3button").title = getTranslation("stop-screen-sharing"); + + if (session.autorecord || session.autorecordlocal) { + log("AUTO RECORD START SSTYPE3"); + setTimeout( + function (s) { + if (!session.screenStream) { + return; + } + try { + var ele = document.getElementById("recordLocalScreenbutton"); + if (ele) { + ele.classList.add("red"); + ele.classList.remove("hidden"); + if (!ele.vid) { + var v = createVideoElement(); + v.muted = true; + v.srcObject = s; + ele.vid = v; + } + if (ele.vid.recorder || ele.vid.recording) { + ele.vid.recorder.stop(); + ele.classList.remove("red"); + ele.classList.add("hidden"); + ele.vid = null; + } else { + var videoKbps = session.recordDefault; + if (session.recordLocal !== false) { + videoKbps = session.recordLocal; + } + recordLocalVideo(null, videoKbps, ele.vid); + } + } + } catch (e) { + errorlog(e); + } + }, + 2000, + session.screenStream + ); + } + + setTimeout(function () { + updateMixer(); + }, 100); + + setTimeout(function () { + updateMixer(); + }, 1000); + }) + .catch(function (err) { + errorlog(err); + }); + } else { + // removing a screen + stopSecondScreenshare(); + } +} +function recordLocalScreenStopRecord() { + var ele = document.getElementById("recordLocalScreenbutton"); + if (ele) { + try { + ele.classList.remove("red"); + ele.classList.add("hidden"); + if (ele.vid) { + if (ele.vid.recorder || ele.vid.recording) { + ele.vid.recorder.stop(); + } + ele.vid = null; + } + } catch (e) { + errorlog(e); + } + } +} +function stopSecondScreenshare() { + var msg = {}; + msg.screenStopped = true; + session.sendMessage(msg); + + for (const peerUUID in session.pcs) { + if (!session.pcs.hasOwnProperty(peerUUID)) { + continue; + } + const peer = session.pcs[peerUUID]; + if (peer && "whipScreen" in peer && peer.whipScreen !== false) { + peer.whipScreen = null; + } + } + + var ele = document.getElementById("recordLocalScreenbutton"); + if (ele) { + try { + ele.classList.remove("red"); + ele.classList.add("hidden"); + if (ele.vid) { + if (ele.vid.recorder || ele.vid.recording) { + ele.vid.recorder.stop(); + } + ele.vid = null; + } + } catch (e) { + errorlog(e); + } + } + if (session.screenStream) { + session.screenStream.getTracks().forEach(function (track) { + // previous video track; saving it. Must remove the track at some point. + for (UUID in session.pcs) { + if (!("realUUID" in session.pcs[UUID])) { + continue; + } // not a screen share, so skip + var senders = getSenders2(UUID); + senders.forEach(sender => { + // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams? + if (sender.track && sender.track.kind == "video") { + sender.track.enabled = false; + } + }); + } + if (track.id in screenshareTracks) { + // obs isn't included, so no point to check track.kind + log("remove track 2"); + session.screenStream.removeTrack(track); + track.stop(); + screenshareTracks[track.id] = false; + } + }); + } + if (document.getElementById("container_screen_director")) { + document.getElementById("container_screen_director") + } + + session.screenStream = false; + session.screenShareState = false; + pokeIframeAPI("screen-share-state", session.screenShareState, null, session.streamID); + + if (session.whipOutScreen) { + try { + session.whipOutScreen.getSenders().forEach(sender => { + try { sender.track && sender.track.stop && sender.track.stop(); } catch (e) { } + }); + } catch (e) { } + try { + session.whipOutScreen.close(); + } catch (e) { } + session.whipOutScreen = null; + } + if (session.whipoutScreenSettings) { + session.whipoutScreenSettings.started = false; + } + + getById("screensharebutton").classList.remove("green"); + getById("screensharebutton").ariaPressed = "false"; + getById("screensharebutton").title = getTranslation("share-a-screen"); + + getById("screenshare2button").classList.remove("green"); + getById("screenshare2button").ariaPressed = "false"; + getById("screenshare2button").title = getTranslation("share-a-screen"); + + getById("screenshare3button").classList.remove("green"); + getById("screenshare3button").ariaPressed = "false"; + getById("screenshare3button").title = getTranslation("share-a-screen"); + + if (document.getElementById("screensharesource")) { + document.getElementById("screensharesource").load() + } + + setTimeout(function () { + updateMixer(); + }, 100); + + setTimeout(function () { + updateMixer(); + }, 1000); +} + +function enableFullscreenZoom() { + const content = document.getElementById('gridlayout'); + let lastScrollPosition = { x: 0, y: 0 }; + content.style.overflow = "visible"; + content.style.transformOrigin = "center center"; // Set transform origin to center + + // Add styles for the container to allow centering + content.style.position = "relative"; + content.parentElement.style.display = "flex"; + content.parentElement.style.justifyContent = "center"; + content.parentElement.style.alignItems = "center"; + content.parentElement.style.minHeight = "100vh"; + + content.parentNode.insertAdjacentHTML('afterbegin', + '
    \ + \ +
    '); + + const viewport = document.querySelector('meta[name="viewport"]'); + + viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=4.0, user-scalable=yes'; + if (!viewport) { + const meta = document.createElement('meta'); + meta.name = 'viewport'; + meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=4.0, user-scalable=yes'; + document.head.appendChild(meta); + } + + const slider = document.getElementById('zoomSlider2'); + slider.addEventListener('input', (e) => { + const scale = e.target.value / 100; + + // Calculate the center point before scaling + const rect = content.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + // Apply the transform with translate to maintain center point + content.style.transform = `scale(${scale})`; + + // Calculate scroll position to keep center point + const newWidth = rect.width * scale; + const newHeight = rect.height * scale; + const newLeft = centerX - newWidth / 2; + const newTop = centerY - newHeight / 2; + + window.scrollTo( + window.scrollX + (newLeft - rect.left), + window.scrollY + (newTop - rect.top) + ); + }); +} + +// Auth Access Control Functions +let currentRoomSettings = null; + +async function loadRoomAccessSettings() { + if (!session.authMode || !session.roomid || !window.vdoAuth) return; + + try { + // Get room settings + const response = await fetch(`${AUTH_SERVICE_URL}/api/room/access`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${session.authToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ room: session.roomid }) + }); + + if (response.ok) { + const roomInfo = await response.json(); + if (roomInfo.isOwner) { + // Get detailed room settings + const settingsResponse = await fetch(`${AUTH_SERVICE_URL}/api/room/settings/${session.realRoomId || session.roomid}`, { + headers: { + 'Authorization': `Bearer ${session.authToken}` + } + }); + + if (settingsResponse.ok) { + currentRoomSettings = await settingsResponse.json(); + + // Update UI with current settings + const accessMode = currentRoomSettings.accessMode || 'public'; + document.querySelector(`input[name="roomAccessMode"][value="${accessMode}"]`).checked = true; + updateRoomAccessMode(accessMode); + + // Load allowlist + if (currentRoomSettings.allowlist && currentRoomSettings.allowlist.length > 0) { + displayAllowlist(currentRoomSettings.allowlist); + } + + // Load pending access requests + loadAccessRequests(); + } + } + } + } catch (e) { + console.error('Failed to load room settings:', e); + } +} + +function updateRoomAccessMode(mode) { + // Show/hide allowlist section based on mode + if (mode === 'allowlist') { + getById('allowlistSection').style.display = 'block'; + getById('accessRequestsSection').style.display = 'block'; + } else { + getById('allowlistSection').style.display = 'none'; + getById('accessRequestsSection').style.display = 'none'; + } + + // Update room settings on server + if (currentRoomSettings && window.vdoAuth) { + window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, { + accessMode: mode + }); + } +} + +function addToAllowlist() { + const input = getById('allowlistInput'); + const value = input.value.trim(); + + if (!value) return; + + // Validate format + if (!value.startsWith('@') && !value.startsWith('email:')) { + alert('Please enter a username (starting with @) or email pattern (starting with email:)'); + return; + } + + // Add to current allowlist + if (!currentRoomSettings) { + currentRoomSettings = { allowlist: [] }; + } + + if (!currentRoomSettings.allowlist.includes(value)) { + currentRoomSettings.allowlist.push(value); + + // Update server + if (window.vdoAuth) { + window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, { + allowlist: currentRoomSettings.allowlist + }); + } + + // Update display + displayAllowlist(currentRoomSettings.allowlist); + + // Clear input + input.value = ''; + } +} + +function displayAllowlist(allowlist) { + const display = getById('allowlistDisplay'); + display.innerHTML = ''; + + allowlist.forEach(entry => { + const item = document.createElement('div'); + item.style.cssText = 'padding: 5px; margin: 2px 0; background: #f0f0f0; border-radius: 3px; display: flex; justify-content: space-between; align-items: center;'; + + const label = document.createElement('span'); + label.textContent = entry; + + const removeBtn = document.createElement('button'); + removeBtn.textContent = 'Remove'; + removeBtn.style.cssText = 'padding: 2px 8px; font-size: 12px;'; + removeBtn.onclick = () => removeFromAllowlist(entry); + + item.appendChild(label); + item.appendChild(removeBtn); + display.appendChild(item); + }); +} + +function removeFromAllowlist(entry) { + if (!currentRoomSettings || !currentRoomSettings.allowlist) return; + + const index = currentRoomSettings.allowlist.indexOf(entry); + if (index > -1) { + currentRoomSettings.allowlist.splice(index, 1); + + // Update server + if (window.vdoAuth) { + window.vdoAuth.updateRoomSettings(session.realRoomId || session.roomid, { + allowlist: currentRoomSettings.allowlist + }); + } + + // Update display + displayAllowlist(currentRoomSettings.allowlist); + } +} + +async function loadAccessRequests() { + if (!session.authMode || !session.roomid || !window.vdoAuth) return; + + try { + const requests = await window.vdoAuth.getRoomAccessRequests(session.realRoomId || session.roomid); + displayAccessRequests(requests); + } catch (e) { + console.error('Failed to load access requests:', e); + } +} + +function displayAccessRequests(requests) { + const list = getById('accessRequestsList'); + list.innerHTML = ''; + + if (requests.length === 0) { + list.innerHTML = '
    No pending requests
    '; + return; + } + + requests.forEach(request => { + const item = document.createElement('div'); + item.style.cssText = 'padding: 10px; margin: 5px 0; background: #f9f9f9; border: 1px solid #ddd; border-radius: 5px;'; + + const info = document.createElement('div'); + const header = document.createElement('div'); + header.style.cssText = 'display: flex; align-items: center; margin-bottom: 5px;'; + + if (request.avatar) { + const avatarImg = document.createElement('img'); + avatarImg.src = request.avatar; + avatarImg.alt = ''; + avatarImg.style.cssText = 'width: 30px; height: 30px; border-radius: 50%; margin-right: 10px;'; + header.appendChild(avatarImg); + } + + const textWrap = document.createElement('div'); + const nameEl = document.createElement('strong'); + nameEl.textContent = request.displayName || ''; + textWrap.appendChild(nameEl); + + if (request.userHandle) { + const handleEl = document.createElement('span'); + handleEl.style.cssText = 'color: #666; margin-left: 5px;'; + handleEl.textContent = request.userHandle; + textWrap.appendChild(handleEl); + } + + header.appendChild(textWrap); + info.appendChild(header); + + const meta = document.createElement('div'); + meta.style.cssText = 'color: #999; font-size: 12px;'; + const metaParts = []; + + if (request.provider) { + metaParts.push(request.provider); + } + + if (request.requestedAt) { + const requestedDate = new Date(request.requestedAt); + if (!Number.isNaN(requestedDate.getTime())) { + metaParts.push(requestedDate.toLocaleString()); + } + } + + meta.textContent = metaParts.join(' • '); + info.appendChild(meta); + + const actions = document.createElement('div'); + actions.style.cssText = 'margin-top: 8px; display: flex; gap: 10px;'; + + const approveBtn = document.createElement('button'); + approveBtn.textContent = 'Approve'; + approveBtn.style.cssText = 'padding: 5px 15px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer;'; + approveBtn.onclick = () => handleAccessRequest(request.userId, 'approve'); + + const denyBtn = document.createElement('button'); + denyBtn.textContent = 'Deny'; + denyBtn.style.cssText = 'padding: 5px 15px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer;'; + denyBtn.onclick = () => handleAccessRequest(request.userId, 'deny'); + + actions.appendChild(approveBtn); + actions.appendChild(denyBtn); + + item.appendChild(info); + item.appendChild(actions); + list.appendChild(item); + }); +} + +async function handleAccessRequest(userId, action) { + if (!window.vdoAuth) return; + + try { + const success = await window.vdoAuth.handleAccessRequest(session.realRoomId || session.roomid, userId, action); + if (success) { + // Reload access requests + loadAccessRequests(); + + // Reload allowlist if approved + if (action === 'approve') { + loadRoomAccessSettings(); + } + } + } catch (e) { + console.error('Failed to handle access request:', e); + } +} diff --git a/main.css b/main.css index 39f227a..0540728 100644 --- a/main.css +++ b/main.css @@ -1,4698 +1,4698 @@ -:root { - /* Discord Greys - Dark to Lighter */ - --discord-grey-0: #121212; - --discord-grey-1: #1e1f22; - --discord-grey-2: #232428; - --discord-grey-3: #2c2c2d; - --discord-grey-4: #2e3035; - --discord-grey-5: #313338; - --discord-grey-6: #383a40; - --discord-grey-7: #404249; /* primary */ - --discord-grey-8: #5e6064; - - --discord-text: hsl( 210 calc(1 * 9.1%) 92% /1); - - --darktheme-red: rgb(161, 45, 45); - --darktheme-blue: rgb(33, 69, 114); - --darktheme-green: rgb(36, 88, 49); - --darktheme-lightgreen: #008770; - --darktheme-brown: rgb(76 58 41); - --darktheme-yellow: rgb(84, 70, 9); - - /* Lightmode white - Darker to lighter */ - --lighttheme-1: #fff; - --lighttheme-2: #f3f3f3; - --lighttheme-3: #ddd; - --lighttheme-4: #ccc; /* primary */ - --lighttheme-5: #bbb; - --lighttheme-6: #aaa; - --lighttheme-7: #7e7e7e; - --lighttheme-8: #373737; - --lighttheme-text: black; - - /* Director v2 - General colors */ - /* -- Links */ - --a-dark-link: #69aadc; - --a-dark-visited: #69aadc; - --a-dark-hover: #6da5dd; - --a-dark-focus: #6da5dd; - --a-dark-active: #3a80c6; - - --a-darker-link: #b9dff9; - --a-darker-visited: #b9dff9; - --a-darker-hover: #048ae8; - --a-darker-focus: #d9e4eb; - --a-darker-active: #d9e4eb; - - --a-lighter-link: #9ed0e1; - --a-lighter-visited: #9ed0e1; - --a-lighter-hover: #8acee4; - --a-lighter-focus: #8acee4; - --a-lighter-active: #89d5ee; - - --a-link: #144267; - --a-visited: #144267; - --a-hover: #38668c; - --a-focus: #38668c; - --a-active: #0165b5; - - /* -- Box colors */ - --director-box: rgb(165 119 18); - --codirector-box: rgb(67 122 213); - - --director-dark-box: rgb(165 119 18); - --codirector-dark-box: rgb(129 127 127); - - --widget-width: 25%; - --rtl-or-ltr: left; - - /* Original colors */ - --background-color: #141926; - --dark-background-color: #02050c; - --container-color: #373737; - --button-color: #2A2A2A; - --blue-accent: #4a4c63; - --red-accent: #553737; - --light-grey: #ddd; - --near-black: #02050c; - --green-accent: #3f4f50; - --olive-accent: #535D32; - --regular-margin: 10px; - --director-margin: 15px 20px 0 0; - --fit-style: contain; - --fadein-speed: 0; - --video-margin: 0px; - --video-rounded: 0px; - --video-border: 0px; - --video-border-color: #0000; - --video-holder-color: #0000; - --video-rounded: 0px; - --button-radius: 2px; - --myvideo-max-width: min(800px,100vw); - --myvideo-width:unset; - --myvideo-height:auto; - --myvideo-background: #FFF1; - --video-background-image: url("data:image/svg+xml,%3Csvg viewBox='-42 0 512 512.002' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m210.351562 246.632812c33.882813 0 63.222657-12.152343 87.195313-36.128906 23.972656-23.972656 36.125-53.304687 36.125-87.191406 0-33.875-12.152344-63.210938-36.128906-87.191406-23.976563-23.96875-53.3125-36.121094-87.191407-36.121094-33.886718 0-63.21875 12.152344-87.191406 36.125s-36.128906 53.308594-36.128906 87.1875c0 33.886719 12.15625 63.222656 36.132812 87.195312 23.976563 23.96875 53.3125 36.125 87.1875 36.125zm0 0'/%3E%3Cpath d='m426.128906 393.703125c-.691406-9.976563-2.089844-20.859375-4.148437-32.351563-2.078125-11.578124-4.753907-22.523437-7.957031-32.527343-3.308594-10.339844-7.808594-20.550781-13.371094-30.335938-5.773438-10.15625-12.554688-19-20.164063-26.277343-7.957031-7.613282-17.699219-13.734376-28.964843-18.199219-11.226563-4.441407-23.667969-6.691407-36.976563-6.691407-5.226563 0-10.28125 2.144532-20.042969 8.5-6.007812 3.917969-13.035156 8.449219-20.878906 13.460938-6.707031 4.273438-15.792969 8.277344-27.015625 11.902344-10.949219 3.542968-22.066406 5.339844-33.039063 5.339844-10.972656 0-22.085937-1.796876-33.046874-5.339844-11.210938-3.621094-20.296876-7.625-26.996094-11.898438-7.769532-4.964844-14.800782-9.496094-20.898438-13.46875-9.75-6.355468-14.808594-8.5-20.035156-8.5-13.3125 0-25.75 2.253906-36.972656 6.699219-11.257813 4.457031-21.003906 10.578125-28.96875 18.199219-7.605469 7.28125-14.390625 16.121094-20.15625 26.273437-5.558594 9.785157-10.058594 19.992188-13.371094 30.339844-3.199219 10.003906-5.875 20.945313-7.953125 32.523437-2.058594 11.476563-3.457031 22.363282-4.148437 32.363282-.679688 9.796875-1.023438 19.964844-1.023438 30.234375 0 26.726562 8.496094 48.363281 25.25 64.320312 16.546875 15.746094 38.441406 23.734375 65.066406 23.734375h246.53125c26.625 0 48.511719-7.984375 65.0625-23.734375 16.757813-15.945312 25.253906-37.585937 25.253906-64.324219-.003906-10.316406-.351562-20.492187-1.035156-30.242187zm0 0'/%3E%3C/svg%3E"); - --background-main-image: unset; - --show-codirectors: inline-block; - --full-screen-button: inherit; - --color-mode: light; - --video-background-image-size: auto 30%; -} - -/* Changes color-mode based on what theme the browser states */ -@media (prefers-color-scheme: dark) { - :root { - --color-mode: dark; - } -} - - -* { - padding: 0; - margin: 0; - box-sizing: border-box; - border: 0; -} - -::selection { - background-color: #0447c888; - color: #FFF; -} - -button:hover,[role="button"] -:not(.column) -:not(.controlsGrid) -:not(#controlButtons) -:hover{ - filter: brightness(98%); -} - -table { - display: inline-block; - padding:10px; - margin:10px; -} - -.drawingCanvas { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} -.buttonContainer { - position: absolute; - bottom: 0; - left: 0; - margin: 5px; -} -.buttonContainer button { - margin: 5px 2px; -} - -.promptModalLabel{ - cursor: pointer; - font-weight: normal; - font-size: 1.0em; - display: block; - margin: 17px 20px 15px 20px; -} - -#bigPlayButton { - margin: 0 auto; - background-color: #0000; - cursor: pointer; - font-family: Cousine, monospace; - font-size: 4em; - line-height: 1.5em; - letter-spacing: 0.0em; - text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1); - width: 100%; - height: 100vh; - z-index: 1; - vertical-align: top; - text-align: center; - top: 0; - position: fixed; - overflow-wrap: anywhere; - padding:3%; -} - -.playButton { - border-radius: 50vh; - width: min(30vw, 30vh); - cursor: pointer; - opacity: 100%; - background-color: #bbb; - background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 122.88 122.88' style='enable-background:new 0 0 122.88 122.88' xml:space='preserve'%3E%3Cstyle type='text/css'%3E.st0%7Bfill-rule:evenodd;clip-rule:evenodd;%7D%3C/style%3E%3Cg%3E%3Cpath class='st0' d='M61.44,0c33.93,0,61.44,27.51,61.44,61.44s-27.51,61.44-61.44,61.44S0,95.37,0,61.44S27.51,0,61.44,0L61.44,0z M83.31,65.24c3.13-2.02,3.12-4.27,0-6.06L50.98,40.6c-2.55-1.6-5.21-0.66-5.14,2.67l0.1,37.55c0.22,3.61,2.28,4.6,5.32,2.93 L83.31,65.24L83.31,65.24z M61.44,12.48c27.04,0,48.96,21.92,48.96,48.96c0,27.04-21.92,48.96-48.96,48.96S12.48,88.48,12.48,61.44 C12.48,34.4,34.4,12.48,61.44,12.48L61.44,12.48z'/%3E%3C/g%3E%3C/svg%3E"); - display: inline-block; - height: min(30vw, 30vh); - background-repeat: no-repeat; - border: #bbb 3vh solid; - position: absolute; -} - -#bigPlayButton>.playButton { - width: min(50vw, 50vh); - margin-top: 10vh; - background-color: #646262; - height: min(50vw, 50vh); - border: #646262 7vh solid; - position: unset; - position: static; -} - -#progressContainer { - width: 100%; - background-color: #ddd9; - position: absolute; - right: 0; - bottom: 0; -} -#progressBar { - width: 0%; - height: 18px; - background-color: #4CAF5099; - text-align: left; - color: black; -} - -select#audioSource { - max-height: 80px; /* Initial height */ - overflow-y: auto; /* Enable scrolling */ - transition: max-height 0.3s ease; /* Smooth transition for expanding and collapsing */ - width: 100%; - margin-top: 7px; - padding: 3px 4px; - min-height: 24px; - user-select: none; -} -select#audioSource.expanded { - max-height: none; -} -select#audioSource.expanded option { - display: block; -} -select#audioSource option:checked { - display: block; - background-color: #1967D2!important; - color: white; -} -select#audioSource option { - display: none; -} -select#audioSource option:hover { - background-color: #8dbdd4!important; - color: black; -} -select#audioSource[size='1'] option { - background-color: var(--lighttheme-1)!important; - color:black; -} -.darktheme select#audioSource[size='1'] option { - background-color: var(--light-grey)!important; - color:black; -} -.paused { - cursor: pointer; -} - -tr { - padding:4px; -} -th { - padding:4px; -} - -.popupSelector_constraints .preSelectbutton { - display: inline-block; - margin: 4px 0 4px 3px; - padding: 2px 8px 1px 8px; -} -.advancedVideoSettings .preSelectButton { - display: inline-block; - margin: 4px 0 4px 3px; - padding: 2px 8px 1px 8px; -} -.meter { - display: inline-block; - width: 0px; - height: 10px; - background: green; - transition: all 100ms linear; -} -.meter2 { - display: inline-block; - width: 0px; - height: 10px; - background: yellow; - transition: all 50ms linear; -} -.meter3 { - display: inline-block; - width: 0px; - height: 10px; - background: red; - transition: all 25ms linear; -} -.meter4 { - display: inline-block; - width: 2px; - height: 10px; - background: black; - position:relative; - float:left; -} -#mynetwork { - width: 600px; - height: 400px; - border: 1px solid lightgray; -} - -.rtl { - direction: rtl; - text-align: right; -} -.rtl input, -.rtl textarea { - text-align: right; -} -[dir="rtl"] .keep-ltr { - direction: ltr; - text-align: left; -} -[dir="rtl"] .mirror-rtl { - transform: scaleX(-1); -} - -.email { - unicode-bidi: bidi-override; - direction: rtl!important; - user-select: none; -} - -a:link { - color: var(--a-link); -} -a:visited { - color: var(--a-visited); -} -a:hover { - color: var(--a-hover); -} -a:focus { - color: var(--a-focus); -} -a:active { - color: var(--a-active); -} - -a.soloLink:link { - cursor: grab; - font-size: 1.2em; - font-weight: 700; - padding: 4px 0 2px 0; - border-radius: 5px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: #b4d7f6; -} -a.soloLink:visited{ - color: #b4d7f6; -} - -/* Links */ -a { - text-decoration: none; -} -.darktheme a:link { - color: var(--a-dark-link); -} -.darktheme a:visited { - color: var(--a-dark-visited); -} -.darktheme a:hover { - color: var(--a-dark-hover); -} -.darktheme a:focus { - color: var(--a-dark-focus); -} -.darktheme a:active { - color: var(--a-dark-active); -} - -.directorContainer a:link { - color: var(--a-lighter-link); -} -.directorContainer a:visited { - color: var(--a-lighter-visited); -} -.directorContainer a:hover { - color: var(--a-lighter-hover); -} -.directorContainer a:focus { - color: var(--a-lighter-focus); -} -.directorContainer a:active { - color: var(--a-lighter-active); -} - -.infoblob a:link { - color: var(--a-lighter-link); -} -.infoblob a:visited { - color: var(--a-lighter-visited); -} -.infoblob a:hover { - color: var(--a-lighter-hover); -} -.infoblob a:focus { - color: var(--a-lighter-focus); -} -.infoblob a:active { - color: var(--a-lighter-active); -} - -.darktheme .infoblob a:link { - color: var(--a-darker-link); -} -.darktheme .infoblob a:visited { - color: var(--a-darker-visited); -} -.darktheme .infoblob a:hover { - color: var(--a-darker-hover); -} -.darktheme .infoblob a:focus { - color: var(--a-darker-focus); -} -.darktheme .infoblob a:active { - color: var(--a-darker-active); -} - -/* White theme styling */ -body.whitetheme { - background-color: #ffffff; - color: #000000; -} -.whitetheme #head4 { - color: black; -} -.whitetheme #reshare { - color: #292 !important; -} -.whitetheme #miniPerformer button { - color: #FFF; -} -.whitetheme .infoblob{ - color: #000000; -} -.whitetheme div#guestFeeds, .whitetheme #roomHeader .hideLinksClass{ - background-color: #ababab; -} - -.whitetheme .container-inner { - background-color: #f8f8f8; -} - -.whitetheme button { - background-color: #f0f0f0; - border: 1px solid #ddd; - color: #000000; -} - -.whitetheme button:hover { - background-color: #e8e8e8; -} - -.whitetheme .directorContainer { - background-color: #f5f5f5; -} - -.whitetheme .card { - background-color: #f5f5f5; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -.whitetheme #addPasswordBasic, -.whitetheme #avatarDiv, -.whitetheme #avatarDiv2, -.whitetheme #avatarDiv3, -.whitetheme #audioScreenShare1, -.whitetheme #audioMenu, -.whitetheme #audioMenu2, -.whitetheme #effectsDiv, -.whitetheme #effectsDiv2, -.whitetheme #effectsDiv3, -.whitetheme #grabDirectorSoloLinkParent, -.whitetheme #videoMenu, -.whitetheme #videoMenu2, -.whitetheme #videoMenu3, -.whitetheme #headphonesDiv, -.whitetheme #headphonesDiv2, -.whitetheme #headphonesDiv3, -.whitetheme #videoSettings, -.whitetheme #videoSettings2, -.whitetheme #popupSelector_user_settings, -.whitetheme .invite_setting_group { - background-color: #ffffff; - border: 1px solid #e0e0e0; -} - -.whitetheme select { - background-color: #ffffff; - border: 1px solid #ddd; - color: #000000; -} - -.whitetheme input[type='text'], -.whitetheme input[type='password'] { - background-color: #ffffff; - border: 1px solid #ddd; - color: #000000; -} - -.whitetheme .directorsgrid .vidcon { - background-color: #f8f8f8; - border: 1px solid #e0e0e0; -} - -.whitetheme #promptModal, -.whitetheme .customModelPopup, -.whitetheme .promptModal { - background-color: #ffffff; - color: #000000; - box-shadow: 0 0 30px rgba(0, 0, 0, 0.15); -} - -.whitetheme .cameraTip { - background-color: #e8f4ff; - border-left: 4px solid #50cef1; - color: #000000; -} - -.whitetheme #header { - color: #000000; -} - -.whitetheme .modalBackdrop { - background-color: rgba(255, 255, 255, 0.9); -} - - -input { - border-radius: 4px; - padding: 2px; -} - -button.grey { - padding: 10px; - margin: 10px 0px; - cursor: pointer; - border-radius: 2px; - background-color: var(--button-color); - color: white; -} - -button.hint { - -webkit-box-shadow: inset 0px 0px 25px #0004; - -moz-box-shadow: inset 0px 0px 25px #0004; - box-shadow: inset 0px 0px 25px #0004; -} - -#miniPerformer > video, #miniPerformer > canvas{ - width: 80px; - height: 45px; - margin: 5px; - background-color: #464749 !important; - background-size: 50%; -} - -#popOutChat{ - cursor: pointer; - text-align:right; - color:#B3C7F9; - margin: 0 5px; - cursor: pointer; - padding:3px; - background-color: black; - border-radius: 50%; - border: solid #B3C7F9 1px; -} - -#closeChat { - cursor: pointer; - text-align: right; - color: #B3C7F9; - margin: 0 5px; - cursor: pointer; - padding: 3px 8px; - background-color: black; - border-radius: 50%; - border: solid #B3C7F9 1px; -} - -/* Clicked buttons overwrite */ -.red { - background-color: #840000 !important; -} -.red:hover { - background-color: #b30c0c !important; -} - -.green { - background-color: #64c04d !important; -} - -.green:hover { - background-color: #76c762 !important; -} - -.blue { - background-color: #161699 !important; -} - -.blue:hover { - background-color: #2727bb !important; -} - -.brown { - background-color: #8d6418 !important; -} - -.brown:hover { - background-color: #a06d10 !important; -} - -/* ///////////////////// */ - -.orange { - background-color: #673100 !important; -} - -#meshcastMenu{ - display: inline-block; - color: #e0dfdf; -} -#header { - width: 100%; - padding: 1px; - color: #FFF; - background-color: #0005; -} -#head1{ - display: inline-block; - padding:1px; - position: relative; -} -#head4{ - font-size: 68%; - color: white; -} -#head9 { - color: #b5e4ff; - font-weight: 500; - font-size: 120%; -} -#head5 { - display: inline-block; - text-decoration: none; - color: white; - text-align: right; - margin-right: 10px; - cursor: help; - float: right; - font-size: 90%; - line-height: 100%; - margin-top: 2px; -} -#head6 { - display: inline-block; - text-decoration: none; - color: white; - text-align: left; - margin-left: 10px; - pointer-events: none; - font-weight: 700; -} - -#head7 { - display: inline-block; - text-decoration: none; - color: white; - text-align: left; - margin-left: 10px; - pointer-events: none; - font-weight: 700; -} -#overlayClockContainer{ - margin: 0 auto; - background-color: #0000; - color: white; - font-family: Cousine, monospace; - font-size: calc(6vh + 6vw / 2); - letter-spacing: 0.0em; - text-shadow: 0.05em 0.05em 0px rgb(0 0 0); - z-index: 6; - vertical-align: top; - text-align: right; - right:0; - bottom: 0; - position: fixed; - overflow-wrap: anywhere; - cursor: pointer; - user-select: none; -} -#overlayClockContainer.top { - top:0%; - bottom:unset; -} -#overlayClockContainer.vmiddle { - bottom: 48%; - top:unset; -} -#overlayClockContainer.bottom { - bottom: 0%; - top:unset; -} -#overlayClockContainer.left { - right:unset; - left: 0; -} -#overlayClockContainer.hmiddle { - right:45%; - left:unset; -} -#overlayClockContainer.right { - right:0; - left:unset; -} - -#overlayClock{ - padding:2px 20px; - background-color: #0009; -} -#overlayClock video { - width: calc(22vh + 22vw / 2); - max-width: 100%; - max-height: 25%; -} -#overlayClock:empty{ - display: none; -} -#overlayClockContainer2{ - margin: 0 auto; - background-color: #0000; - color: white; - font-family: Cousine, monospace; - font-size: calc(3vh + 3vw / 2); - letter-spacing: 0.0em; - text-shadow: 0.05em 0.05em 0px rgb(0 0 0); - z-index: 6; - vertical-align: top; - text-align: right; - position: fixed; - right:0; - bottom: 0; - overflow-wrap: anywhere; - cursor: pointer; - user-select: none; -} -#overlayClockContainer2.top { - top:0%; - bottom:unset; -} -#overlayClockContainer2.vmiddle { - bottom: 48%; - top:unset; -} -#overlayClockContainer2.bottom { - bottom: 0%; - top:unset; -} -#overlayClockContainer2.left { - right:unset; - left: 0; -} -#overlayClockContainer2.hmiddle { - right:45%; - left:unset; -} -#overlayClockContainer2.right { - right:0; - left:unset; -} - -#overlayClock2{ - padding:0 5px; - background-color: #0009; -} -#overlayClock2:empty{ - display: none; -} -#stickyMsgs{ - margin: 0 auto; - background-color: #0000; - color: white; - font-family: Cousine, monospace; - font-size: 6vh; - line-height: 8vh; - letter-spacing: 0.0em; - text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1); - width: 100%; - z-index: 7; - vertical-align: top; - text-align: center; - position: fixed; - overflow-wrap: anywhere; - padding:2% 3%; -} -.avatarSelection{ - vertical-align: top; - margin: 10px 0; - width: 130px; - display: inline-block; - margin: 0 1px; - text-align: center; - cursor: pointer; -} -.overlayCloseBtn{ - padding: 0; - width: 16px; - height: 16px; - position: relative; - bottom: 7px; - padding: 18px 18px 20px 18px; - font-size: 22px; - margin-left: 20px; -} - -#overlayMsgs{ - margin: 0 auto; - background-color: #0000; - color: white; - font-family: Cousine, monospace; - font-size: 6vh; - line-height: 8vh; - letter-spacing: 0.0em; - text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1); - width: 100%; - height: 100vh; - z-index: 7; - vertical-align: top; - text-align: center; - position: fixed; - overflow-wrap: anywhere; - padding:2% 3%; - pointer-events: none -} -#overlayMsgs span{ - background-color: #000B; - padding: 2px; - margin: 0.5vh; - text-align: center; - width: 100%; - pointer-events: none -} - -#credits { - position: fixed; - bottom: 2px; - right: 0; - font-size: 80%; - margin-right:31px; -} - - -#legal { - padding-right: 6px; - bottom: 3px; - position: relative; -} -@media only screen and (max-width: 1023px){ - #credits { - position: static; - right: auto; - bottom: auto; - margin: 24px 0 16px; - padding: 0 16px; - text-align: center; - width: 100%; - } - #legal { - display:none; - } -} - -.footer { - margin-top: 10px; - color: #101020; - font-size: 80%; -} - -.footer>a { - color: #101020; -} - -.footer>a:visited { - color: #101020; -} - -body.darktheme .footer { - color: #707a93; -} - -body.darktheme .footer>a { - color: #707a93; -} - -body.darktheme .footer>a:hover { - color: #769ade; -} - -body.darktheme .footer>a:visited { - color: #707a93; -} - -.label { - float: left; - font-size: 1.2em; - color: white; - display: inline-block; - position: absolute; - bottom: 0; - align-self: center; - z-index: 1000; - margin: 5% 20%; - padding: 1%; - background-color: black; -} - -.advancedAudioSettings, .advancedVideoSettings { - display: flex; - max-height: 300px; - overflow-y: auto; - width: 100%; - font-size: 14px; -} - -.advancedAudioSettings div { - display: flex; - width: 100%; - align-items: center; - gap: 4px; -} -.advancedAudioSettings div button { - padding: 4px; - height: 24px; - margin: unset; - flex: 1; -} - -.advancedAudioSettings div select { - width: 100%; - border-radius: 4px; - flex: 2; - height: 24px; - box-shadow: 1px 1px 3px rgba(0,0,0,0.75); - font-size: 14px; - padding: 0; -} - -.advancedAudioSettings div select[data-chosen='false'], .advancedVideoSettings div select[data-chosen='false'] { - border: 1px solid red; -} -.advancedAudioSettings div select[data-chosen='true'], .advancedVideoSettings div select[data-chosen='true'] { - border: 1px solid green; -} - -.advancedAudioSettings > div:nth-child(1) { - flex-direction: column; - align-items: flex-start; - width: 100%; - padding: unset; -} - -.darktheme .advancedAudioSettings .settingsLabel, .darktheme .advancedVideoSettings .settingsLabel { - color: var(--lighttheme-3); -} - -.advancedAudioSettings .settingsLabel, .advancedVideoSettings .settingsLabel { - display: block; - color: #fff; - font-family: system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,Oxygen,Ubuntu,Cantarell,open sans,helvetica neue,sans-serif; - font-size: 12px; - margin-top: 15px; - border-top: 3px solid #4f4c4c; - width: calc(100% - 4px); - padding: 5px 0; -} - -.advancedVideoSettings div:nth-child(2) { - display: flex; - width: 100%; - align-items: center; - gap: 4px; -} - -.advancedVideoSettings div:nth-child(2) select { - width: 100%; - flex: 2; - height: 24px; - border-radius: 4px; - box-shadow: 1px 1px 3px rgba(0,0,0,0.75); - font-size: 14px; -} - -.advancedVideoSettings div:nth-child(2) button { - width: 100%; - flex: 1; - height: 24px; - margin: unset; -} -.advancedAudioSettings label, .advancedVideoSettings label { - color: #000; -} -.darktheme .advancedAudioSettings label, .darktheme .advancedVideoSettings label { - color: #FFF; -} - -.pressed.altpress, .altpress { - background: #673100 !important; - -webkit-box-shadow: inset 0px 0px 1px #b90000; - -moz-box-shadow: inset 0px 0px 1px #b90000; - box-shadow: inset 0px 0px 1px #b90000; - outline: none; - color: white; -} -.pressed.armed, .armed { - background: #BF3F3F !important; -} -#mainmenu.row { - text-align: center; - margin-top: 20px; - padding: 0 10px; -} -#mainmenu.row:after { - content: ""; - display: table; - clear: both; -} - -hr { - height: 2px; - border-width: 0; - color: gray; - background-color: gray; -} - -.vidcon { - max-width: 100%; - border: 0; -} - -.tile { - object-fit: var(--fit-style); - width: 100%; - height: 100%; - overflow: hidden; - border-radius: var(--video-rounded); -} - -#gridlayout, #directorlayout { - padding: 0; - width: 100%; - height: 100%; - overflow: hidden; - justify-items: stretch; - border: 0; - margin: 0; -} -#gridlayout{ - z-index:-1; -} -.directorsgrid { - justify-items: normal; - display: block ! important; - overflow-y: auto !important; -} - -.directorsgrid .vidcon video { - margin: 0 5px; - padding:0; - width: 100%; - max-height: 148px; - height:unset; - max-width: 260px; - min-height: 80px; -} - -.directorsgrid .vidcon { - display: inline-block; - width: 269.7px!important; - background: var(--lighttheme-7); - color: var(--lighttheme-text); - vertical-align: top; -} - -.directorsgrid .vidcon>.las { - color: black; - background: #999999; - width: 90%; -} -#activeShares>div{ - font-weight: normal; - font-size: 12px; - margin: 10px 0 0 0; -} - -.minimized { - float: left; - position: absolute; - bottom: 0; - height: 24px; - overflow:hidden; - box-shadow: inset -1px 1px white -} - -.minimized:nth-child(1) { - left:0px; - z-index:10; -} -.minimized:nth-child(2) { - left:max(100px, calc(10% - 260px)); - z-index:9; -} -.minimized:nth-child(3) { - left:max(200px, calc(20% - 260px)); - z-index:8; -} -.minimized:nth-child(4) { - left:max(250px, calc(30% - 260px)); - z-index:7; -} -.minimized:nth-child(5) { - left:max(300px, calc(40% - 260px)); - z-index:6; -} -.minimized:nth-child(6) { - left:max(463px, calc(50% - 260px)); - z-index:5; -} -.minimized:nth-child(7) { - left:max(500px, calc(60% - 260px)); - z-index:4; -} -.minimized:nth-child(8) { - left:max(650px, calc(70% - 260px)); - z-index:3; -} -.minimized:nth-child(9) { - left:max(700px, calc(80% - 260px)); - z-index:2; -} -.minimized:nth-child(10) { - left:max(850px, calc(90% - 260px)); - z-index:1; -} -.minimized:nth-child(11) { - left:max(900px, calc(100% - 260px)); - z-index:0; -} -.battery { - border: 3px solid #4192c5; - width: 11px; - height: 19px; - border-radius: 4px; - position: absolute; - left: 27px; - top: 3px; - background-color: #FFF2; - font-size: 1.5em; - z-index: 2; - cursor: help; - display: block; -} - -.battery-charging{ - margin: 0; - left: -1px; - padding: 0; - position: absolute; - font-size: 0.54em; - display: none; -} - -.battery[data-plugged="1"] { - display: none; -} - -.battery.warn { - border: 3px solid #EFAF13; - animation: blink-warn 2s infinite; -} -.battery.alert { - border: 3px solid #e81309; - animation: blink-alert 1s infinite; -} -.battery-level { - background: #30b455; - position: absolute; - bottom: 0px; - right: 0; - left: 0; - font-size: 0.7em; - margin: 0; - padding: 0; -} - -.hasMedia > .battery { - display: block; -} -.slotsbar { - border-radius: 6px; - margin: 3.8px 3.8px 0 3.8px; - padding: 0 6px; - box-shadow: 0 0 1px #111; - cursor:grab; - color: white; - text-shadow: 0 0 1px black; - text-align: center; -} -.slotsbar>button{ - margin: 0; - padding: 0 10px; -} -.slotsbar:active { - cursor:grabbing; -} -[data-slot='0'] { - background: linear-gradient(145deg, #dadada, #b8b8b8); -} -#slotpicker{ - box-shadow: 0 0 1px #908080; - width: 180px; - margin: 3px; - background-color: white; - text-shadow: 0px 0px 1px white; - vertical-align: middle; - align-content: center; - text-align: center; - position: absolute; - z-index: 5; - border-radius: 3px; - border: 1px solid black; - outline: 6px solid #444; -} -#slotPicker [data-slot]{ - margin: 2px; - width: 50px; - height: 34px; - border-radius: 3px; - display: inline-block; - cursor: pointer; - font-size:12px; -} -#alertModalMessage [data-slot]{ - margin: 2px; - cursor: pointer; - text-shadow: 0px 0px 10px white; -} -#alertModalMessage{ - display: block; - max-height: 70vh; - overflow-y: auto; -} -.alertModal--anchored{ - transform: none !important; -} -.rem-con-count{ - position: absolute; - left: 49px; - top: 0px; - color: white; - background-color: #0006; - font-size: 1em; - z-index: 2; - cursor: help; - border-radius: 4px; - padding: 1px 4px 1px 0px; -} -.signal-meter{ - width: 22px; - height: 22px; - position: absolute; - left: 5px; - top: 1px; - background-color: #FFF2; - font-size: 1.5em; - z-index: 2; - cursor: help; -} -.hasMedia > .signal-meter { - display: block; -} -.signal-meter[data-cpu="0"]>.la-signal { - display: block; -} -.signal-meter[data-cpu="0"]>.la-fire-alt { - display: none; -} -.signal-meter[data-cpu="1"]>.la-signal { - display: none; -} -.signal-meter[data-cpu="1"]>.la-fire-alt { - display: block; -} -.signal-meter[data-cpu="1"] { - display:block!important; -} -.signal-meter[data-level="0"] { - color:#000F; - display: none; -} -.signal-meter[data-level="1"] { - color:#FF1B01; -} -.signal-meter[data-level="2"] { - color:#FF8D01; -} -.signal-meter[data-level="3"] { - color:#FFD201; -} -.signal-meter[data-level="4"] { - color:#C6FF01; -} -.signal-meter[data-level="5"] { - color:#00FF00; -} - -.volume-control { - height: 44px; - position: absolute; - left: 0px; - bottom: 0px; - font-size: 1.5em; - z-index: 2; - cursor: help; - padding: 18px 5% !important; - background: #555A; - border-radius: 0 !important; - margin: 0!important; -} - -#mutespeakerbutton { - position: relative; -} - -.speaker-volume-panel { - position: absolute; - left: 50%; - transform: translateX(-50%); - bottom: 120%; - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - padding: 10px 12px; - border-radius: 12px; - background: rgba(24, 32, 45, 0.94); - box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45); - z-index: 30; -} - -.speaker-volume-panel input[type=range] { - -webkit-appearance: slider-vertical; - writing-mode: bt-lr; - height: 110px; - width: 18px; - cursor: pointer; - padding: 0px; -} - -.speaker-volume-value { - color: #fff; - font-size: 0.8em; - font-weight: 600; - letter-spacing: 0.02em; -} - -.togglePreview{ - border-radius: 11px; - background-color: #00000044; - top: calc(19px + 2vh); - right: 2vw; - cursor: pointer; - width: 22px; - display: flex; - margin: 5px; - position: absolute; - color: white; - font-size: 22px; - z-Index:35; - height: 22px; -} -.togglePreview > .la-eye-slash{ - display: block; -} - -.togglePreview > .la-eye{ - display: none; -} -.togglePreview.blinded > .la-eye-slash{ - display:none!important; -} -.togglePreview.blinded > .la-eye{ - display:block!important; -} -.rounded{ - border-radius: 5px; -} - -.mirror{ - transform: scaleX(-1); -} - -.notification { - position: absolute; - top: -4px; - right: -4px; - padding: 2px 0; - border-radius: 50%; - color: white; - width: 11px; - height: 11px; - margin: 0; -} -.queueNotification { - - padding: 2px 0; - border-radius: 50%; - background: #335c3a; - color: white; - width: 23px; - height: 23px; - margin: 0; -} - -/* Highlight the queue controls whenever guests are waiting */ -#queuebutton.queueAttention { - box-shadow: 0 0 0 2px rgba(255, 120, 120, 0.35); - border-radius: 8px; -} - -#queuebutton.queueAttention i { - color: #ff7b7b; -} - -.queueNotification.queueNotificationPulse { - animation: queuePulse 1.6s ease-in-out infinite; -} - -@keyframes queuePulse { - 0% { - transform: scale(1); - background: #335c3a; - } - 50% { - transform: scale(1.08); - background: #ff7b7b; - } - 100% { - transform: scale(1); - background: #335c3a; - } -} - -button.glyphicon-button:focus, -button.glyphicon-button:active:focus, -button.glyphicon-button.active:focus, -button.glyphicon-button.focus, -button.glyphicon-button:active.focus, -button.glyphicon-button.active.focus { - outline: none !important; -} - -.main { - -webkit-tap-highlight-color: rgba(255, 255, 255, 0) !important; - -webkit-tap-highlight-color: transparent !important; - outline: 0px !important; - height: 100%; - animation: fadeIn 0.2s; - background-size: cover; - background-image: var(--background-main-image); - background-repeat: no-repeat; - background-attachment: fixed; - background-position: center; - overflow-x: hidden; - position: absolute; - z-index: 0; -} - -#controlButtons { - display: flex; - position: fixed; - z-index: 995; - padding: 0px 10px; - bottom: 10px; - padding: 0 10px; - border: 0; - min-height: 0; /* Must have a min-height or drag-drop doesn't work */ - pointer-events: none; - width: 100%; - justify-content: center; - align-items: center; - transform-origin: bottom; /* Keeps it at bottom even if scaled */ -} -#controlButtons:empty { - display: none; -} - -.controlPositioning { - display: flex; - flex-direction: column; - justify-content: center; - position: relative; - width: 100%; - height: 100%; - align-items: center; -} - -.resizable-div { - border: 1px solid #ccc; - overflow: auto; - position: relative; -} - -#subControlButtons { - display: flex; - position: absolute; - background-color: var(--discord-grey-0); - box-shadow: 0px 0px 10px rgba(0,0,0,1); - pointer-events: auto; - border: #cccccc22 1px solid; - border-radius: 10px; - align-items: center; - justify-content: center; - flex-wrap: wrap; - bottom: 0px; - min-width: 230px; - cursor: grab; -} - -#subControlButtons:empty{ - display: none; -} - -#subControlButtons div, #subControlButtons span button { - display: flex; - align-items: center; - justify-content: center; - background-color: var(--discord-grey-1); - opacity: unset; - border-radius: 8px; - transition: border-radius 200ms ease-in-out; - box-shadow: 1px 1px 3px rgba(0,0,0,0.75); -} - -#subControlButtons div:hover { - background-color: var(--discord-grey-3); - border-radius: 4px; -} -button#press2talk{ - border: 0; -} -#press2talk:hover { - box-shadow: inset 0px 0px 1px 1px #346324; -} -#press2talk[data-enabled="true"] { - background: #1e0000; - -webkit-box-shadow: inset 0px 0px 1px #b90000; - -moz-box-shadow: inset 0px 0px 1px #b90000; - outline: none; -} -/* Set for the notification button to use as offset */ -#chatbutton { - position: relative; -} - -button.btnArmTransferRoom{ - width:auto; - margin-left: 2px; - height:38px; - border-radius: 15px; -} -button.btnArmTransferRoom i{ - margin-right:3px; -} -button.btnArmTransferRoom:hover{ - background-color: var(--green-accent)!important; -} - -button.btnArmTransferRoom.selected{ - background-color: #840000!important; -} - -#container.vidcon { - height: 100%; -} - -.nocontrolbar #container.vidcon { - height:100%!important; -} - -.labelSmall { - display: none; -} - -@media only screen and (max-width: 640px){ - .labelSmall { - display:inherit; - padding:5px; - } - - #guestTips{ - max-width: 100%; - font-size: 90%; - } - - .labelLarge { - display:none!important; - } - - .gobutton{ - width: 100vh; - margin-left: 10px; - margin-right: 10px; - max-width: 87%; - } - - .roomnotes{ - display:none!important; - } - - #head5 { - margin-right: 1px; - } -} -@media only screen and (max-width: 480px){ - #guestTips{ - max-width: 100%; - font-size: 80%; - } -} - -@media only screen and (max-height: 540px){ - #gridlayout>#container.vidcon { - height:88% - } - #copythisurl { - font-size:80%; - } -} -@media only screen and (max-height: 500px){ - #gridlayout>#container.vidcon { - height:87% - } -} -@media only screen and (max-height: 400px){ - #logoname{ - display: none; - } - #head4{ - display: none; - } - #head2{ - display: none; - } - #gridlayout>#container.vidcon { - height:85% - } -} -@media only screen and (max-height: 300px){ - #gridlayout>#container.vidcon { - height:81% - } - #head2 { - display:none !important; - } - -} -@media only screen and (max-height: 240px){ - #gridlayout>#container.vidcon { - height:78% - } -} -@media only screen and (max-height: 190px){ - #gridlayout>#container.vidcon { - height:75% - } -} -@media only screen and (max-height: 160px){ - #gridlayout>#container.vidcon { - height:70% - } -} -@media only screen and (max-height: 120px){ - #gridlayout>#container.vidcon { - height:70% - } -} - -#header:empty{ - display: none; -} - -.la-sliders-h { - cursor: pointer; -} - -.icn-spinner { - animation: spin-animation 3s infinite; - display: inline-block; - z-index: 10; -} - -#recordLocalbutton.la-spinner { - animation: spin-animation 3s infinite; - display: inline-block; -} - -.retry-spinner { - border: 1vh solid #7f838666; - border-top: 1vh solid #f0f0f066; - border-radius: 50%; - width: 10vh; - height: 10vh; - animation: spin-animation 3s infinite linear, fadeIn 5s; - margin: 44vh auto; - cursor: help; -} -#retryimage{ - display: block; - margin: auto; - max-width: 100%; - max-height: 100%; - width: 100%; - height: 100%; - animation: fadeIn 2s; - object-fit: contain; -} -#retrymessage{ - display: block; - margin: 80vh auto; - animation: fadeIn 2s; - color: white; - position: absolute; - left: 0; - top: 0; - float: left; - width: 100%; - height: 100%; - text-align: center; - font-size: 2em; -} - -html { - border: 0; - margin: 0; - outline: 0; -} - -body { - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - color: var(--gray90); - padding: 0 0px; - height: 100%; - width: 100%; - background-color: var(--background-color); - font-family: Helvetica, Arial, sans-serif; - border: 0; - margin: 0; - opacity: 1; - transition: opacity 0.1s linear; - scrollbar-color:#666 #201c29; - display:flex; - flex-direction: column; - position: fixed; -} -body.darktheme{ - background-color: var(--dark-background-color); -} - -::-webkit-scrollbar { - width: 15px; - height: 15px; -} - -::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 13px rgb(0 0 0 / 50%); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb { - border-radius: 5px; - -webkit-box-shadow: inset 0 0 16px rgb(150 150 150 / 100%); - border: solid 3px transparent; -} - -.previewWebcam { - max-width: 640px; - max-width: 83vw; - height: 30vh; - opacity:1; - animation: fadeIn 0.2s; -} - -#getPermissions{ - font-size: 110%; - border: 3px solid #99A; - cursor: pointer; - background-color: #cce0ff; - margin: 20px; - padding: 10px 50px; - text-align:center; -} - -.gowebcam { - font-size: 110%; - border: 3px solid #ddd; - background-color: #f0f0f0; - color: black; - cursor: pointer; - margin: 20px; - padding: 10px 50px; -} -.gowebcam:enabled { - background-color: #26e726 !important; - background: radial-gradient(#26e726, #2EeF2E); - color: black!important; - font-weight: bold !important; - box-shadow: 0 0 31px 0px #244e1c44; - animation: pulsate 2s ease-out infinite; -} -#logoname { - transition: opacity 0.2s ease-in-out; -} -#logoname:hover { - opacity: 0.8; -} - -#mainmenu { - height: 100vh; -} -.mainmenuclass { - display: inline-block; - width: 100%; -} -.welcomeOverlay{ - object-fit: cover; - width: 100%; - height: 100%; - display: block; - position: absolute; - left: 0; - z-index: 500; - top: 0; - animation: fadeIn 0.1s; - -webkit-animation: fadeIn 0.3s; - -moz-animation: fadeIn 0.3s; - -o-animation: fadeIn 0.3s; - -ms-animation: fadeIn 0.3s; - animation-iteration-count: 1; -} -div[data-action-type='toggle-group'] { - padding: 0 10px; -} -.infoblob { - color: white; - width: 100%; - padding: 20px; - max-width: 1280px; - text-align: var(--rtl-or-ltr); -} - -.outer { - position: relative; - margin: auto; - width: 70px; - margin-top: 0px; - cursor: pointer; -} - -.close { - position: absolute; - right: 20px; - top: 20px; - cursor: pointer; - display: none; -} -.testtonebutton{ - margin: 0 0 0 15px; - padding: 0px 10px 0px 10px !important; - font-size: 84%; - border-radius: 5px!important; - box-shadow: 10px 8px 32px -10px #8883; -} -.testtonebutton:hover{ - background-color: #DDD; -} -.testtonebutton:active{ - background-color: #AAA; -} - -.highlight { - background-color: #ddeeff; -} - -#effectSelector{ - display: inline-block; - padding:2px 0; - min-height: 24px; -} - -#passwordBasicInput{ - min-height: 24px; -} - -/*https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/*/ - -input[type=range] { - -webkit-appearance: none; - margin: 18px 0; - width: 100%; - background-color: #0000; -} - -input[type=range]:focus { - outline: none; -} - -input[type=range]::-webkit-slider-runnable-track { - width: 100%; - height: 8.4px; - cursor: pointer; - box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; - background: #3071a9; - border-radius: 1.3px; - border: .2px solid #010101 -} - -input[type=range]:focus::-webkit-slider-runnable-track { - background: #367ebd -} - -input[type=range]::-moz-range-track { - width: 100%; - height: 8.4px; - cursor: pointer; - box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; - background: #3071a9; - border-radius: 1.3px; - border: .2px solid #010101 -} - -input[type=range]::-ms-track { - width: 100%; - height: 8.4px; - cursor: pointer; - background: 0 0; - border-color: transparent; - border-width: 16px 0; - color: transparent -} - -input[type=range]::-webkit-slider-thumb { - box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; - border: 1px solid #000; - height: 24px; - width: 24px; - border-radius: 3px; - cursor: pointer; - -webkit-appearance: none; - margin-top: -9px; - background-color: var(--lighttheme-2); -} - -input[type=range]::-moz-range-thumb { - box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; - border: 1px solid #000; - height: 24px; - width: 24px; - border-radius: 3px; - cursor: pointer; - background-color: var(--lighttheme-2); -} - -input[type=range]::-ms-thumb { - box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; - border: 1px solid #000; - height: 24px; - width: 24px; - border-radius: 3px; - cursor: pointer; - background-color: var(--lighttheme-2); -} - -input[type=range]::-ms-fill-lower { - background: #2a6495; - border: .2px solid #010101; - border-radius: 2.6px; - box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; -} - -input[type=range]::-ms-fill-upper { - background: #3071a9; - border: .2px solid #010101; - border-radius: 2.6px; - box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; -} - -input[type=range]:focus::-ms-fill-lower { - background: #3071a9 -} - -input[type=range]:focus::-ms-fill-upper { - background: #367ebd -} - -input[type=range].inputConstraint { - display: block; - width: 100%; - margin: 5px 0px 10px 0px; -} -/* ////// Icon resizing for mobile subControl bar ////// */ - -input[type=range].thinSlider { - margin: 0; - padding: 0; - height: 10px; - top: 5px; - overflow: hidden; - position: relative; - display: flex; -} -.actionMessage { - color: #000; - margin: 3px; - border-radius: 3px; - background: #FFF; - padding: 5px; - text-align: left; - margin: 10px 3px; - border: 1px solid black; -} -.inMessage, .outMessage, .tipMessage { - margin-bottom: 10px; - padding: 5px; - border-radius: 5px; -} -.inMessage { - background-color: #e6e6e6; -} - -.outMessage { - background-color: #c5d3ff; - text-align: right; -} - -.tipMessage { - background: linear-gradient(135deg, #ffd700 0%, #ffb700 100%); - color: #1a1a1a; - text-align: center; - font-weight: 500; - border: 1px solid #e6a800; -} - -/* Tip banner - on-screen notification */ -.tipBanner { - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%) translateY(-100px); - background: linear-gradient(135deg, rgba(255, 215, 0, 0.95) 0%, rgba(255, 183, 0, 0.95) 100%); - color: #1a1a1a; - padding: 15px 30px; - border-radius: 10px; - font-size: 1.4em; - font-weight: 600; - text-align: center; - z-index: 99999; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - border: 2px solid #e6a800; - opacity: 0; - transition: transform 0.4s ease-out, opacity 0.4s ease-out; - pointer-events: none; -} - -.tipBanner.tipBannerShow { - transform: translateX(-50%) translateY(0); - opacity: 1; -} - -.tipBanner.tipBannerHide { - transform: translateX(-50%) translateY(-20px); - opacity: 0; -} - -.tipBannerMessage { - font-size: 0.75em; - font-weight: 400; - margin-top: 5px; - font-style: italic; -} - -#chatBody { - flex-grow: 1; - overflow-y: auto; - padding: 10px; - background-color: #4d4d4d; - min-height: 50px; - border-color: #6b6767; - height: 300px; -} - -#chatBody::-webkit-scrollbar { - width: 0px; - background: transparent; /* make scrollbar transparent */ -} -.chat-input-area { - display: flex; - padding: 10px 10px 4px 10px; - background-color: #2a2a2a; -} -.resizer { - height: 5px; - background: #2a2a2a; - cursor: ns-resize; - position: relative; - overflow: hidden; - cursor: ns-resize; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} - -.resizer::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: 30px; - height: 2px; - background: linear-gradient(to right, #666 20%, transparent 20%, transparent 40%, #666 40%, #666 60%, transparent 60%, transparent 80%, #666 80%); -} - -.resizer:hover::before { - background: linear-gradient(to right, #999 20%, transparent 20%, transparent 40%, #999 40%, #999 60%, transparent 60%, transparent 80%, #999 80%); -} -div#chatBody a { - color: blue; - text-decoration: underline; - background-color: #c9c9c9; - border: 2px solid black; - padding: 2px 10px; - border-radius: 6px; - cursor: pointer; -} - -#chatModule { - position: fixed; - bottom: 10px; - right: 10px; - width: 400px; - max-width: 100%; - min-width: 100px; - background-color: #fff; - border: 1px solid #4f4e4e; - border-radius: 5px; - display: flex; - flex-direction: column; - max-height: 100vh; - z-index: 1000; -} -.chat-header { - padding: 10px; - background-color: #2a2a2a; - display: flex; - justify-content: space-between; - align-items: center; - cursor: move; - color:white; -} - -#chatInput { - flex-grow: 1; - margin-right: 5px; -} -.chatBarInputButton { - width: 50px; - margin: 0 5px; -} -#callerMenu { - position: absolute; - left: 0; - bottom: 200px; -} -#callerMenu button { - font-size: 300%; -} -.foregroundMedia { - width: 100%; - height: 100%; - position: relative; -} -.section { - display: block; - padding: 20px 5px; - margin: 20px auto; - border-radius: 20px; - border: 2px solid #6e6e6e; - max-width: 680px; -} - -@media only screen and (max-width: 500px) { - #subControlButtons { - position: unset; - min-width: unset; - } - #subControlButtons > div { - min-width: 40px; - min-height: 40px; - margin: 4px; - } - #SubControlButtons > div i { - font-size: 28px; - } - #hangupbutton { - margin-left: 15px; - } -} - -@media only screen and (max-height: 500px) { - - #subControlButtons > div { - min-width: 40px; - min-height: 40px; - margin: 4px; - } - #SubControlButtons > div i { - font-size: 28px; - } - #chatModule{ - margin-bottom: 50px; - } -} - -@media only screen and (max-width: 410px) { - #subControlButtons > div { - min-width: 35px; - min-height: 35px; - border-radius: 6px; - margin: 3px; - } - #SubControlButtons > div i { - font-size: 26px; - } - #hangupbutton { - margin-left: 15px!important; - } -} - -@media only screen and (max-height: 410px) { - #subControlButtons > div { - min-width: 35px; - min-height: 35px; - border-radius: 6px; - margin: 3px; - } - #SubControlButtons > div i { - font-size: 26px; - } - #chatModule{ - margin-bottom: 40px; - } - -} - -@media only screen and (max-width: 360px) { - #subControlButtons > div { - min-width: 30px; - min-height: 30px; - border-radius: 4px; - margin: 3px; - } - #SubControlButtons > div i { - font-size: 20px; - } - #hangupbutton { - margin-left: unset; - } -} - -@media only screen and (max-height: 360px) { - #subControlButtons > div { - min-width: 30px; - min-height: 30px; - border-radius: 4px; - margin: 3px; - } - #SubControlButtons > div i { - font-size: 20px; - } - #chatModule { - margin-bottom: 37px; - } -} - -@media only screen and (max-width: 320px) { - #subControlButtons > div { - min-width: 28px; - min-height: 28px; - border-radius: 4px; - margin: 3px; - } - #SubControlButtons > div i { - font-size: 18px; - } -} - -@media only screen and (max-width: 280px) { - #subControlButtons > div { - min-width: 25px; - min-height: 25px; - border-radius: 4px; - margin: 3px; - } - #SubControlButtons > div i { - font-size: 16px; - } -} - -/* //////////////////////////////////// */ - -@media only screen and (max-height: 650px) { - body { - font-size: 0.9em; - } - .gowebcam { - padding: 5px; - margin: 5px; - } - .infoblob { - color: white; - width: 100%; - padding: 20px; - max-width: 1280px; - } - #qrcode img { - max-height: 150px; - } - .outer { - width: 50px; - } - .close { - top: 0px; - right: 0px; - } -} - -@media only screen and (max-width: 1220px) { - #fakeguest4{ - display: none!important; - } - #fakeguestinfo{ - display: none!important; - } -} -@media only screen and (max-width: 933px) { - #fakeguest3{ - display: none!important; - } -} -@media only screen and (max-width: 700px) { - #fakeguest2{ - display: none!important; - } - #chatModule { - margin-bottom: 5px !important; - } -} -@media only screen and (max-width: 292px) { - #fakeguest1{ - display: none!important; - } -} - -@media screen and (max-width: 768px) { - #popOutChat{ - display: none; - } -} - -@media only screen and (max-width: 650px) { - - .mainmenuclass { - display: inline-block; - } - .outer { - width: 50px; - } - .close { - top: 0; - right: 0; - } - select { - font-size: 120%; - } - #reshare { - max-width: 650px !important; - font-size: 96% !important; - width: 300px !important; - } - .fa-paperclip { - display: none; - } - #copythisurl { - color: #DDD; - display: inline-block; - font-size: 75% !important; - } - #logoname { - font-size: 100%; - } - .column { - float: none !important; - padding: 15px 10px 1px 10px !important; - } - div.multiselect, .videoMenu, #videoSettings { - max-width: 100% !important; - min-width: 100% !important; - } - #addPasswordBasic, #headphonesDiv, #effectsDiv, #effectsDiv3, #headphonesDiv3 { - max-width: 100% !important; - min-width: 100% !important; - overflow: hidden !important; - } - #outputSource { - width: 100% !important; - } - #outputSource3 { - width: 100% !important; - } - #audioSourceScreenshare, #videoSettings2 { - max-width: 90% !important; - min-width: 90% !important; - overflow: hidden !important; - } - .popupSelector_constraints{ - margin:25px 15% 0 1%; - } - .mobileHide{ - display:none !important; - } - #effectSelector { - max-width: 100%; - width: 100%; - margin: 4px 0 0 0; - } -} - -@media only screen and (max-height: 355px) { - - .popupSelector_constraints{ - margin:20px 12% 0 2%; - } -} -#popupSelector_user_settings{ - margin-top: 10px; -} - -.tooltip { - position: relative; - display: inline-block; - border-bottom: 1px dotted black; -} -.tooltip .tooltiptext { - visibility: hidden; - width: 10em; - background-color: #9d5050; - color: #fff; - text-align: center; - border-radius: 10px; - position: absolute; - z-index: 1; - top: -10px; - font-family: "Lato", sans-serif; -} -.tooltip:hover .tooltiptext { - visibility: visible; -} - -#previewWebcam.miconly { - display: none; -} - -.notmain > .mainonly { - display:none!important; -} - -#audioSourceScreenshare { - overflow: auto; - resize: both; - width: 100%; -} - -#outputSourceScreenshare { - width: 100%; -} -#outputSource { - margin-top: 7px; - padding:2px 0; - min-height: 24px; -} -/* #audioSourceScreenshare { - display: block; - height: 60px; - width: 100%; - overflow: auto; - padding: 5px; - resize: both; - border: solid 1px #AAA; - border-radius: 4px; -} */ - -/* #headphonesDiv2{ - min-width: 350px; - display: none; - padding: 4px 10px 10px 10px; - vertical-align: middle; - margin: 10px 0; - text-align: left; -} */ -/* #audioScreenShare1 > i { - display: inline-block; -} - -#audioScreenShare1 > span { - margin: 7px 0px; - text-align: left; - display: inline-block; -} */ - -h2 { - color: white; - -webkit-user-select: none; - /* Safari */ - -moz-user-select: none; - /* Firefox */ - -ms-user-select: none; - /* IE10+/Edge */ - user-select: none; - /* Standard */ -} - -.inner { - width: inherit; - text-align: center; -} - -.labelclass { - opacity: 0; - font-size: 1.1em; - line-height: 4em; - text-transform: uppercase; - transition: all .3s ease-in; - cursor: pointer; -} - -label { - color: #000; - -webkit-user-select: none; /* Safari */ - -ms-user-select: none; /* IE 10 and IE 11 */ - user-select: none; -} - -.darktheme .inner:before, -.darktheme .inner:after { - background: var(--discord-text); -} - -.inner:before, -.inner:after { - position: absolute; - content: ''; - height: 7px; - width: inherit; - background: #000; - left: 0; - font-weight: bold; - transition: all .3s ease-in; -} - -.inner:before { - top: 50%; - transform: rotate(45deg); -} - -.inner:after { - bottom: 50%; - transform: rotate(-45deg); -} - -.outer:hover .labelclass { - opacity: 1; -} - -.outer:hover .inner:before, -.outer:hover .inner:after { - transform: rotate(0); -} - -.outer:hover .inner:before { - top: 0; -} - -.outer:hover .inner:after { - bottom: 0; -} -.microphoneBackground{ - background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M 13 4 C 11.90625 4 11 4.90625 11 6 L 11 18 C 11 19.09375 11.90625 20 13 20 L 19 20 C 20.09375 20 21 19.09375 21 18 L 21 6 C 21 4.90625 20.09375 4 19 4 Z M 13 6 L 19 6 L 19 18 L 13 18 Z M 7 14 L 7 18 C 7 21.300781 9.699219 24 13 24 L 15 24 L 15 26 L 11 26 L 11 28 L 21 28 L 21 26 L 17 26 L 17 24 L 19 24 C 22.300781 24 25 21.300781 25 18 L 25 14 L 23 14 L 23 18 C 23 20.21875 21.21875 22 19 22 L 13 22 C 10.78125 22 9 20.21875 9 18 L 9 14 Z'/%3E%3C/svg%3E")!important; -} - -#dropButton{ - font-size: 2em; - display: block; - margin: auto; - background-color: #5555; - border-radius: 30px; - cursor: pointer; - color: #636363; - padding: 3px; - width: 100px; -} -.fullcolumn { - float: left; - display: inline-block; - margin: 0 auto; - width: 100%; - text-align: center; -} - -/* .card { - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, .1); - background-color: #ddd; -} */ - -.column { - display: inline-block; - margin: 1.8%; - min-width: 300px; - width: 20%; - padding: 25px; - height: 200px; - text-align: center; - font-size: 100%; - transition: box-shadow 0.2s ease-in-out, border-color 0.2s ease-in-out, transform 0.2s ease-in-out; - border-radius: 12px; - border: 1px solid rgba(0, 0, 0, 0.08); - position: relative; - transform: translateZ(0); - backface-visibility: hidden; -} - -.column:hover { - box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .15); - border-color: rgba(0, 0, 0, 0.1); -} - -.column:active { - box-shadow: 0 5px 10px 0 rgba(0, 0, 0, .2); -} - -.column>h2 { - color: black; -} - -.graphSection { - display: flex; - flex-direction: column; - max-width: 50%; - gap: 5px; -} -.darktheme .graphSection > span { - color: var(--discord-text) -} -.graphSection > span { - display: block; - font-size: 10px; - max-height: 50px; - min-height: 20px; -} -.graphSection>span:last-child{ - margin-bottom: 0px; -} -span[data-action-type="stats-graphs-details-container"]>span{ - padding: 1px; - display: block; -} - -#empty-container { - display: inline-block; - width: 20%; - min-width: 300px; - padding: 28px; - height: 200px; - margin: 1.8%; - text-align: center; -} -#container-1 { - background-repeat: no-repeat; - background-size: 80px; - background-position: 50% 65%; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='342.728' height='325.878' viewBox='0 0 90.68 86.222' fill='none' stroke='%23000' stroke-width='5.6' stroke-linejoin='round' stroke-dashoffset='22.7149601' xmlns:v='https://vecta.io/nano'%3E%3Cpath d='M3.15 3.15h37.378v35.24H3.15zm47.002 0H87.53v35.24H50.152zM3.15 47.832h37.378v35.24H3.15zm47.002 0H87.53v35.24H50.152z'/%3E%3C/svg%3E"); -} -#container-2 { - background-repeat: no-repeat; - background-size: 80px; - background-position: 50% 67%; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='347.569' height='278.797' viewBox='0 0 91.961 73.765' fill='none' stroke='%23000' xmlns:v='https://vecta.io/nano'%3E%3Cpath d='M3.02 3.02h85.921v54.399H3.02z' stroke-width='6.04' stroke-linejoin='round' stroke-dashoffset='22.7149601'/%3E%3Cg stroke-width='6.3'%3E%3Cpath d='M35.607 70.527l21.839.071' stroke-linecap='round' paint-order='markers fill stroke'/%3E%3Cpath d='M46.404 73.517l.142-15.596' paint-order='markers fill stroke'/%3E%3C/g%3E%3C/svg%3E"); -} -.mainScreenShareButton { - padding: 18px; - font-size: 120%; - margin: 16px; - animation: pulsate 2s ease-out infinite; - background-color: #26e726 !important; - background: radial-gradient(#26e726, #2EeF2E); - box-shadow: 1px 1px 1px; - color: black !important; - background-color: #26e726 !important; - background: radial-gradient(#26e726, #2EeF2E); - font-weight: bold !important; - box-shadow: 0 0 31px 0px #244e1c44; - animation: pulsate 2s ease-out infinite; -} -.screenshare-container { - max-width: 600px; - margin: 0 auto; - padding: 20px; - color: #fff; -} - -.screenshare-header { - text-align: center; - margin-bottom: 30px; -} - -.screenshare-icon { - width: 240px; - height: 160px; - margin: 0 auto 20px; - display: block; - box-shadow: 0 0 0 0 !important; -} - -.select-screen-btn { - background: #2a2a2a; - color: #fff; - border: 1px solid #666; - padding: 12px 24px; - border-radius: 6px; - font-size: 16px; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - width: 100%; - margin-bottom: 15px; - cursor: pointer; - transition: background 0.2s; -} - -.select-screen-btn:hover { - background: #3a3a3a; -} - -.quality-toggle { - background: transparent; - border: none; - color: #999; - cursor: pointer; - padding: 5px; - border-radius: 50%; - transition: background 0.2s; -} - -.quality-toggle:hover { - background: rgba(255,255,255,0.1); -} - -.audio-section { - margin-top: 20px; - padding: 15px; - background: rgba(0,0,0,0.2); - border-radius: 8px; -} - -.info-box { - margin-top: 20px; - padding: 15px; - background: rgba(255,255,255,0.05); - border-radius: 8px; - font-size: 14px; - display: inline-block; - text-align: left; -} - -.info-box h3 { - margin-top: 0; - color: #ccc; - font-size: 16px; -} - -.info-box ul { - margin: 10px 0; - padding-left: 20px; - color: #aaa; -} - -.info-box li { - margin: 8px 0; -} -.info-box li:empty { - display:none; -} -.audio-device-list { - margin-top: 15px; - padding: 10px; - background: rgba(0,0,0,0.2); - border-radius: 6px; -} - -.audio-device-item { - padding: 5px 0; - display: flex; - align-items: center; - gap: 10px; -} - -.audio-device-item:hover { - background: rgba(255,255,255,0.05); - border-radius: 4px; -} - -.audio-device-item label { - margin-left: 8px; - cursor: pointer; -} - -.audio-select-btn { - width: 100%; - padding: 8px; - margin: 10px 0; - background: none; - border: 1px solid currentColor; - border-radius: 4px; - cursor: pointer; -} - -.audio-select-btn:hover { - background: rgba(255, 255, 255, 0.1); -} - -.error-message { - margin-top: 10px; - padding: 10px; - background: rgba(255,0,0,0.1); - border: 1px solid rgba(255,0,0,0.2); - border-radius: 4px; - color: #ff9999; -} - -#container-3 { - background-repeat: no-repeat; - background-size: 90px; - transition: background-image 0.3s ease-in-out; - background-position: 50% 65%; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='347.184' height='187.007' viewBox='0 0 91.859 49.479' fill='none' stroke='%23000' stroke-width='5' stroke-linejoin='round' xmlns:v='https://vecta.io/nano'%3E%3Cpath d='M3.15 3.15h65.569v43.179H3.15z' stroke-dashoffset='22.7149601' paint-order='markers fill stroke'/%3E%3Cpath d='M68.919 28.837L88.709 44.73V7.148L69.019 22.341z'/%3E%3C/svg%3E"); -} -#container-3a { - background-repeat: no-repeat; - background-size: 90px; - transition: background-image 0.3s ease-in-out; - background-position: 50% 65%; -} -#container-4 { - background-repeat: no-repeat; - background-size: 80px; - background-position: 50% 65%; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='303.594' height='274.946' viewBox='0 0 80.326 72.746' fill='none' stroke='%23000' stroke-width='4.6' stroke-linejoin='round' stroke-dashoffset='6.01000023' xmlns:v='https://vecta.io/nano'%3E%3Cpath d='M2 51.27L78.326 2l-8.03 63.359-37.093-12.414z'/%3E%3Cpath d='M33.047 70.746l.157-17.802L78.326 2 33.203 52.944l10.314 3.39z'/%3E%3C/svg%3E"); -} -#container-5 { - background-repeat: no-repeat; - background-size: 80px; - background-position: 50% 65%; - background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMjkgMTI5IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMjkgMTI5IiB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiPgogIDxnPgogICAgPGc+CiAgICAgIDxwYXRoIGQ9Im0xOC43LDEyMi41aDkxLjZjMi4zLDAgNC4xLTEuOCA0LjEtNC4xdi0xMDcuOWMwLTIuMy0xLjgtNC4xLTQuMS00LjFoLTY4LjdjLTAuMywwLTAuNywwLTEsMC4xLTAuMSwwLTAuMiwwLjEtMC4yLDAuMS0wLjMsMC4xLTAuNSwwLjItMC44LDAuMy0wLjEsMC4xLTAuMiwwLjEtMC4zLDAuMi0wLjMsMC4yLTAuNiwwLjQtMC44LDAuN2wtMjIuOSwyN2MtMC4zLDAuMy0wLjUsMC43LTAuNywxLjEtMC4xLDAuMS0wLjEsMC4zLTAuMSwwLjQtMC4xLDAuMy0wLjEsMC42LTAuMiwwLjkgMCwwLjEgMCwwLjEgMCwwLjJ2ODAuOWMtMS4wNjU4MWUtMTQsMi40IDEuOSw0LjIgNC4xLDQuMnptMTguOC0xMDAuOHYxMS44aC0xMGwxMC0xMS44em0tMTQuNywxOS45aDE4LjhjMi4zLDAgNC4xLTEuOCA0LjEtNC4xdi0yMi45aDYwLjV2OTkuN2gtODMuNHYtNzIuN3oiIGZpbGw9IiMwMDAwMDAiLz4KICAgICAgPHBhdGggZD0ibTk0LDUwLjVoLTU5Yy0yLjMsMC00LjEsMS44LTQuMSw0LjEgMCwyLjMgMS44LDQuMSA0LjEsNC4xaDU5YzIuMywwIDQuMS0xLjggNC4xLTQuMSAwLTIuMy0xLjgtNC4xLTQuMS00LjF6IiBmaWxsPSIjMDAwMDAwIi8+CiAgICAgIDxwYXRoIGQ9Im05NCw3MC4zaC01OWMtMi4zLDAtNC4xLDEuOC00LjEsNC4xIDAsMi4zIDEuOCw0LjEgNC4xLDQuMWg1OWMyLjMsMCA0LjEtMS44IDQuMS00LjEgMC0yLjItMS44LTQuMS00LjEtNC4xeiIgZmlsbD0iIzAwMDAwMCIvPgogICAgPC9nPgogIDwvZz4KPC9zdmc+Cg==) -} -#container-5 input[type="file"] { - width: 0.1px; - height: 0.1px; - opacity: 0; - position: absolute; - z-index: -1; -} - -#container-5 input[type="file"] + label { - display: inline-block; - padding: 12px 24px; - background: var(--accent-color, #4a90e2); - color: white; - border-radius: 4px; - cursor: pointer; - margin: 1rem 0; - font-weight: 500; - transition: background 0.2s ease; -} - -#container-5 input[type="file"] + label:hover { - background: var(--accent-hover-color, #357abd); -} - - input[type="file"]#fileselector4 + label:hover { - background: var(--accent-hover-color, #357abd); -} -input[type="file"]#fileselector4 { - width: 0.1px; - height: 0.1px; - opacity: 0; - position: absolute; - z-index: -1; -} - -input[type="file"]#fileselector4 + label { - display: inline-block; - padding: 12px 24px; - background: var(--accent-color, #4a90e2); - color: white; - border-radius: 4px; - cursor: pointer; - margin: 1rem 0; - font-weight: 500; - transition: background 0.2s ease; -} - - -.file-manager { - max-width: 400px; - background: #ddda; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - padding: 16px; - margin: 10px; - transition: all 0.3s ease; -} - -.file-manager.minimized { - max-width: 300px; - padding: 8px; - min-height: 40px; -} - -.file-manager.minimized .file-list, -.file-manager.minimized .file-upload-zone { - display: none; -} - -#activeShares .file-list { - max-height: 400px; - overflow-y: auto; -} - -#activeShares .header-controls { - display: flex; - gap: 8px; -} - -#activeShares .file-manager-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - padding-bottom: 8px; - border-bottom: 1px solid #eee; - gap: 12px; -} - -#activeShares .file-upload-zone { - border: 2px dashed #ccc; - border-radius: 4px; - padding: 20px; - text-align: center; - margin-bottom: 16px; - cursor: pointer; - transition: border-color 0.3s; -} - -#activeShares .file-upload-zone:hover { - border-color: #666; -} - -#activeShares .file-list { - max-height: 400px; - overflow-y: auto; -} - -#activeShares .file-item { - display: grid; - grid-template-columns: 1fr auto auto; - gap: 12px; - align-items: center; - padding: 12px; - border-bottom: 1px solid #eee; -} - -#activeShares .file-info { - display: flex; - flex-direction: column; - gap: 4px; -} - -#activeShares .file-name { - font-weight: 500; -} - -#activeShares .file-size { - color: #666; - font-size: 0.9em; -} - -#activeShares .download-info { - font-size: 0.85em; - color: #666; - margin-top: 4px; -} - -#activeShares .file-size { - color: #666; - font-size: 0.9em; -} - -#activeShares .file-progress { - position: relative; - height: 4px; - background: #eee; - border-radius: 2px; - overflow: hidden; - margin: 4px 0; -} - -#activeShares .progress-bar { - position: absolute; - left: 0; - top: 0; - height: 100%; - background: #4CAF50; - transition: width 0.3s; -} - -#activeShares .progress-text { - font-size: 0.9em; - color: #666; -} - -#activeShares .transfer-info { - display: flex; - flex-direction: column; - gap: 4px; -} - -#activeShares .no-files { - text-align: center; - color: #666; - padding: 20px; -} - -#activeShares .transfer-item { - background: #f5f5f5; - padding: 8px; - border-radius: 4px; - margin-top: 4px; -} - -#activeShares .transfer-speed { - font-size: 0.9em; - color: #666; -} - -#activeShares .button-primary { - background: #4CAF50; - color: white; -} - -#activeShares .button { - padding: 6px 12px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.9em; - transition: background 0.3s; -} - -#activeShares .button-primary { - background: #4CAF50; - color: white; -} - -#activeShares .button-secondary { - background: #666; - color: white; -} - -#activeShares .button-danger { - background: #f44336; - color: white; -} - -#activeShares .button:hover { - opacity: 0.9; -} - -.controlVideoBox { - position:relative; -} - -.canvasStats{ - background-color: black; - width: 100%; - height: 50px; - border-radius: 4px; -} -.manualInput{ - width: 75px; - display: inline-block; - overflow: scroll; - margin: 4px 0 4px 4px; - padding: 1px 4px; - color:black; -} - -#add_screen { - padding-bottom: 20px; -} -.soloButton{ - display: flex; - flex-wrap: wrap; - font-size: 0.7em; -} -.soloButton button { - margin: 5px 0px 0px 0px; - box-shadow: 1px 1px 3px rgba(0,0,0,0.75); - background-color: #ecfaff; - border-radius: 2px; - width: 100%; -} - -.lowerRaisedHand{ - margin: auto; - margin-top: 5px; - margin-left: 5px; - margin-bottom: 5px; - background-color: yellow!important; - width: 100%; - color:black!important; -} - -.removeFromQueue{ - margin: auto; - margin-top: 5px; - margin-left: 5px; - margin-bottom: 5px; - background-color: #ff00b8!important; - width: 100% -} - -.float { - opacity: 0.8; - min-width: 45px; - min-height: 45px; - height: 100%; - box-shadow: 1px 1px 3px rgba(0,0,0,0.75); - color: #FFF; - border-radius: 8px; - text-align: center; - margin: 5px; - pointer-events: auto; - outline:none; - padding: 5px 5px; -} -.float2 { - opacity: 0.8; - min-width: 45px; - min-height: 45px; - height: 100%; - background-color: #8888; - box-shadow: 1px 1px 3px rgba(0,0,0,0.75); - color: #FFF; - border-radius: 38px; - text-align: center; - z-index: 10; - margin: 5px; - pointer-events: auto; - outline:none; - padding: 5px 5px; -} - -.rotate225 { - transform: rotate(135deg); - position: relative; - top: 1px; -} - -.controlVideoBox video[data-rotated='90']{ - transform: rotate(90deg) translate(9px,-80px) !important; - height: 260px; - width:80px; -} -.controlVideoBox video[data-rotated='180']{ - transform: rotate(180deg) !important; -} -.controlVideoBox video[data-rotated='270']{ - transform: rotate(270deg) translate(-9px,80px) !important; - height: 260px; - width:80px; -} - -#previewWebcam.rotate{ - max-width: 30vh; -} -#previewWebcam.rotate{ - max-width: 30vh; -} - -.myVideo { - box-shadow: rgb(88, 88, 88) 0px 0px 5px 1px; - width: var(--myvideo-width); - max-width: 800px !important; - max-height: 100% !important; - height: var(--myvideo-height) !important; - display: block !important; - margin: auto auto !important; - position: relative !important; - top: 50% !important; - background-color: var(--myvideo-background); - object-fit: var(--fit-style); - max-width: var(--myvideo-max-width) !important; -} -#calendarButton { - cursor: pointer; - z-index: 1; - display: none; -} -#calendar a { - display: block; - margin:10px; -} -#translateButton { - cursor: pointer; - z-index: 1; -} -#helpButton { - cursor: pointer; - z-index: 1; -} -.controlsGrid button[data-action-type="solo-video"]>i{ - color: #b3b300; -} -iframe { - z-index: 2; -} - -#mutebutton.bigbutton { - bottom: 100px; - padding: 100px; - position: fixed; - display: block; - box-sizing: unset; -} -.bigbutton #mutetoggle { - bottom: 20px; - right: 0; - top: unset; -} -.bigbutton .bigbuttontext { - border-radius: 0; - margin: 3px; - display: block; - font-size:200%; -} - -@media only screen and (max-height: 600px) { - .bigbutton { - padding: 80px; - } -} -@media only screen and (max-width: 600px) { - .bigbutton { - padding: 80px; - } -} -@media only screen and (max-height: 500px) { - .bigbutton { - bottom: 80px; - padding: 70px; - } -} -@media only screen and (max-width: 500px) { - .bigbutton { - padding: 70px; - } -} -@media only screen and (max-height: 400px) { - .bigbutton { - padding: 60px; - bottom: 50px; - } -} -@media only screen and (max-width: 300px) { - .bigbutton { - padding: 60px; - } -} -@media only screen and (max-height: 300px) { - .bigbutton { - bottom: 40px; - padding: 50px; - } -} - -@media only screen and (max-width: 390px) { - #translateButton { - display: none; - } - #helpButton { - display: none; - } -} - -.popup .menu { margin: 2px; } - -.toggleSize { - font-size: 32px; - color: white; -} - -img { - max-width: 100%; -} - -/* In-animation, out-animation, and skip-animation are used for the card/buttons on the home page */ -.in-animation { - animation: inlightbox 0.5s forwards; /* @keyframes found in animations.css */ - position: fixed !important; - margin: 0 !important; - z-index: 20; - border-radius: 0 !important; -} -.out-animation { - animation: outlightbox .5s forwards; /* the @keyframes for this animation are dynamically created in lib.js */ - z-index: 20; -} - -.skip-animation { - position: fixed !important; - margin: 0 !important; - height: 100%; - width: 100%; - top: 0; - left: 0; - z-index: 20; - border-radius: 0 !important; -} -.skip-animation .container-inner{ - display: block; -} - - -.pointer { - cursor: pointer; -} -.modal { - display: none; - position: fixed; - padding-top: 50px; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgb(0, 0, 0); - background-color: rgba(0, 0, 0, 0.5); -} -.modal-content { - position: relative; - background-color: white; - padding: 20px; - margin: auto; - max-width: 400px; -} -.close-btn { - float: right; - color: lightgray; - font-size: 24px; - font-weight: bold; -} -.close-btn:hover { - color: darkgray; -} -#chattoggle{ - top: 0.5px; - position: relative; -} - -.la-phone { - color: red; - top:0.5; -} - -#obsState { - border:#888 solid 2px; - padding:2px 5px; - color: white; - z-index:2; - margin: 0 auto; - background-color: #222D; - display: inline-block; - opacity: 0.8; - border-radius: 4px; - text-align: center; - - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - transform-origin: top center; - -} -#obsState.larger { - padding:2px 10px; - font-size: 30px; -} - -@media only screen and (max-height: 400px){ - #obsState { - transform: translateX(-50%) scale(0.8); - } -} -@media only screen and (max-height: 300px){ - #obsState { - transform: translateX(-50%) scale(0.7); - } -} -@media only screen and (max-width: 620px){ - #obsState { - top:20px; - transform: translateX(-50%) scale(0.7); - } -} -@media only screen and (max-height: 200px){ - #obsState { - transform: translateX(-50%) scale(0.6); - } -} - -@media only screen and (max-width: 400px){ - #obsState { - top:30px; - transform: translateX(-50%) scale(0.6); - display:none!important; - opacity:0; - } -} -@media only screen and (max-width: 300px){ - #obsState { - display:none!important; - opacity:0; - } -} - -#obsState.noheader{ - top:0px; -} -.onair { - box-shadow: inset 0 0 max(10vw, 10vh) green; -} - -.ondeck { - display: block !important; - box-shadow: inset 0 0 max(10vw, 10vh) yellow; - color: black!important; -} - -.recording{ - box-shadow: inset 0 0 max(10vw, 10vh) red -} - -.raisedHand{ - background-color: #DD1A !important; -} -#request_info_prompt{ - z-index: 20; - color: white; - font-size: 30px; - font-size: 3.5vw; - top: 0; - align-self: center; - margin: 25vh 0; - position: absolute; -} - -.holder { - position: relative; - width: 100%; - height: 100%; - max-width: 100%; - max-height: 100%; - object-fit: contain; - overflow:hidden; - display: flex; - align-items: center; - justify-content: center; - margin: var(--video-margin); - border-radius: var(--video-rounded); - border-width: var(--video-border); - border-color: var(--video-border-color); - background-color: var(--video-holder-color); - border-style: solid; -} - -video { - transition: opacity .25s ease-in-out; - -moz-transition: opacity .25s ease-in-out; - -webkit-transition: opacity .25s ease-in-out; - pointer-events: auto; - background-color: transparent; - border: 0; - margin: 0; - user-select:none; - -webkit-user-select: none; /* Chrome/Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+ */ - -webkit-tap-highlight-color:transparent; - outline-style:none; - border-style:solid; - background-size: var(--video-background-image-size); - background-repeat: no-repeat; - background-position: center; - background-image: var(--video-background-image); - -} -video[data-speaking="0"] { - transition: opacity .25s ease-in-out, background-size 0.5s ease; -} -video[data-speaking="1"] { - background-image: var(--video-background-image-talking, var(--video-background-image)); - background-size: var(--video-background-image-size-talking, var(--video-background-image-size)); - transition: background-size 0.5s ease; -} -video[data-speaking="2"] { - background-image: var(--video-background-image-screaming, var(--video-background-image)); - background-size: var(--video-background-image-size-screaming, var(--video-background-image-size)); - transition: background-size 0.5s ease; -} - -.nogb { background-image: unset !important } - -video::-webkit-media-controls-timeline { - display: none; -} - -video::-webkit-media-controls-fullscreen-button { - display: var(--full-screen-button); -} - -video::-webkit-media-controls-timeline-container { - display: none; -} - -audio::-webkit-media-controls-overlay-play-button, video::-webkit-media-controls-overlay-play-button { - display: none; -} - -audio::-webkit-media-controls-play-button, video::-webkit-media-controls-play-button { - display: none; -} - -video::-webkit-media-controls-toggle-closed-captions-button { - display: none; -} - -video.clean::-webkit-media-controls-current-time-display { - display: inherit; -} - -video.clean::-webkit-media-controls-time-remaining-display { - display: inherit; -} - -video.clean::-webkit-media-controls-timeline { - display: inherit; -} - -video.clean::-webkit-media-controls-timeline-container { - display: inherit; -} - -audio.fileshare::-webkit-media-controls-overlay-play-button, video.fileshare::-webkit-media-controls-overlay-play-button { - display: inherit; -} - -audio.fileshare::-webkit-media-controls-play-button, video.fileshare::-webkit-media-controls-play-button { - display: inherit; -} - -video::-webkit-media-controls-panel { - background-color: #0000; -} - -#main.forcecontrols video::-webkit-media-controls-panel { - opacity: 1 !important; - visibility: visible !important; - display: flex !important; -} - -.mirrorControl::-webkit-media-controls-enclosure { - padding: 0px; - height: 30px; - transform: scaleX(-1); - -webkit-transform: scaleX(-1); -} -.popup-screen { - text-align: center; - position: absolute; - display: none; - top:0; - left: 0; - z-index: 7 !important; - padding: 20px; - margin:15px 15px 80px 15px; - width: 80vh !important; - height: 80vh !important; - background-color: #ccc !important; - border: solid 1px #dfdfdf !important; - box-shadow: 1px 1px 2px #cfcfcf !important; -} -.context-menu { - display: none; - position: absolute; - z-index: 996 !important; - padding: 12px 0 !important; - width: 240px !important; - background-color: #fff !important; - border: solid 1px #dfdfdf !important; - box-shadow: 1px 1px 2px #cfcfcf !important; -} - -.darktheme .popup-message { - background-color: var(--discord-grey-1); - border-color: var(--discord-grey-3); - color: var(--discord-text); - box-shadow: 1px 1px 3px black; -} - -.popup-message { - display: none; - text-align: center; - position: absolute; - z-index: 35; - padding: 5px; - border-radius: 4px; - min-width: 180px; - background-color: #fff; - border: solid 1px #dfdfdf; - box-shadow: 1px 1px 2px #cfcfcf; -} - -.darktheme .popup-message ul { - list-style: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAALCAYAAACtWacbAAAAr0lEQVQYlY3QMUoDUBAE0KciNtpI+hQ5gdhZ5QIewwN4EisJpM0RhJRiZSt4A8EynWlCig0j+wttdODzYXZndhj/QlX9frOqeq6q96paZ3aMNW7b8AorTPCBRcijKJrI4gsuscXNSBKnDaZ4xQX2OGtBnL+X7lp5jhNc9x/HR8xybmR5aKeBXQs3cQreMO/BKb46U7JOxlJCP7Uyp++bX+JzdJQ+0kv6SU8/uvsbOACH0VkbmsdQwQAAAABJRU5ErkJggg==); -} - -.popup-message ul { - list-style: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAA/ElEQVQYlTWPwUrEMBCGJ2mUNKZxIbRQ2oPeRT34OH2HPfQ5eugzeRG8rOLdvRVagm5Jm+DSILO0c5kZZub/vyF1XcMWTdNgda2UeqSUSmvt736/P7C2bZ9DCN8hhBMAxEqpJ601H4bhJ4RwxCsqpdwJIe6wieP4Icsy3nWdt9Z+rIdAlFIvWmuJAynlVRRFsCwLTNPknXNfAODoOI6fxhhfliUnhNCqql4xF0Vxg9bISFbmGK2EEJwxdlGy1p7zPOfGGEvX75xz7n2apnOSJBFaee/fEB6/ZBd6Sm8RPk1T3vf9xgLzPB8ppWRbumeM7VAaGQHgb3U4AcDhH7U6eiFtegoRAAAAAElFTkSuQmCC'); - margin-left: 14px; - padding: 5px; -} - -.popup-message li { - text-align: left; - padding-left: unset; - line-height: unset; - margin: 0 0 0 18px; -} - -.context-menu--active { - display: block !important; -} -.context-menu__items { - list-style: none !important; - margin: 0; - padding: 0; -} -.context-menu__item { - display: block; - margin-bottom: 4px !important; -} -.context-menu__item:last-child { - margin-bottom: 0 !important; -} -.context-menu__link { - display: block; - padding: 4px 12px; - color: #0066aa !important; - text-decoration: none; -} -.context-menu__link:hover { - color: #fff !important; - background-color: #0066aa !important; -} -.context-menu__tip { - margin-left: 15px; - color: #777; - margin-top: 10px; - padding-top: 10px; - position: relative; - top: 7px; -} -#bufferSliderValue{ - margin-left:10px; -} -.context-menu__link:hover > #bufferSliderValue { - color: #fff; -} - -/* Submenu support for context menu */ -.context-menu__item--has-submenu { - position: relative; -} -.context-menu__item--has-submenu > .context-menu__link::after { - content: '\276F'; - float: right; - margin-left: 10px; - font-size: 0.8em; -} -.context-menu__submenu { - display: none; - position: absolute; - left: 100%; - top: 0; - z-index: 997 !important; - padding: 12px 0 !important; - width: 200px !important; - background-color: #fff !important; - border: solid 1px #dfdfdf !important; - box-shadow: 1px 1px 2px #cfcfcf !important; - list-style: none !important; - margin: 0; -} -.context-menu__item--has-submenu:hover > .context-menu__submenu { - display: block; -} -.context-menu__submenu--left { - left: auto; - right: 100%; -} - -/* Dark theme for context menu and submenus */ -.darktheme .context-menu { - background-color: var(--discord-grey-1) !important; - border-color: var(--discord-grey-3) !important; - box-shadow: 1px 1px 3px black !important; -} -.darktheme .context-menu__link { - color: var(--discord-text) !important; -} -.darktheme .context-menu__link:hover { - background-color: var(--discord-grey-3) !important; -} -.darktheme .context-menu__submenu { - background-color: var(--discord-grey-1) !important; - border-color: var(--discord-grey-3) !important; - box-shadow: 1px 1px 3px black !important; -} - -.selectedContentEffectsImage{ - box-shadow: 0 0 10px #2c3554; - outline: 2px solid black; -} - -.multiselect .multiselect-trigger:hover { - cursor: pointer; - cursor: hand; - text-decoration: none; -} -/* .multiselect .multiselect-trigger.open { - border-bottom: 0; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} -.multiselect .multiselect-trigger.closed { - border-bottom: 1px solid #ccc; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; -} */ - -span[data-resolution]:hover, span[data-bitrate]:hover { - color:lightblue; - text-decoration: underline; -} -.gear_microphone{ - user-select: none; - float: right; - height: 0; - padding: 0; - top: 6px; - position: relative; -} -.gear_microphone.gearflat{ - top: -1px; -} -.gear_microphone>input{ - top: 1px; - position: relative; -} -#micStereoMonoInput3{ - width: 10px; - height: 11px; -} -#headphonesDiv3 { - text-align: left; - margin: 17px 0 0 0; - width: 463px; - padding: 10px 10px; - vertical-align: middle; -} -#headphonesDiv { - width: 463px; - padding: 4px; -} -.selected { - border: solid 3px black; - padding: 4px; -} -#audioMenu { - margin: 15px 0 0 0; -} - -#minipreview > #videosource { - height:auto!important; - width:auto!important; - max-height:100%!important; - max-width:100%!important; - border-radius: 0!important; -} - -#videoSource3 { - width: calc(100% - 50px); -} - -#videoSourceSelect { - padding:2px 0; - min-height: 24px; -} -#gear_webcam{ - cursor: pointer; - display: inline-block; - padding: 0 0 0 3px; -} -.gone { - position:absolute; - top: -150px; -} -.grabLinks { - display: inline-flex; - cursor: grab; - font-weight: bold; - font-size: 1em; - padding: 10px; - margin: 5px 0; - word-break: break-all; -} -.grabLinks a:hover { - color: black !important; -} -.grabLinks a:active { - color: black !important; -} -.grabLinks a:link { - color: black !important; -} - -.hidden { - display: none !important; - visibility: hidden; - width: 0px; - height: 0px; - opacity: 0; -} -.hidden2 { - display: none !important; - visibility: hidden; - width: 0px; - height: 0px; - opacity: 0; -} - -.grabLinks a:visited { - color: black !important; -} - -.permahide { - display: none!important; - visibility: hidden; - width:0px; - height:0px; - opacity: 0; - background: #0000; - color: #0000; - font-size: 0em; - pointer-events:none; -} - -.multiselect .multiselect-contents { - display: block; - margin: 0; - font-size: 95%; - padding: 2px 3px 0px; - border-top: 0; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - text-align: left; -} - -.multiselect .multiselect-contents li { - list-style: none; - overflow: hidden; -} -.select .select-trigger:hover { - cursor: pointer; - cursor: hand; - text-decoration: none; -} -.select .select-trigger.open { - border-bottom: 0; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} -.select .select-trigger.closed { - border-bottom: 1px solid #ccc; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; -} -.select .select-contents { - display: none; - margin: 0; - padding: 0 24px 24px; - border: 1px solid #ccc; - border-top: 0; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; -} -.select .select-contents li { - list-style: none; -} -::-webkit-input-placeholder { - color: #555 !important; -} -::-moz-placeholder { - color: #555 !important; -} -:-ms-input-placeholder { - color: #555 !important; -} -:-moz-placeholder { - color: #555 !important; -} -label { - font-weight: 400; -} - -#screenshare { - height: 300px; - display: inline-block; - max-height: 50vh; - max-width: 50vh; - border: 0; - margin: 0; - margin-bottom:15px; - padding: 0; - text-shadow: unset; - box-shadow: unset; - text-decoration: none; - border-image-width: 0; - background-size: contain; - background-color: rgba(0, 0, 0, 0); -} - -.debugStats { - font-size: 0.8rem; - list-style-type: none; - left: 50px; - top: 0px; - width: 300px; - min-height: 200px; - max-height: 99vh; - overflow-y: auto; - background-color: rgba(0, 0, 0, 0.95); - position: absolute; - z-index: 20; - color: white; - padding: 20px; - border: 2px solid #1d1d1d; - padding-bottom: 100px!important; - margin-bottom: 100px!important; -} -.debugStats::-webkit-scrollbar { - width: 0.5em; -} -.debugStats::-webkit-scrollbar-track { - background: black; - border-radius: 10px; -} - -.debugStats::-webkit-scrollbar-thumb { - background: rgb(119, 119, 119); - border-radius: 10px; -} - -.debugStats::-webkit-scrollbar-thumb:hover { - background: rgb(158, 158, 158); - ; -} -.debugStats h1 { - font-size: 1rem; - text-align: left; - text-transform: uppercase; - margin-bottom: 10px; - margin-top: -5px; -} -.debugStats h2 { - font-size: 0.8rem; - text-align: left; - text-transform: uppercase; - margin-top: 10px; - white-space: nowrap; - text-overflow: ellipsis; - display: block; - overflow: hidden; -} -.viewstats::-webkit-scrollbar-track { - box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); -} -.debugStats li { - display: flex; - margin: 5px 0px; -} -.debugStats li:nth-child(even) { - background: rgba(33, 33, 33, 0.8); - padding: 2px 0px; -} -.debugStats li span:first-child { - flex: 1; - white-space: nowrap; -} -.debugStats li span:last-child { - flex: 1; - text-align: right; - max-height: 49px; - overflow: auto; -} -.debugStats .close { - font-weight: bold; - color: white; - display: block; - background: none; - padding: 0; - margin: 0; - font-size: 1.5rem; - border: none; - top: 10px; - right: 10px; -} -.debugStats button:not(.close) { - margin: 10px 0px; - padding: 10px 0px; - background: #263250; - color: white; - border-radius: 0; - width: 100%; - font-weight: bold; - border-bottom: 2px solid #364c84; -} - -.directorMargins { - margin: var(--director-margin); -} - -.hideLinksClass { - background-color: var(--container-color); - width:1191px; - padding: 10px; - margin: 5px 10px 10px 10px; - max-width:100%; -} -.directorContainer { - background-color: var(--container-color); - margin: 10px 0px 10px 10px; - padding: 10px; - max-width: min(100%, 1191px); -} - -#directorLinksButton{ - cursor: pointer; -} -.directorContainer.half { - background-color: var(--container-color); - padding: 10px 10px; - width: min(100%, 591px); -} -.directorBlock { - padding: 10px 10px 5px 10px; - margin: var(--regular-margin); - color: white; - position:relative; - max-width: 100%; - overflow: hidden; - display: block; - min-height: 174px; -} -.directorBlock:nth-child(1) { - background-color: var(--blue-accent); -} -.directorBlock:nth-child(2) { - background-color: var(--green-accent); -} -.directorBlock:nth-child(3) { - background-color: var(--olive-accent); -} -.directorBlock:nth-child(4) { - background-color: var(--red-accent); -} -.directorBlock button { - margin: 10px; - box-shadow: unset; - border:0; - border-radius:0; - font-size: 1.1em; - padding: 6px 9px 4px 9px; - background-color: #2a2a2a; - color:white; - max-width:25%; -} -.directorBlock button i { - margin-right: 5px; -} -a.task { - width: 100%; -} - -.directorBlock h2 { - text-transform: uppercase; - margin-bottom: 10px; - margin-left: 5px; - font-size:1.2em; -} -.shift { - display: inline-block; - position: relative; - margin: 0 0 0 4px; - padding: 0; - min-width: 25px; - font-size: 0.8em; - top: -4.9px; - color: white; -} -.shift>i { - cursor: pointer; - width: 10px; - margin: 0 auto; - left: -1.1px; - position: relative; -} -.shift.locked>i{ - display: none; -} -.shift.locked>span{ - margin-left: 7px; -} -div#roomnotes2 { - background: var(--container-color); - padding: 10px !important; - margin: 0 var(--regular-margin) 10px var(--regular-margin); - width: 100%; -} - -#yourDirectorStatus, #yourDirectorStatusSS { - color: var(--discord-text); -} -.directorBlue{ - background-color: #5c7785 !important; - display: var(--show-codirectors) !important; - -} -.directorBox{ - background-color: #606383 !important; - display: var(--show-codirectors) !important; -} - -/* ---- DIRECTORS PAGE - Guest Controls Box ---- */ -.controlsGrid { - display: flex; - flex-wrap: wrap; - gap: 5px; -} - -.controlsGrid .group { - width: 100%; - display: flex; - flex-direction: column; - gap: 5px; -} - -.controlsGrid .row { - width: 100%; - display: flex; - flex-wrap: wrap; - gap: 5px; -} -.controlsGrid .row > .row { - margin: 0; -} -.controlsGrid .row.two > * { - flex: 1 33%; -} -.controlsGrid .row.three > * { - flex: 1 25%; -} -.controlsGrid .row.four > * { - flex: 1 20%; -} -.controlsGrid .row.six > * { - flex: 1 13%; -} - -.controlsGrid .row.eight > * { - flex: 1 10%; -} - - -.controlsGrid button { - display: flex; - align-items: center; - justify-content: center; - margin: 0; - padding: 4px; - border-radius: 2px; - gap: 2px; - box-shadow: 1px 1px 3px rgba(0,0,0,0.25); - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - text-transform: lowercase; -} - -.controlsGrid button span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.controlsGrid button i { - font-size: 15px; -} - -.controlsGrid button[data-action-type="mute-guest"] { - min-width: calc(33.3% - 5px); - box-shadow: inset 0px 0px 1px 0px #ff0000b5, 1px 1px 3px rgba(0,0,0,0.25); -} -/* Button icons: Red */ -.controlsGrid button[data-action-type="mute-scene"] i, -.controlsGrid button[data-action-type="mute-guest"] i, -.controlsGrid button[data-action-type="hangup"] i, -.controlsGrid button[data-action-type="remote-global-record"] i, -.controlsGrid button[data-action-type="local-global-record"] i, -.controlsGrid button[data-action-type="recorder-local"] i, -.controlsGrid button[data-action-type="recorder-google-drive-remote"] i, -.controlsGrid button[data-action-type="recorder-remote"] i { - color: #e30000; -} -/* Button icons: Green */ -.controlsGrid button[data-action-type="addToScene"] i, -.controlsGrid button[data-action-type="solo-chat"] i { - color: #03a303; -} -/* Button icons: Blue */ -.controlsGrid button[data-action-type=""] i { - color: #00f; -} - -/* Specitic CSS for different elements inside the guest control-buttons */ -.darktheme .controlsGrid .director-message-box { - background-color: var(--discord-grey-3); - border: 1px solid var(--discord-grey-8); -} - -.darktheme .controlsGrid .director-message-box button { - background-color: var(--discord-grey-6); -} - -.controlsGrid .director-message-box { - display: flex; - flex-wrap: wrap; - gap: 5px; - flex: 1 100% !important; - padding: 5px; - background: rgba(0, 0, 0, .15); - border-radius: 4px; - max-width: 100%; -} - -.darktheme .controlsGrid .director-message-box textarea { - background-color: var(--discord-grey-6); - border: 1px solid var(--discord-grey-7); - color: var(--discord-text); -} - -.controlsGrid .director-message-box textarea { - flex: 1 100%; - padding: 5px; - border-radius: 4px; - outline: none; -} - -.controlsGrid .director-message-box .message-close { - flex: 1; -} -.controlsGrid .director-message-box .message-send { - flex: 1 33%; -} - -.controlsGrid .tooltip { - flex: 1 calc(50% - 10px); - display: flex; - align-items: center; -} -.controlsGrid .tooltip input[type=range] { - margin: 0; -} -.controlsGrid .tooltip .tooltiptext { - height: 18px; - line-height: 1.2; - top: 3px !important; - left: 100% !important; - background-color: #e6a0a0; - border: 1px solid rgba(0,0,0,1); - border-radius: 4px; - font-size: 12px; -} - -.controlsGrid .hideDropMenu{ - justify-content: left; - width: 100%; - box-shadow:unset; - background-color: #0000; - border:0; - color: #fcfcfc; -} - -.darktheme .controlsGrid .orderspan { - color: var(--discord-text); -} - -.controlsGrid .orderspan { - font-size: 50%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - text-align: center; - position: relative; - user-select: none; - - color: #fcfcfc; -} -.controlsGrid .orderspan .order-label { - width: max-content; -} - -/* Hightlights for buttons in the guest control-buttons */ -/* Dark theme */ -.darktheme .controlsGrid .btn-HL-yellow { - background-color: var(--darktheme-yellow); -} -.darktheme .controlsGrid .btn-HL-peach { - background-color: var(--darktheme-brown); -} -.darktheme .controlsGrid .btn-HL-green { - background-color: var(--darktheme-green); -} -.darktheme .controlsGrid .btn-HL-blue { - background-color: var(--darktheme-blue); -} -.darktheme .controlsGrid .btn-HL-paleblue { - background-color: rgb(41 54 72); -} -.darktheme .controlsGrid .btn-HL-orange { - background-color: rgb(185, 104, 42); -} -.darktheme .controlsGrid .btn-HL-purple { - background-color: rgb(111, 72, 139); -} -.darktheme .controlsGrid .btn-HL-red { - background-color: rgb(159, 62, 56); -} -.darktheme .controlsGrid .btn-HL-teal { - background-color: rgb(54, 140, 140); -} -/* Light theme */ -.controlsGrid .btn-HL-yellow { - background-color: rgb(255, 229, 127); -} -.controlsGrid .btn-HL-peach { - background-color: rgb(228, 203, 189); -} -.controlsGrid .btn-HL-green { - background-color: rgb(189, 228, 199); -} -.controlsGrid .btn-HL-blue { - background-color: rgb(170, 204, 248); -} -.controlsGrid .btn-HL-paleblue { - background-color: rgb(170, 204, 248); -} -.controlsGrid .btn-HL-orange { - background-color: rgb(255, 187, 115); -} -.controlsGrid .btn-HL-purple { - background-color: rgb(208, 186, 233); -} -.controlsGrid .btn-HL-red { - background-color: rgb(255, 153, 153); -} -.controlsGrid .btn-HL-teal { - background-color: rgb(131, 215, 215); -} - -/* Hides buttons that are supposed to be hidden when &novice is added to URL */ -.controlsGrid .advanced.hide { - display: none; -} - -.appmode #head1 , .appmode #head1a { - display:flex; -} - -.appmode #head1 input, .appmode #head1a input{ - width: 100%; - padding: 10px; - margin: 5px; - font-size: 125%; -} -#widget { - position: absolute; - width: var(--widget-width); - max-width: 75%!important; - height: 100%; - right: 0; - top: 0; -} -#widget.left{ - right: unset!important; - left: 0!important; -} -#directorlayout.widget { - position: absolute; - width: 75%; - left: 0; -} -#directorlayout.widget.left { - left: unset!important; - right: 0!important; -} -#localMuteElement{ - top: 1vh; - right: 1vh; -} -#localVoiceMeter{ - width: 10px; - height: 10px; - top: 8px; - right: 10px; -} -.controlCenterBox{ - display: flex; - flex-direction: column; - gap: 2.5px; - padding: 5px; -} - -.darktheme .controlCenterBox .flexBreak { - border-bottom: 1px double var(--discord-grey-7); -} - -.controlCenterBox .flexBreak { - width: 100%; - border-bottom: 1px #ccc double; - margin: 0; - position: relative; - user-select: none; - filter: blur(2px); -} - -.darktheme .controlCenterBox .flexBreak span { - text-shadow: 0px 0px 1px var(--discord-text); - background-color: var(--discord-grey-3); - color: var(--discord-text); -} -#hangupContainer { - font-size: 500%; - text-align: center; - margin: auto auto; - display: flex; - height: 100%; - width: 100%; - vertical-align: middle; - flex-wrap: wrap; - align-content: center; - flex-direction: column; - justify-content: center; - align-items: stretch; -} -.controlCenterBox .flexBreak span { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-transform: uppercase; - font-size: 10px; - text-shadow: 0px 0px 1px var(--discord-text); - color: var(--discord-text); - background-color: #7E7E7E; - padding: 0px 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.contolboxLabel { - float: left; - top: 2px; - margin-left: 5px; - position: relative; - cursor: pointer; -} -.fullwindowButton { - transition: opacity 0.3s; - width: 4vh; - height: 4vh; - max-width: 30px; - max-height: 30px; - min-width: 15px; - min-height: 15px; - position: absolute; - display: none; - z-index: 6; - right: 4vh; - top : 4vh; - color: white; - cursor: pointer; -} -.fullwindowButtonimg { - user-select: none; - background-color: #0007; - width: 4vh; -} - - -.pull-right { - float: right; - right: 0; -} -.pull-left { - float: left; - left: 0; -} -.streamID { - text-align: right; - margin: 5px 5px 2px 0px; - font-size: 0.7em; - text-overflow: ellipsis; - overflow: hidden; - position: relative; - width: 227px; - display: inline-block; - color: white; -} -.streamID i { - margin-left: 1px; - font-size: 1.3em; - position: relative; - top: 1px; -} - -.vidcon>h2 { - font-size: 1em; - margin-top: 20px; -} -#pptbackbutton { - margin-left: 20; -} -#pptnextbutton { - background-color: #007900; -} -#pptbackbutton:active { - -webkit-box-shadow: inset 0px 0px 17px #4b4b4b; - -moz-box-shadow: inset 0px 0px 17px #4b4b4b; - box-shadow: inset 0px 0px 17px #4b4b4b; - outline: none; -} -#pptnextbutton:active { - -webkit-box-shadow: inset 0px 0px 17px #4b4b4b; - -moz-box-shadow: inset 0px 0px 17px #4b4b4b; - box-shadow: inset 0px 0px 17px #4b4b4b; - outline: none; -} - -.darktheme div#guestFeeds { - background-color: var(--container-color) -} - -div#guestFeeds { - background-color: var(--container-color); - padding: 5px 0 15px 20px; - display: inline-block; - margin: 0px var(--regular-margin) 80px var(--regular-margin); -} - -div#guestFeeds:empty { - display: none; -} -#hiddenElements{ - visibility:hidden; - left:-9999; - top:-9999; - width:0px; - height:0px; - display: block; -} -#reshare, #inviteLinkURL { - font-weight: bold; - color: #afa !important; - cursor: grab; - background-color: #0000; - font-size: 115%; - min-width: 335px; - max-width: 800px -} -a#reshare , a#inviteLinkURL{ - white-space: nowrap; - margin: 0; - padding: 0; - display: inline; -} -#copythisurl+i, #copythisurl2+i { - display: inline; - font-size: 130%; -} -#joinroomID { - border-radius: 0; - padding: 5px; -} -#joinroomID+button { - margin: 0px var(--regular-margin); -} - -#joinbyURL { - border-radius: 0; - padding: 5px; -} -#joinbyURL+button { - margin: 0px var(--regular-margin); -} - -.appmode #joinroomID+button { - width: 110px; -} - -.appmode #joinbyURL+button { - width: 110px; -} - +:root { + /* Discord Greys - Dark to Lighter */ + --discord-grey-0: #121212; + --discord-grey-1: #1e1f22; + --discord-grey-2: #232428; + --discord-grey-3: #2c2c2d; + --discord-grey-4: #2e3035; + --discord-grey-5: #313338; + --discord-grey-6: #383a40; + --discord-grey-7: #404249; /* primary */ + --discord-grey-8: #5e6064; + + --discord-text: hsl( 210 calc(1 * 9.1%) 92% /1); + + --darktheme-red: rgb(161, 45, 45); + --darktheme-blue: rgb(33, 69, 114); + --darktheme-green: rgb(36, 88, 49); + --darktheme-lightgreen: #008770; + --darktheme-brown: rgb(76 58 41); + --darktheme-yellow: rgb(84, 70, 9); + + /* Lightmode white - Darker to lighter */ + --lighttheme-1: #fff; + --lighttheme-2: #f3f3f3; + --lighttheme-3: #ddd; + --lighttheme-4: #ccc; /* primary */ + --lighttheme-5: #bbb; + --lighttheme-6: #aaa; + --lighttheme-7: #7e7e7e; + --lighttheme-8: #373737; + --lighttheme-text: black; + + /* Director v2 - General colors */ + /* -- Links */ + --a-dark-link: #69aadc; + --a-dark-visited: #69aadc; + --a-dark-hover: #6da5dd; + --a-dark-focus: #6da5dd; + --a-dark-active: #3a80c6; + + --a-darker-link: #b9dff9; + --a-darker-visited: #b9dff9; + --a-darker-hover: #048ae8; + --a-darker-focus: #d9e4eb; + --a-darker-active: #d9e4eb; + + --a-lighter-link: #9ed0e1; + --a-lighter-visited: #9ed0e1; + --a-lighter-hover: #8acee4; + --a-lighter-focus: #8acee4; + --a-lighter-active: #89d5ee; + + --a-link: #144267; + --a-visited: #144267; + --a-hover: #38668c; + --a-focus: #38668c; + --a-active: #0165b5; + + /* -- Box colors */ + --director-box: rgb(165 119 18); + --codirector-box: rgb(67 122 213); + + --director-dark-box: rgb(165 119 18); + --codirector-dark-box: rgb(129 127 127); + + --widget-width: 25%; + --rtl-or-ltr: left; + + /* Original colors */ + --background-color: #141926; + --dark-background-color: #02050c; + --container-color: #373737; + --button-color: #2A2A2A; + --blue-accent: #4a4c63; + --red-accent: #553737; + --light-grey: #ddd; + --near-black: #02050c; + --green-accent: #3f4f50; + --olive-accent: #535D32; + --regular-margin: 10px; + --director-margin: 15px 20px 0 0; + --fit-style: contain; + --fadein-speed: 0; + --video-margin: 0px; + --video-rounded: 0px; + --video-border: 0px; + --video-border-color: #0000; + --video-holder-color: #0000; + --video-rounded: 0px; + --button-radius: 2px; + --myvideo-max-width: min(800px,100vw); + --myvideo-width:unset; + --myvideo-height:auto; + --myvideo-background: #FFF1; + --video-background-image: url("data:image/svg+xml,%3Csvg viewBox='-42 0 512 512.002' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m210.351562 246.632812c33.882813 0 63.222657-12.152343 87.195313-36.128906 23.972656-23.972656 36.125-53.304687 36.125-87.191406 0-33.875-12.152344-63.210938-36.128906-87.191406-23.976563-23.96875-53.3125-36.121094-87.191407-36.121094-33.886718 0-63.21875 12.152344-87.191406 36.125s-36.128906 53.308594-36.128906 87.1875c0 33.886719 12.15625 63.222656 36.132812 87.195312 23.976563 23.96875 53.3125 36.125 87.1875 36.125zm0 0'/%3E%3Cpath d='m426.128906 393.703125c-.691406-9.976563-2.089844-20.859375-4.148437-32.351563-2.078125-11.578124-4.753907-22.523437-7.957031-32.527343-3.308594-10.339844-7.808594-20.550781-13.371094-30.335938-5.773438-10.15625-12.554688-19-20.164063-26.277343-7.957031-7.613282-17.699219-13.734376-28.964843-18.199219-11.226563-4.441407-23.667969-6.691407-36.976563-6.691407-5.226563 0-10.28125 2.144532-20.042969 8.5-6.007812 3.917969-13.035156 8.449219-20.878906 13.460938-6.707031 4.273438-15.792969 8.277344-27.015625 11.902344-10.949219 3.542968-22.066406 5.339844-33.039063 5.339844-10.972656 0-22.085937-1.796876-33.046874-5.339844-11.210938-3.621094-20.296876-7.625-26.996094-11.898438-7.769532-4.964844-14.800782-9.496094-20.898438-13.46875-9.75-6.355468-14.808594-8.5-20.035156-8.5-13.3125 0-25.75 2.253906-36.972656 6.699219-11.257813 4.457031-21.003906 10.578125-28.96875 18.199219-7.605469 7.28125-14.390625 16.121094-20.15625 26.273437-5.558594 9.785157-10.058594 19.992188-13.371094 30.339844-3.199219 10.003906-5.875 20.945313-7.953125 32.523437-2.058594 11.476563-3.457031 22.363282-4.148437 32.363282-.679688 9.796875-1.023438 19.964844-1.023438 30.234375 0 26.726562 8.496094 48.363281 25.25 64.320312 16.546875 15.746094 38.441406 23.734375 65.066406 23.734375h246.53125c26.625 0 48.511719-7.984375 65.0625-23.734375 16.757813-15.945312 25.253906-37.585937 25.253906-64.324219-.003906-10.316406-.351562-20.492187-1.035156-30.242187zm0 0'/%3E%3C/svg%3E"); + --background-main-image: unset; + --show-codirectors: inline-block; + --full-screen-button: inherit; + --color-mode: light; + --video-background-image-size: auto 30%; +} + +/* Changes color-mode based on what theme the browser states */ +@media (prefers-color-scheme: dark) { + :root { + --color-mode: dark; + } +} + + +* { + padding: 0; + margin: 0; + box-sizing: border-box; + border: 0; +} + +::selection { + background-color: #0447c888; + color: #FFF; +} + +button:hover,[role="button"] +:not(.column) +:not(.controlsGrid) +:not(#controlButtons) +:hover{ + filter: brightness(98%); +} + +table { + display: inline-block; + padding:10px; + margin:10px; +} + +.drawingCanvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.buttonContainer { + position: absolute; + bottom: 0; + left: 0; + margin: 5px; +} +.buttonContainer button { + margin: 5px 2px; +} + +.promptModalLabel{ + cursor: pointer; + font-weight: normal; + font-size: 1.0em; + display: block; + margin: 17px 20px 15px 20px; +} + +#bigPlayButton { + margin: 0 auto; + background-color: #0000; + cursor: pointer; + font-family: Cousine, monospace; + font-size: 4em; + line-height: 1.5em; + letter-spacing: 0.0em; + text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1); + width: 100%; + height: 100vh; + z-index: 1; + vertical-align: top; + text-align: center; + top: 0; + position: fixed; + overflow-wrap: anywhere; + padding:3%; +} + +.playButton { + border-radius: 50vh; + width: min(30vw, 30vh); + cursor: pointer; + opacity: 100%; + background-color: #bbb; + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 122.88 122.88' style='enable-background:new 0 0 122.88 122.88' xml:space='preserve'%3E%3Cstyle type='text/css'%3E.st0%7Bfill-rule:evenodd;clip-rule:evenodd;%7D%3C/style%3E%3Cg%3E%3Cpath class='st0' d='M61.44,0c33.93,0,61.44,27.51,61.44,61.44s-27.51,61.44-61.44,61.44S0,95.37,0,61.44S27.51,0,61.44,0L61.44,0z M83.31,65.24c3.13-2.02,3.12-4.27,0-6.06L50.98,40.6c-2.55-1.6-5.21-0.66-5.14,2.67l0.1,37.55c0.22,3.61,2.28,4.6,5.32,2.93 L83.31,65.24L83.31,65.24z M61.44,12.48c27.04,0,48.96,21.92,48.96,48.96c0,27.04-21.92,48.96-48.96,48.96S12.48,88.48,12.48,61.44 C12.48,34.4,34.4,12.48,61.44,12.48L61.44,12.48z'/%3E%3C/g%3E%3C/svg%3E"); + display: inline-block; + height: min(30vw, 30vh); + background-repeat: no-repeat; + border: #bbb 3vh solid; + position: absolute; +} + +#bigPlayButton>.playButton { + width: min(50vw, 50vh); + margin-top: 10vh; + background-color: #646262; + height: min(50vw, 50vh); + border: #646262 7vh solid; + position: unset; + position: static; +} + +#progressContainer { + width: 100%; + background-color: #ddd9; + position: absolute; + right: 0; + bottom: 0; +} +#progressBar { + width: 0%; + height: 18px; + background-color: #4CAF5099; + text-align: left; + color: black; +} + +select#audioSource { + max-height: 80px; /* Initial height */ + overflow-y: auto; /* Enable scrolling */ + transition: max-height 0.3s ease; /* Smooth transition for expanding and collapsing */ + width: 100%; + margin-top: 7px; + padding: 3px 4px; + min-height: 24px; + user-select: none; +} +select#audioSource.expanded { + max-height: none; +} +select#audioSource.expanded option { + display: block; +} +select#audioSource option:checked { + display: block; + background-color: #1967D2!important; + color: white; +} +select#audioSource option { + display: none; +} +select#audioSource option:hover { + background-color: #8dbdd4!important; + color: black; +} +select#audioSource[size='1'] option { + background-color: var(--lighttheme-1)!important; + color:black; +} +.darktheme select#audioSource[size='1'] option { + background-color: var(--light-grey)!important; + color:black; +} +.paused { + cursor: pointer; +} + +tr { + padding:4px; +} +th { + padding:4px; +} + +.popupSelector_constraints .preSelectbutton { + display: inline-block; + margin: 4px 0 4px 3px; + padding: 2px 8px 1px 8px; +} +.advancedVideoSettings .preSelectButton { + display: inline-block; + margin: 4px 0 4px 3px; + padding: 2px 8px 1px 8px; +} +.meter { + display: inline-block; + width: 0px; + height: 10px; + background: green; + transition: all 100ms linear; +} +.meter2 { + display: inline-block; + width: 0px; + height: 10px; + background: yellow; + transition: all 50ms linear; +} +.meter3 { + display: inline-block; + width: 0px; + height: 10px; + background: red; + transition: all 25ms linear; +} +.meter4 { + display: inline-block; + width: 2px; + height: 10px; + background: black; + position:relative; + float:left; +} +#mynetwork { + width: 600px; + height: 400px; + border: 1px solid lightgray; +} + +.rtl { + direction: rtl; + text-align: right; +} +.rtl input, +.rtl textarea { + text-align: right; +} +[dir="rtl"] .keep-ltr { + direction: ltr; + text-align: left; +} +[dir="rtl"] .mirror-rtl { + transform: scaleX(-1); +} + +.email { + unicode-bidi: bidi-override; + direction: rtl!important; + user-select: none; +} + +a:link { + color: var(--a-link); +} +a:visited { + color: var(--a-visited); +} +a:hover { + color: var(--a-hover); +} +a:focus { + color: var(--a-focus); +} +a:active { + color: var(--a-active); +} + +a.soloLink:link { + cursor: grab; + font-size: 1.2em; + font-weight: 700; + padding: 4px 0 2px 0; + border-radius: 5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #b4d7f6; +} +a.soloLink:visited{ + color: #b4d7f6; +} + +/* Links */ +a { + text-decoration: none; +} +.darktheme a:link { + color: var(--a-dark-link); +} +.darktheme a:visited { + color: var(--a-dark-visited); +} +.darktheme a:hover { + color: var(--a-dark-hover); +} +.darktheme a:focus { + color: var(--a-dark-focus); +} +.darktheme a:active { + color: var(--a-dark-active); +} + +.directorContainer a:link { + color: var(--a-lighter-link); +} +.directorContainer a:visited { + color: var(--a-lighter-visited); +} +.directorContainer a:hover { + color: var(--a-lighter-hover); +} +.directorContainer a:focus { + color: var(--a-lighter-focus); +} +.directorContainer a:active { + color: var(--a-lighter-active); +} + +.infoblob a:link { + color: var(--a-lighter-link); +} +.infoblob a:visited { + color: var(--a-lighter-visited); +} +.infoblob a:hover { + color: var(--a-lighter-hover); +} +.infoblob a:focus { + color: var(--a-lighter-focus); +} +.infoblob a:active { + color: var(--a-lighter-active); +} + +.darktheme .infoblob a:link { + color: var(--a-darker-link); +} +.darktheme .infoblob a:visited { + color: var(--a-darker-visited); +} +.darktheme .infoblob a:hover { + color: var(--a-darker-hover); +} +.darktheme .infoblob a:focus { + color: var(--a-darker-focus); +} +.darktheme .infoblob a:active { + color: var(--a-darker-active); +} + +/* White theme styling */ +body.whitetheme { + background-color: #ffffff; + color: #000000; +} +.whitetheme #head4 { + color: black; +} +.whitetheme #reshare { + color: #292 !important; +} +.whitetheme #miniPerformer button { + color: #FFF; +} +.whitetheme .infoblob{ + color: #000000; +} +.whitetheme div#guestFeeds, .whitetheme #roomHeader .hideLinksClass{ + background-color: #ababab; +} + +.whitetheme .container-inner { + background-color: #f8f8f8; +} + +.whitetheme button { + background-color: #f0f0f0; + border: 1px solid #ddd; + color: #000000; +} + +.whitetheme button:hover { + background-color: #e8e8e8; +} + +.whitetheme .directorContainer { + background-color: #f5f5f5; +} + +.whitetheme .card { + background-color: #f5f5f5; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.whitetheme #addPasswordBasic, +.whitetheme #avatarDiv, +.whitetheme #avatarDiv2, +.whitetheme #avatarDiv3, +.whitetheme #audioScreenShare1, +.whitetheme #audioMenu, +.whitetheme #audioMenu2, +.whitetheme #effectsDiv, +.whitetheme #effectsDiv2, +.whitetheme #effectsDiv3, +.whitetheme #grabDirectorSoloLinkParent, +.whitetheme #videoMenu, +.whitetheme #videoMenu2, +.whitetheme #videoMenu3, +.whitetheme #headphonesDiv, +.whitetheme #headphonesDiv2, +.whitetheme #headphonesDiv3, +.whitetheme #videoSettings, +.whitetheme #videoSettings2, +.whitetheme #popupSelector_user_settings, +.whitetheme .invite_setting_group { + background-color: #ffffff; + border: 1px solid #e0e0e0; +} + +.whitetheme select { + background-color: #ffffff; + border: 1px solid #ddd; + color: #000000; +} + +.whitetheme input[type='text'], +.whitetheme input[type='password'] { + background-color: #ffffff; + border: 1px solid #ddd; + color: #000000; +} + +.whitetheme .directorsgrid .vidcon { + background-color: #f8f8f8; + border: 1px solid #e0e0e0; +} + +.whitetheme #promptModal, +.whitetheme .customModelPopup, +.whitetheme .promptModal { + background-color: #ffffff; + color: #000000; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.15); +} + +.whitetheme .cameraTip { + background-color: #e8f4ff; + border-left: 4px solid #50cef1; + color: #000000; +} + +.whitetheme #header { + color: #000000; +} + +.whitetheme .modalBackdrop { + background-color: rgba(255, 255, 255, 0.9); +} + + +input { + border-radius: 4px; + padding: 2px; +} + +button.grey { + padding: 10px; + margin: 10px 0px; + cursor: pointer; + border-radius: 2px; + background-color: var(--button-color); + color: white; +} + +button.hint { + -webkit-box-shadow: inset 0px 0px 25px #0004; + -moz-box-shadow: inset 0px 0px 25px #0004; + box-shadow: inset 0px 0px 25px #0004; +} + +#miniPerformer > video, #miniPerformer > canvas{ + width: 80px; + height: 45px; + margin: 5px; + background-color: #464749 !important; + background-size: 50%; +} + +#popOutChat{ + cursor: pointer; + text-align:right; + color:#B3C7F9; + margin: 0 5px; + cursor: pointer; + padding:3px; + background-color: black; + border-radius: 50%; + border: solid #B3C7F9 1px; +} + +#closeChat { + cursor: pointer; + text-align: right; + color: #B3C7F9; + margin: 0 5px; + cursor: pointer; + padding: 3px 8px; + background-color: black; + border-radius: 50%; + border: solid #B3C7F9 1px; +} + +/* Clicked buttons overwrite */ +.red { + background-color: #840000 !important; +} +.red:hover { + background-color: #b30c0c !important; +} + +.green { + background-color: #64c04d !important; +} + +.green:hover { + background-color: #76c762 !important; +} + +.blue { + background-color: #161699 !important; +} + +.blue:hover { + background-color: #2727bb !important; +} + +.brown { + background-color: #8d6418 !important; +} + +.brown:hover { + background-color: #a06d10 !important; +} + +/* ///////////////////// */ + +.orange { + background-color: #673100 !important; +} + +#meshcastMenu{ + display: inline-block; + color: #e0dfdf; +} +#header { + width: 100%; + padding: 1px; + color: #FFF; + background-color: #0005; +} +#head1{ + display: inline-block; + padding:1px; + position: relative; +} +#head4{ + font-size: 68%; + color: white; +} +#head9 { + color: #b5e4ff; + font-weight: 500; + font-size: 120%; +} +#head5 { + display: inline-block; + text-decoration: none; + color: white; + text-align: right; + margin-right: 10px; + cursor: help; + float: right; + font-size: 90%; + line-height: 100%; + margin-top: 2px; +} +#head6 { + display: inline-block; + text-decoration: none; + color: white; + text-align: left; + margin-left: 10px; + pointer-events: none; + font-weight: 700; +} + +#head7 { + display: inline-block; + text-decoration: none; + color: white; + text-align: left; + margin-left: 10px; + pointer-events: none; + font-weight: 700; +} +#overlayClockContainer{ + margin: 0 auto; + background-color: #0000; + color: white; + font-family: Cousine, monospace; + font-size: calc(6vh + 6vw / 2); + letter-spacing: 0.0em; + text-shadow: 0.05em 0.05em 0px rgb(0 0 0); + z-index: 6; + vertical-align: top; + text-align: right; + right:0; + bottom: 0; + position: fixed; + overflow-wrap: anywhere; + cursor: pointer; + user-select: none; +} +#overlayClockContainer.top { + top:0%; + bottom:unset; +} +#overlayClockContainer.vmiddle { + bottom: 48%; + top:unset; +} +#overlayClockContainer.bottom { + bottom: 0%; + top:unset; +} +#overlayClockContainer.left { + right:unset; + left: 0; +} +#overlayClockContainer.hmiddle { + right:45%; + left:unset; +} +#overlayClockContainer.right { + right:0; + left:unset; +} + +#overlayClock{ + padding:2px 20px; + background-color: #0009; +} +#overlayClock video { + width: calc(22vh + 22vw / 2); + max-width: 100%; + max-height: 25%; +} +#overlayClock:empty{ + display: none; +} +#overlayClockContainer2{ + margin: 0 auto; + background-color: #0000; + color: white; + font-family: Cousine, monospace; + font-size: calc(3vh + 3vw / 2); + letter-spacing: 0.0em; + text-shadow: 0.05em 0.05em 0px rgb(0 0 0); + z-index: 6; + vertical-align: top; + text-align: right; + position: fixed; + right:0; + bottom: 0; + overflow-wrap: anywhere; + cursor: pointer; + user-select: none; +} +#overlayClockContainer2.top { + top:0%; + bottom:unset; +} +#overlayClockContainer2.vmiddle { + bottom: 48%; + top:unset; +} +#overlayClockContainer2.bottom { + bottom: 0%; + top:unset; +} +#overlayClockContainer2.left { + right:unset; + left: 0; +} +#overlayClockContainer2.hmiddle { + right:45%; + left:unset; +} +#overlayClockContainer2.right { + right:0; + left:unset; +} + +#overlayClock2{ + padding:0 5px; + background-color: #0009; +} +#overlayClock2:empty{ + display: none; +} +#stickyMsgs{ + margin: 0 auto; + background-color: #0000; + color: white; + font-family: Cousine, monospace; + font-size: 6vh; + line-height: 8vh; + letter-spacing: 0.0em; + text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1); + width: 100%; + z-index: 7; + vertical-align: top; + text-align: center; + position: fixed; + overflow-wrap: anywhere; + padding:2% 3%; +} +.avatarSelection{ + vertical-align: top; + margin: 10px 0; + width: 130px; + display: inline-block; + margin: 0 1px; + text-align: center; + cursor: pointer; +} +.overlayCloseBtn{ + padding: 0; + width: 16px; + height: 16px; + position: relative; + bottom: 7px; + padding: 18px 18px 20px 18px; + font-size: 22px; + margin-left: 20px; +} + +#overlayMsgs{ + margin: 0 auto; + background-color: #0000; + color: white; + font-family: Cousine, monospace; + font-size: 6vh; + line-height: 8vh; + letter-spacing: 0.0em; + text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1); + width: 100%; + height: 100vh; + z-index: 7; + vertical-align: top; + text-align: center; + position: fixed; + overflow-wrap: anywhere; + padding:2% 3%; + pointer-events: none +} +#overlayMsgs span{ + background-color: #000B; + padding: 2px; + margin: 0.5vh; + text-align: center; + width: 100%; + pointer-events: none +} + +#credits { + position: fixed; + bottom: 2px; + right: 0; + font-size: 80%; + margin-right:31px; +} + + +#legal { + padding-right: 6px; + bottom: 3px; + position: relative; +} +@media only screen and (max-width: 1023px){ + #credits { + position: static; + right: auto; + bottom: auto; + margin: 24px 0 16px; + padding: 0 16px; + text-align: center; + width: 100%; + } + #legal { + display:none; + } +} + +.footer { + margin-top: 10px; + color: #101020; + font-size: 80%; +} + +.footer>a { + color: #101020; +} + +.footer>a:visited { + color: #101020; +} + +body.darktheme .footer { + color: #707a93; +} + +body.darktheme .footer>a { + color: #707a93; +} + +body.darktheme .footer>a:hover { + color: #769ade; +} + +body.darktheme .footer>a:visited { + color: #707a93; +} + +.label { + float: left; + font-size: 1.2em; + color: white; + display: inline-block; + position: absolute; + bottom: 0; + align-self: center; + z-index: 1000; + margin: 5% 20%; + padding: 1%; + background-color: black; +} + +.advancedAudioSettings, .advancedVideoSettings { + display: flex; + max-height: 300px; + overflow-y: auto; + width: 100%; + font-size: 14px; +} + +.advancedAudioSettings div { + display: flex; + width: 100%; + align-items: center; + gap: 4px; +} +.advancedAudioSettings div button { + padding: 4px; + height: 24px; + margin: unset; + flex: 1; +} + +.advancedAudioSettings div select { + width: 100%; + border-radius: 4px; + flex: 2; + height: 24px; + box-shadow: 1px 1px 3px rgba(0,0,0,0.75); + font-size: 14px; + padding: 0; +} + +.advancedAudioSettings div select[data-chosen='false'], .advancedVideoSettings div select[data-chosen='false'] { + border: 1px solid red; +} +.advancedAudioSettings div select[data-chosen='true'], .advancedVideoSettings div select[data-chosen='true'] { + border: 1px solid green; +} + +.advancedAudioSettings > div:nth-child(1) { + flex-direction: column; + align-items: flex-start; + width: 100%; + padding: unset; +} + +.darktheme .advancedAudioSettings .settingsLabel, .darktheme .advancedVideoSettings .settingsLabel { + color: var(--lighttheme-3); +} + +.advancedAudioSettings .settingsLabel, .advancedVideoSettings .settingsLabel { + display: block; + color: #fff; + font-family: system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,Oxygen,Ubuntu,Cantarell,open sans,helvetica neue,sans-serif; + font-size: 12px; + margin-top: 15px; + border-top: 3px solid #4f4c4c; + width: calc(100% - 4px); + padding: 5px 0; +} + +.advancedVideoSettings div:nth-child(2) { + display: flex; + width: 100%; + align-items: center; + gap: 4px; +} + +.advancedVideoSettings div:nth-child(2) select { + width: 100%; + flex: 2; + height: 24px; + border-radius: 4px; + box-shadow: 1px 1px 3px rgba(0,0,0,0.75); + font-size: 14px; +} + +.advancedVideoSettings div:nth-child(2) button { + width: 100%; + flex: 1; + height: 24px; + margin: unset; +} +.advancedAudioSettings label, .advancedVideoSettings label { + color: #000; +} +.darktheme .advancedAudioSettings label, .darktheme .advancedVideoSettings label { + color: #FFF; +} + +.pressed.altpress, .altpress { + background: #673100 !important; + -webkit-box-shadow: inset 0px 0px 1px #b90000; + -moz-box-shadow: inset 0px 0px 1px #b90000; + box-shadow: inset 0px 0px 1px #b90000; + outline: none; + color: white; +} +.pressed.armed, .armed { + background: #BF3F3F !important; +} +#mainmenu.row { + text-align: center; + margin-top: 20px; + padding: 0 10px; +} +#mainmenu.row:after { + content: ""; + display: table; + clear: both; +} + +hr { + height: 2px; + border-width: 0; + color: gray; + background-color: gray; +} + +.vidcon { + max-width: 100%; + border: 0; +} + +.tile { + object-fit: var(--fit-style); + width: 100%; + height: 100%; + overflow: hidden; + border-radius: var(--video-rounded); +} + +#gridlayout, #directorlayout { + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; + justify-items: stretch; + border: 0; + margin: 0; +} +#gridlayout{ + z-index:-1; +} +.directorsgrid { + justify-items: normal; + display: block ! important; + overflow-y: auto !important; +} + +.directorsgrid .vidcon video { + margin: 0 5px; + padding:0; + width: 100%; + max-height: 148px; + height:unset; + max-width: 260px; + min-height: 80px; +} + +.directorsgrid .vidcon { + display: inline-block; + width: 269.7px!important; + background: var(--lighttheme-7); + color: var(--lighttheme-text); + vertical-align: top; +} + +.directorsgrid .vidcon>.las { + color: black; + background: #999999; + width: 90%; +} +#activeShares>div{ + font-weight: normal; + font-size: 12px; + margin: 10px 0 0 0; +} + +.minimized { + float: left; + position: absolute; + bottom: 0; + height: 24px; + overflow:hidden; + box-shadow: inset -1px 1px white +} + +.minimized:nth-child(1) { + left:0px; + z-index:10; +} +.minimized:nth-child(2) { + left:max(100px, calc(10% - 260px)); + z-index:9; +} +.minimized:nth-child(3) { + left:max(200px, calc(20% - 260px)); + z-index:8; +} +.minimized:nth-child(4) { + left:max(250px, calc(30% - 260px)); + z-index:7; +} +.minimized:nth-child(5) { + left:max(300px, calc(40% - 260px)); + z-index:6; +} +.minimized:nth-child(6) { + left:max(463px, calc(50% - 260px)); + z-index:5; +} +.minimized:nth-child(7) { + left:max(500px, calc(60% - 260px)); + z-index:4; +} +.minimized:nth-child(8) { + left:max(650px, calc(70% - 260px)); + z-index:3; +} +.minimized:nth-child(9) { + left:max(700px, calc(80% - 260px)); + z-index:2; +} +.minimized:nth-child(10) { + left:max(850px, calc(90% - 260px)); + z-index:1; +} +.minimized:nth-child(11) { + left:max(900px, calc(100% - 260px)); + z-index:0; +} +.battery { + border: 3px solid #4192c5; + width: 11px; + height: 19px; + border-radius: 4px; + position: absolute; + left: 27px; + top: 3px; + background-color: #FFF2; + font-size: 1.5em; + z-index: 2; + cursor: help; + display: block; +} + +.battery-charging{ + margin: 0; + left: -1px; + padding: 0; + position: absolute; + font-size: 0.54em; + display: none; +} + +.battery[data-plugged="1"] { + display: none; +} + +.battery.warn { + border: 3px solid #EFAF13; + animation: blink-warn 2s infinite; +} +.battery.alert { + border: 3px solid #e81309; + animation: blink-alert 1s infinite; +} +.battery-level { + background: #30b455; + position: absolute; + bottom: 0px; + right: 0; + left: 0; + font-size: 0.7em; + margin: 0; + padding: 0; +} + +.hasMedia > .battery { + display: block; +} +.slotsbar { + border-radius: 6px; + margin: 3.8px 3.8px 0 3.8px; + padding: 0 6px; + box-shadow: 0 0 1px #111; + cursor:grab; + color: white; + text-shadow: 0 0 1px black; + text-align: center; +} +.slotsbar>button{ + margin: 0; + padding: 0 10px; +} +.slotsbar:active { + cursor:grabbing; +} +[data-slot='0'] { + background: linear-gradient(145deg, #dadada, #b8b8b8); +} +#slotpicker{ + box-shadow: 0 0 1px #908080; + width: 180px; + margin: 3px; + background-color: white; + text-shadow: 0px 0px 1px white; + vertical-align: middle; + align-content: center; + text-align: center; + position: absolute; + z-index: 5; + border-radius: 3px; + border: 1px solid black; + outline: 6px solid #444; +} +#slotPicker [data-slot]{ + margin: 2px; + width: 50px; + height: 34px; + border-radius: 3px; + display: inline-block; + cursor: pointer; + font-size:12px; +} +#alertModalMessage [data-slot]{ + margin: 2px; + cursor: pointer; + text-shadow: 0px 0px 10px white; +} +#alertModalMessage{ + display: block; + max-height: 70vh; + overflow-y: auto; +} +.alertModal--anchored{ + transform: none !important; +} +.rem-con-count{ + position: absolute; + left: 49px; + top: 0px; + color: white; + background-color: #0006; + font-size: 1em; + z-index: 2; + cursor: help; + border-radius: 4px; + padding: 1px 4px 1px 0px; +} +.signal-meter{ + width: 22px; + height: 22px; + position: absolute; + left: 5px; + top: 1px; + background-color: #FFF2; + font-size: 1.5em; + z-index: 2; + cursor: help; +} +.hasMedia > .signal-meter { + display: block; +} +.signal-meter[data-cpu="0"]>.la-signal { + display: block; +} +.signal-meter[data-cpu="0"]>.la-fire-alt { + display: none; +} +.signal-meter[data-cpu="1"]>.la-signal { + display: none; +} +.signal-meter[data-cpu="1"]>.la-fire-alt { + display: block; +} +.signal-meter[data-cpu="1"] { + display:block!important; +} +.signal-meter[data-level="0"] { + color:#000F; + display: none; +} +.signal-meter[data-level="1"] { + color:#FF1B01; +} +.signal-meter[data-level="2"] { + color:#FF8D01; +} +.signal-meter[data-level="3"] { + color:#FFD201; +} +.signal-meter[data-level="4"] { + color:#C6FF01; +} +.signal-meter[data-level="5"] { + color:#00FF00; +} + +.volume-control { + height: 44px; + position: absolute; + left: 0px; + bottom: 0px; + font-size: 1.5em; + z-index: 2; + cursor: help; + padding: 18px 5% !important; + background: #555A; + border-radius: 0 !important; + margin: 0!important; +} + +#mutespeakerbutton { + position: relative; +} + +.speaker-volume-panel { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: 120%; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 10px 12px; + border-radius: 12px; + background: rgba(24, 32, 45, 0.94); + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45); + z-index: 30; +} + +.speaker-volume-panel input[type=range] { + -webkit-appearance: slider-vertical; + writing-mode: bt-lr; + height: 110px; + width: 18px; + cursor: pointer; + padding: 0px; +} + +.speaker-volume-value { + color: #fff; + font-size: 0.8em; + font-weight: 600; + letter-spacing: 0.02em; +} + +.togglePreview{ + border-radius: 11px; + background-color: #00000044; + top: calc(19px + 2vh); + right: 2vw; + cursor: pointer; + width: 22px; + display: flex; + margin: 5px; + position: absolute; + color: white; + font-size: 22px; + z-Index:35; + height: 22px; +} +.togglePreview > .la-eye-slash{ + display: block; +} + +.togglePreview > .la-eye{ + display: none; +} +.togglePreview.blinded > .la-eye-slash{ + display:none!important; +} +.togglePreview.blinded > .la-eye{ + display:block!important; +} +.rounded{ + border-radius: 5px; +} + +.mirror{ + transform: scaleX(-1); +} + +.notification { + position: absolute; + top: -4px; + right: -4px; + padding: 2px 0; + border-radius: 50%; + color: white; + width: 11px; + height: 11px; + margin: 0; +} +.queueNotification { + + padding: 2px 0; + border-radius: 50%; + background: #335c3a; + color: white; + width: 23px; + height: 23px; + margin: 0; +} + +/* Highlight the queue controls whenever guests are waiting */ +#queuebutton.queueAttention { + box-shadow: 0 0 0 2px rgba(255, 120, 120, 0.35); + border-radius: 8px; +} + +#queuebutton.queueAttention i { + color: #ff7b7b; +} + +.queueNotification.queueNotificationPulse { + animation: queuePulse 1.6s ease-in-out infinite; +} + +@keyframes queuePulse { + 0% { + transform: scale(1); + background: #335c3a; + } + 50% { + transform: scale(1.08); + background: #ff7b7b; + } + 100% { + transform: scale(1); + background: #335c3a; + } +} + +button.glyphicon-button:focus, +button.glyphicon-button:active:focus, +button.glyphicon-button.active:focus, +button.glyphicon-button.focus, +button.glyphicon-button:active.focus, +button.glyphicon-button.active.focus { + outline: none !important; +} + +.main { + -webkit-tap-highlight-color: rgba(255, 255, 255, 0) !important; + -webkit-tap-highlight-color: transparent !important; + outline: 0px !important; + height: 100%; + animation: fadeIn 0.2s; + background-size: cover; + background-image: var(--background-main-image); + background-repeat: no-repeat; + background-attachment: fixed; + background-position: center; + overflow-x: hidden; + position: absolute; + z-index: 0; +} + +#controlButtons { + display: flex; + position: fixed; + z-index: 995; + padding: 0px 10px; + bottom: 10px; + padding: 0 10px; + border: 0; + min-height: 0; /* Must have a min-height or drag-drop doesn't work */ + pointer-events: none; + width: 100%; + justify-content: center; + align-items: center; + transform-origin: bottom; /* Keeps it at bottom even if scaled */ +} +#controlButtons:empty { + display: none; +} + +.controlPositioning { + display: flex; + flex-direction: column; + justify-content: center; + position: relative; + width: 100%; + height: 100%; + align-items: center; +} + +.resizable-div { + border: 1px solid #ccc; + overflow: auto; + position: relative; +} + +#subControlButtons { + display: flex; + position: absolute; + background-color: var(--discord-grey-0); + box-shadow: 0px 0px 10px rgba(0,0,0,1); + pointer-events: auto; + border: #cccccc22 1px solid; + border-radius: 10px; + align-items: center; + justify-content: center; + flex-wrap: wrap; + bottom: 0px; + min-width: 230px; + cursor: grab; +} + +#subControlButtons:empty{ + display: none; +} + +#subControlButtons div, #subControlButtons span button { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--discord-grey-1); + opacity: unset; + border-radius: 8px; + transition: border-radius 200ms ease-in-out; + box-shadow: 1px 1px 3px rgba(0,0,0,0.75); +} + +#subControlButtons div:hover { + background-color: var(--discord-grey-3); + border-radius: 4px; +} +button#press2talk{ + border: 0; +} +#press2talk:hover { + box-shadow: inset 0px 0px 1px 1px #346324; +} +#press2talk[data-enabled="true"] { + background: #1e0000; + -webkit-box-shadow: inset 0px 0px 1px #b90000; + -moz-box-shadow: inset 0px 0px 1px #b90000; + outline: none; +} +/* Set for the notification button to use as offset */ +#chatbutton { + position: relative; +} + +button.btnArmTransferRoom{ + width:auto; + margin-left: 2px; + height:38px; + border-radius: 15px; +} +button.btnArmTransferRoom i{ + margin-right:3px; +} +button.btnArmTransferRoom:hover{ + background-color: var(--green-accent)!important; +} + +button.btnArmTransferRoom.selected{ + background-color: #840000!important; +} + +#container.vidcon { + height: 100%; +} + +.nocontrolbar #container.vidcon { + height:100%!important; +} + +.labelSmall { + display: none; +} + +@media only screen and (max-width: 640px){ + .labelSmall { + display:inherit; + padding:5px; + } + + #guestTips{ + max-width: 100%; + font-size: 90%; + } + + .labelLarge { + display:none!important; + } + + .gobutton{ + width: 100vh; + margin-left: 10px; + margin-right: 10px; + max-width: 87%; + } + + .roomnotes{ + display:none!important; + } + + #head5 { + margin-right: 1px; + } +} +@media only screen and (max-width: 480px){ + #guestTips{ + max-width: 100%; + font-size: 80%; + } +} + +@media only screen and (max-height: 540px){ + #gridlayout>#container.vidcon { + height:88% + } + #copythisurl { + font-size:80%; + } +} +@media only screen and (max-height: 500px){ + #gridlayout>#container.vidcon { + height:87% + } +} +@media only screen and (max-height: 400px){ + #logoname{ + display: none; + } + #head4{ + display: none; + } + #head2{ + display: none; + } + #gridlayout>#container.vidcon { + height:85% + } +} +@media only screen and (max-height: 300px){ + #gridlayout>#container.vidcon { + height:81% + } + #head2 { + display:none !important; + } + +} +@media only screen and (max-height: 240px){ + #gridlayout>#container.vidcon { + height:78% + } +} +@media only screen and (max-height: 190px){ + #gridlayout>#container.vidcon { + height:75% + } +} +@media only screen and (max-height: 160px){ + #gridlayout>#container.vidcon { + height:70% + } +} +@media only screen and (max-height: 120px){ + #gridlayout>#container.vidcon { + height:70% + } +} + +#header:empty{ + display: none; +} + +.la-sliders-h { + cursor: pointer; +} + +.icn-spinner { + animation: spin-animation 3s infinite; + display: inline-block; + z-index: 10; +} + +#recordLocalbutton.la-spinner { + animation: spin-animation 3s infinite; + display: inline-block; +} + +.retry-spinner { + border: 1vh solid #7f838666; + border-top: 1vh solid #f0f0f066; + border-radius: 50%; + width: 10vh; + height: 10vh; + animation: spin-animation 3s infinite linear, fadeIn 5s; + margin: 44vh auto; + cursor: help; +} +#retryimage{ + display: block; + margin: auto; + max-width: 100%; + max-height: 100%; + width: 100%; + height: 100%; + animation: fadeIn 2s; + object-fit: contain; +} +#retrymessage{ + display: block; + margin: 80vh auto; + animation: fadeIn 2s; + color: white; + position: absolute; + left: 0; + top: 0; + float: left; + width: 100%; + height: 100%; + text-align: center; + font-size: 2em; +} + +html { + border: 0; + margin: 0; + outline: 0; +} + +body { + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + color: var(--gray90); + padding: 0 0px; + height: 100%; + width: 100%; + background-color: var(--background-color); + font-family: Helvetica, Arial, sans-serif; + border: 0; + margin: 0; + opacity: 1; + transition: opacity 0.1s linear; + scrollbar-color:#666 #201c29; + display:flex; + flex-direction: column; + position: fixed; +} +body.darktheme{ + background-color: var(--dark-background-color); +} + +::-webkit-scrollbar { + width: 15px; + height: 15px; +} + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 13px rgb(0 0 0 / 50%); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + border-radius: 5px; + -webkit-box-shadow: inset 0 0 16px rgb(150 150 150 / 100%); + border: solid 3px transparent; +} + +.previewWebcam { + max-width: 640px; + max-width: 83vw; + height: 30vh; + opacity:1; + animation: fadeIn 0.2s; +} + +#getPermissions{ + font-size: 110%; + border: 3px solid #99A; + cursor: pointer; + background-color: #cce0ff; + margin: 20px; + padding: 10px 50px; + text-align:center; +} + +.gowebcam { + font-size: 110%; + border: 3px solid #ddd; + background-color: #f0f0f0; + color: black; + cursor: pointer; + margin: 20px; + padding: 10px 50px; +} +.gowebcam:enabled { + background-color: #26e726 !important; + background: radial-gradient(#26e726, #2EeF2E); + color: black!important; + font-weight: bold !important; + box-shadow: 0 0 31px 0px #244e1c44; + animation: pulsate 2s ease-out infinite; +} +#logoname { + transition: opacity 0.2s ease-in-out; +} +#logoname:hover { + opacity: 0.8; +} + +#mainmenu { + height: 100vh; +} +.mainmenuclass { + display: inline-block; + width: 100%; +} +.welcomeOverlay{ + object-fit: cover; + width: 100%; + height: 100%; + display: block; + position: absolute; + left: 0; + z-index: 500; + top: 0; + animation: fadeIn 0.1s; + -webkit-animation: fadeIn 0.3s; + -moz-animation: fadeIn 0.3s; + -o-animation: fadeIn 0.3s; + -ms-animation: fadeIn 0.3s; + animation-iteration-count: 1; +} +div[data-action-type='toggle-group'] { + padding: 0 10px; +} +.infoblob { + color: white; + width: 100%; + padding: 20px; + max-width: 1280px; + text-align: var(--rtl-or-ltr); +} + +.outer { + position: relative; + margin: auto; + width: 70px; + margin-top: 0px; + cursor: pointer; +} + +.close { + position: absolute; + right: 20px; + top: 20px; + cursor: pointer; + display: none; +} +.testtonebutton{ + margin: 0 0 0 15px; + padding: 0px 10px 0px 10px !important; + font-size: 84%; + border-radius: 5px!important; + box-shadow: 10px 8px 32px -10px #8883; +} +.testtonebutton:hover{ + background-color: #DDD; +} +.testtonebutton:active{ + background-color: #AAA; +} + +.highlight { + background-color: #ddeeff; +} + +#effectSelector{ + display: inline-block; + padding:2px 0; + min-height: 24px; +} + +#passwordBasicInput{ + min-height: 24px; +} + +/*https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/*/ + +input[type=range] { + -webkit-appearance: none; + margin: 18px 0; + width: 100%; + background-color: #0000; +} + +input[type=range]:focus { + outline: none; +} + +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 8.4px; + cursor: pointer; + box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; + background: #3071a9; + border-radius: 1.3px; + border: .2px solid #010101 +} + +input[type=range]:focus::-webkit-slider-runnable-track { + background: #367ebd +} + +input[type=range]::-moz-range-track { + width: 100%; + height: 8.4px; + cursor: pointer; + box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; + background: #3071a9; + border-radius: 1.3px; + border: .2px solid #010101 +} + +input[type=range]::-ms-track { + width: 100%; + height: 8.4px; + cursor: pointer; + background: 0 0; + border-color: transparent; + border-width: 16px 0; + color: transparent +} + +input[type=range]::-webkit-slider-thumb { + box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; + border: 1px solid #000; + height: 24px; + width: 24px; + border-radius: 3px; + cursor: pointer; + -webkit-appearance: none; + margin-top: -9px; + background-color: var(--lighttheme-2); +} + +input[type=range]::-moz-range-thumb { + box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; + border: 1px solid #000; + height: 24px; + width: 24px; + border-radius: 3px; + cursor: pointer; + background-color: var(--lighttheme-2); +} + +input[type=range]::-ms-thumb { + box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; + border: 1px solid #000; + height: 24px; + width: 24px; + border-radius: 3px; + cursor: pointer; + background-color: var(--lighttheme-2); +} + +input[type=range]::-ms-fill-lower { + background: #2a6495; + border: .2px solid #010101; + border-radius: 2.6px; + box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; +} + +input[type=range]::-ms-fill-upper { + background: #3071a9; + border: .2px solid #010101; + border-radius: 2.6px; + box-shadow: 1px 1px 1px #000,0 0 1px #0d0d0d; +} + +input[type=range]:focus::-ms-fill-lower { + background: #3071a9 +} + +input[type=range]:focus::-ms-fill-upper { + background: #367ebd +} + +input[type=range].inputConstraint { + display: block; + width: 100%; + margin: 5px 0px 10px 0px; +} +/* ////// Icon resizing for mobile subControl bar ////// */ + +input[type=range].thinSlider { + margin: 0; + padding: 0; + height: 10px; + top: 5px; + overflow: hidden; + position: relative; + display: flex; +} +.actionMessage { + color: #000; + margin: 3px; + border-radius: 3px; + background: #FFF; + padding: 5px; + text-align: left; + margin: 10px 3px; + border: 1px solid black; +} +.inMessage, .outMessage, .tipMessage { + margin-bottom: 10px; + padding: 5px; + border-radius: 5px; +} +.inMessage { + background-color: #e6e6e6; +} + +.outMessage { + background-color: #c5d3ff; + text-align: right; +} + +.tipMessage { + background: linear-gradient(135deg, #ffd700 0%, #ffb700 100%); + color: #1a1a1a; + text-align: center; + font-weight: 500; + border: 1px solid #e6a800; +} + +/* Tip banner - on-screen notification */ +.tipBanner { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%) translateY(-100px); + background: linear-gradient(135deg, rgba(255, 215, 0, 0.95) 0%, rgba(255, 183, 0, 0.95) 100%); + color: #1a1a1a; + padding: 15px 30px; + border-radius: 10px; + font-size: 1.4em; + font-weight: 600; + text-align: center; + z-index: 99999; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + border: 2px solid #e6a800; + opacity: 0; + transition: transform 0.4s ease-out, opacity 0.4s ease-out; + pointer-events: none; +} + +.tipBanner.tipBannerShow { + transform: translateX(-50%) translateY(0); + opacity: 1; +} + +.tipBanner.tipBannerHide { + transform: translateX(-50%) translateY(-20px); + opacity: 0; +} + +.tipBannerMessage { + font-size: 0.75em; + font-weight: 400; + margin-top: 5px; + font-style: italic; +} + +#chatBody { + flex-grow: 1; + overflow-y: auto; + padding: 10px; + background-color: #4d4d4d; + min-height: 50px; + border-color: #6b6767; + height: 300px; +} + +#chatBody::-webkit-scrollbar { + width: 0px; + background: transparent; /* make scrollbar transparent */ +} +.chat-input-area { + display: flex; + padding: 10px 10px 4px 10px; + background-color: #2a2a2a; +} +.resizer { + height: 5px; + background: #2a2a2a; + cursor: ns-resize; + position: relative; + overflow: hidden; + cursor: ns-resize; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.resizer::before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 2px; + background: linear-gradient(to right, #666 20%, transparent 20%, transparent 40%, #666 40%, #666 60%, transparent 60%, transparent 80%, #666 80%); +} + +.resizer:hover::before { + background: linear-gradient(to right, #999 20%, transparent 20%, transparent 40%, #999 40%, #999 60%, transparent 60%, transparent 80%, #999 80%); +} +div#chatBody a { + color: blue; + text-decoration: underline; + background-color: #c9c9c9; + border: 2px solid black; + padding: 2px 10px; + border-radius: 6px; + cursor: pointer; +} + +#chatModule { + position: fixed; + bottom: 10px; + right: 10px; + width: 400px; + max-width: 100%; + min-width: 100px; + background-color: #fff; + border: 1px solid #4f4e4e; + border-radius: 5px; + display: flex; + flex-direction: column; + max-height: 100vh; + z-index: 1000; +} +.chat-header { + padding: 10px; + background-color: #2a2a2a; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + color:white; +} + +#chatInput { + flex-grow: 1; + margin-right: 5px; +} +.chatBarInputButton { + width: 50px; + margin: 0 5px; +} +#callerMenu { + position: absolute; + left: 0; + bottom: 200px; +} +#callerMenu button { + font-size: 300%; +} +.foregroundMedia { + width: 100%; + height: 100%; + position: relative; +} +.section { + display: block; + padding: 20px 5px; + margin: 20px auto; + border-radius: 20px; + border: 2px solid #6e6e6e; + max-width: 680px; +} + +@media only screen and (max-width: 500px) { + #subControlButtons { + position: unset; + min-width: unset; + } + #subControlButtons > div { + min-width: 40px; + min-height: 40px; + margin: 4px; + } + #SubControlButtons > div i { + font-size: 28px; + } + #hangupbutton { + margin-left: 15px; + } +} + +@media only screen and (max-height: 500px) { + + #subControlButtons > div { + min-width: 40px; + min-height: 40px; + margin: 4px; + } + #SubControlButtons > div i { + font-size: 28px; + } + #chatModule{ + margin-bottom: 50px; + } +} + +@media only screen and (max-width: 410px) { + #subControlButtons > div { + min-width: 35px; + min-height: 35px; + border-radius: 6px; + margin: 3px; + } + #SubControlButtons > div i { + font-size: 26px; + } + #hangupbutton { + margin-left: 15px!important; + } +} + +@media only screen and (max-height: 410px) { + #subControlButtons > div { + min-width: 35px; + min-height: 35px; + border-radius: 6px; + margin: 3px; + } + #SubControlButtons > div i { + font-size: 26px; + } + #chatModule{ + margin-bottom: 40px; + } + +} + +@media only screen and (max-width: 360px) { + #subControlButtons > div { + min-width: 30px; + min-height: 30px; + border-radius: 4px; + margin: 3px; + } + #SubControlButtons > div i { + font-size: 20px; + } + #hangupbutton { + margin-left: unset; + } +} + +@media only screen and (max-height: 360px) { + #subControlButtons > div { + min-width: 30px; + min-height: 30px; + border-radius: 4px; + margin: 3px; + } + #SubControlButtons > div i { + font-size: 20px; + } + #chatModule { + margin-bottom: 37px; + } +} + +@media only screen and (max-width: 320px) { + #subControlButtons > div { + min-width: 28px; + min-height: 28px; + border-radius: 4px; + margin: 3px; + } + #SubControlButtons > div i { + font-size: 18px; + } +} + +@media only screen and (max-width: 280px) { + #subControlButtons > div { + min-width: 25px; + min-height: 25px; + border-radius: 4px; + margin: 3px; + } + #SubControlButtons > div i { + font-size: 16px; + } +} + +/* //////////////////////////////////// */ + +@media only screen and (max-height: 650px) { + body { + font-size: 0.9em; + } + .gowebcam { + padding: 5px; + margin: 5px; + } + .infoblob { + color: white; + width: 100%; + padding: 20px; + max-width: 1280px; + } + #qrcode img { + max-height: 150px; + } + .outer { + width: 50px; + } + .close { + top: 0px; + right: 0px; + } +} + +@media only screen and (max-width: 1220px) { + #fakeguest4{ + display: none!important; + } + #fakeguestinfo{ + display: none!important; + } +} +@media only screen and (max-width: 933px) { + #fakeguest3{ + display: none!important; + } +} +@media only screen and (max-width: 700px) { + #fakeguest2{ + display: none!important; + } + #chatModule { + margin-bottom: 5px !important; + } +} +@media only screen and (max-width: 292px) { + #fakeguest1{ + display: none!important; + } +} + +@media screen and (max-width: 768px) { + #popOutChat{ + display: none; + } +} + +@media only screen and (max-width: 650px) { + + .mainmenuclass { + display: inline-block; + } + .outer { + width: 50px; + } + .close { + top: 0; + right: 0; + } + select { + font-size: 120%; + } + #reshare { + max-width: 650px !important; + font-size: 96% !important; + width: 300px !important; + } + .fa-paperclip { + display: none; + } + #copythisurl { + color: #DDD; + display: inline-block; + font-size: 75% !important; + } + #logoname { + font-size: 100%; + } + .column { + float: none !important; + padding: 15px 10px 1px 10px !important; + } + div.multiselect, .videoMenu, #videoSettings { + max-width: 100% !important; + min-width: 100% !important; + } + #addPasswordBasic, #headphonesDiv, #effectsDiv, #effectsDiv3, #headphonesDiv3 { + max-width: 100% !important; + min-width: 100% !important; + overflow: hidden !important; + } + #outputSource { + width: 100% !important; + } + #outputSource3 { + width: 100% !important; + } + #audioSourceScreenshare, #videoSettings2 { + max-width: 90% !important; + min-width: 90% !important; + overflow: hidden !important; + } + .popupSelector_constraints{ + margin:25px 15% 0 1%; + } + .mobileHide{ + display:none !important; + } + #effectSelector { + max-width: 100%; + width: 100%; + margin: 4px 0 0 0; + } +} + +@media only screen and (max-height: 355px) { + + .popupSelector_constraints{ + margin:20px 12% 0 2%; + } +} +#popupSelector_user_settings{ + margin-top: 10px; +} + +.tooltip { + position: relative; + display: inline-block; + border-bottom: 1px dotted black; +} +.tooltip .tooltiptext { + visibility: hidden; + width: 10em; + background-color: #9d5050; + color: #fff; + text-align: center; + border-radius: 10px; + position: absolute; + z-index: 1; + top: -10px; + font-family: "Lato", sans-serif; +} +.tooltip:hover .tooltiptext { + visibility: visible; +} + +#previewWebcam.miconly { + display: none; +} + +.notmain > .mainonly { + display:none!important; +} + +#audioSourceScreenshare { + overflow: auto; + resize: both; + width: 100%; +} + +#outputSourceScreenshare { + width: 100%; +} +#outputSource { + margin-top: 7px; + padding:2px 0; + min-height: 24px; +} +/* #audioSourceScreenshare { + display: block; + height: 60px; + width: 100%; + overflow: auto; + padding: 5px; + resize: both; + border: solid 1px #AAA; + border-radius: 4px; +} */ + +/* #headphonesDiv2{ + min-width: 350px; + display: none; + padding: 4px 10px 10px 10px; + vertical-align: middle; + margin: 10px 0; + text-align: left; +} */ +/* #audioScreenShare1 > i { + display: inline-block; +} + +#audioScreenShare1 > span { + margin: 7px 0px; + text-align: left; + display: inline-block; +} */ + +h2 { + color: white; + -webkit-user-select: none; + /* Safari */ + -moz-user-select: none; + /* Firefox */ + -ms-user-select: none; + /* IE10+/Edge */ + user-select: none; + /* Standard */ +} + +.inner { + width: inherit; + text-align: center; +} + +.labelclass { + opacity: 0; + font-size: 1.1em; + line-height: 4em; + text-transform: uppercase; + transition: all .3s ease-in; + cursor: pointer; +} + +label { + color: #000; + -webkit-user-select: none; /* Safari */ + -ms-user-select: none; /* IE 10 and IE 11 */ + user-select: none; +} + +.darktheme .inner:before, +.darktheme .inner:after { + background: var(--discord-text); +} + +.inner:before, +.inner:after { + position: absolute; + content: ''; + height: 7px; + width: inherit; + background: #000; + left: 0; + font-weight: bold; + transition: all .3s ease-in; +} + +.inner:before { + top: 50%; + transform: rotate(45deg); +} + +.inner:after { + bottom: 50%; + transform: rotate(-45deg); +} + +.outer:hover .labelclass { + opacity: 1; +} + +.outer:hover .inner:before, +.outer:hover .inner:after { + transform: rotate(0); +} + +.outer:hover .inner:before { + top: 0; +} + +.outer:hover .inner:after { + bottom: 0; +} +.microphoneBackground{ + background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M 13 4 C 11.90625 4 11 4.90625 11 6 L 11 18 C 11 19.09375 11.90625 20 13 20 L 19 20 C 20.09375 20 21 19.09375 21 18 L 21 6 C 21 4.90625 20.09375 4 19 4 Z M 13 6 L 19 6 L 19 18 L 13 18 Z M 7 14 L 7 18 C 7 21.300781 9.699219 24 13 24 L 15 24 L 15 26 L 11 26 L 11 28 L 21 28 L 21 26 L 17 26 L 17 24 L 19 24 C 22.300781 24 25 21.300781 25 18 L 25 14 L 23 14 L 23 18 C 23 20.21875 21.21875 22 19 22 L 13 22 C 10.78125 22 9 20.21875 9 18 L 9 14 Z'/%3E%3C/svg%3E")!important; +} + +#dropButton{ + font-size: 2em; + display: block; + margin: auto; + background-color: #5555; + border-radius: 30px; + cursor: pointer; + color: #636363; + padding: 3px; + width: 100px; +} +.fullcolumn { + float: left; + display: inline-block; + margin: 0 auto; + width: 100%; + text-align: center; +} + +/* .card { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, .1); + background-color: #ddd; +} */ + +.column { + display: inline-block; + margin: 1.8%; + min-width: 300px; + width: 20%; + padding: 25px; + height: 200px; + text-align: center; + font-size: 100%; + transition: box-shadow 0.2s ease-in-out, border-color 0.2s ease-in-out, transform 0.2s ease-in-out; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.08); + position: relative; + transform: translateZ(0); + backface-visibility: hidden; +} + +.column:hover { + box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .15); + border-color: rgba(0, 0, 0, 0.1); +} + +.column:active { + box-shadow: 0 5px 10px 0 rgba(0, 0, 0, .2); +} + +.column>h2 { + color: black; +} + +.graphSection { + display: flex; + flex-direction: column; + max-width: 50%; + gap: 5px; +} +.darktheme .graphSection > span { + color: var(--discord-text) +} +.graphSection > span { + display: block; + font-size: 10px; + max-height: 50px; + min-height: 20px; +} +.graphSection>span:last-child{ + margin-bottom: 0px; +} +span[data-action-type="stats-graphs-details-container"]>span{ + padding: 1px; + display: block; +} + +#empty-container { + display: inline-block; + width: 20%; + min-width: 300px; + padding: 28px; + height: 200px; + margin: 1.8%; + text-align: center; +} +#container-1 { + background-repeat: no-repeat; + background-size: 80px; + background-position: 50% 65%; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='342.728' height='325.878' viewBox='0 0 90.68 86.222' fill='none' stroke='%23000' stroke-width='5.6' stroke-linejoin='round' stroke-dashoffset='22.7149601' xmlns:v='https://vecta.io/nano'%3E%3Cpath d='M3.15 3.15h37.378v35.24H3.15zm47.002 0H87.53v35.24H50.152zM3.15 47.832h37.378v35.24H3.15zm47.002 0H87.53v35.24H50.152z'/%3E%3C/svg%3E"); +} +#container-2 { + background-repeat: no-repeat; + background-size: 80px; + background-position: 50% 67%; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='347.569' height='278.797' viewBox='0 0 91.961 73.765' fill='none' stroke='%23000' xmlns:v='https://vecta.io/nano'%3E%3Cpath d='M3.02 3.02h85.921v54.399H3.02z' stroke-width='6.04' stroke-linejoin='round' stroke-dashoffset='22.7149601'/%3E%3Cg stroke-width='6.3'%3E%3Cpath d='M35.607 70.527l21.839.071' stroke-linecap='round' paint-order='markers fill stroke'/%3E%3Cpath d='M46.404 73.517l.142-15.596' paint-order='markers fill stroke'/%3E%3C/g%3E%3C/svg%3E"); +} +.mainScreenShareButton { + padding: 18px; + font-size: 120%; + margin: 16px; + animation: pulsate 2s ease-out infinite; + background-color: #26e726 !important; + background: radial-gradient(#26e726, #2EeF2E); + box-shadow: 1px 1px 1px; + color: black !important; + background-color: #26e726 !important; + background: radial-gradient(#26e726, #2EeF2E); + font-weight: bold !important; + box-shadow: 0 0 31px 0px #244e1c44; + animation: pulsate 2s ease-out infinite; +} +.screenshare-container { + max-width: 600px; + margin: 0 auto; + padding: 20px; + color: #fff; +} + +.screenshare-header { + text-align: center; + margin-bottom: 30px; +} + +.screenshare-icon { + width: 240px; + height: 160px; + margin: 0 auto 20px; + display: block; + box-shadow: 0 0 0 0 !important; +} + +.select-screen-btn { + background: #2a2a2a; + color: #fff; + border: 1px solid #666; + padding: 12px 24px; + border-radius: 6px; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + margin-bottom: 15px; + cursor: pointer; + transition: background 0.2s; +} + +.select-screen-btn:hover { + background: #3a3a3a; +} + +.quality-toggle { + background: transparent; + border: none; + color: #999; + cursor: pointer; + padding: 5px; + border-radius: 50%; + transition: background 0.2s; +} + +.quality-toggle:hover { + background: rgba(255,255,255,0.1); +} + +.audio-section { + margin-top: 20px; + padding: 15px; + background: rgba(0,0,0,0.2); + border-radius: 8px; +} + +.info-box { + margin-top: 20px; + padding: 15px; + background: rgba(255,255,255,0.05); + border-radius: 8px; + font-size: 14px; + display: inline-block; + text-align: left; +} + +.info-box h3 { + margin-top: 0; + color: #ccc; + font-size: 16px; +} + +.info-box ul { + margin: 10px 0; + padding-left: 20px; + color: #aaa; +} + +.info-box li { + margin: 8px 0; +} +.info-box li:empty { + display:none; +} +.audio-device-list { + margin-top: 15px; + padding: 10px; + background: rgba(0,0,0,0.2); + border-radius: 6px; +} + +.audio-device-item { + padding: 5px 0; + display: flex; + align-items: center; + gap: 10px; +} + +.audio-device-item:hover { + background: rgba(255,255,255,0.05); + border-radius: 4px; +} + +.audio-device-item label { + margin-left: 8px; + cursor: pointer; +} + +.audio-select-btn { + width: 100%; + padding: 8px; + margin: 10px 0; + background: none; + border: 1px solid currentColor; + border-radius: 4px; + cursor: pointer; +} + +.audio-select-btn:hover { + background: rgba(255, 255, 255, 0.1); +} + +.error-message { + margin-top: 10px; + padding: 10px; + background: rgba(255,0,0,0.1); + border: 1px solid rgba(255,0,0,0.2); + border-radius: 4px; + color: #ff9999; +} + +#container-3 { + background-repeat: no-repeat; + background-size: 90px; + transition: background-image 0.3s ease-in-out; + background-position: 50% 65%; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='347.184' height='187.007' viewBox='0 0 91.859 49.479' fill='none' stroke='%23000' stroke-width='5' stroke-linejoin='round' xmlns:v='https://vecta.io/nano'%3E%3Cpath d='M3.15 3.15h65.569v43.179H3.15z' stroke-dashoffset='22.7149601' paint-order='markers fill stroke'/%3E%3Cpath d='M68.919 28.837L88.709 44.73V7.148L69.019 22.341z'/%3E%3C/svg%3E"); +} +#container-3a { + background-repeat: no-repeat; + background-size: 90px; + transition: background-image 0.3s ease-in-out; + background-position: 50% 65%; +} +#container-4 { + background-repeat: no-repeat; + background-size: 80px; + background-position: 50% 65%; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='303.594' height='274.946' viewBox='0 0 80.326 72.746' fill='none' stroke='%23000' stroke-width='4.6' stroke-linejoin='round' stroke-dashoffset='6.01000023' xmlns:v='https://vecta.io/nano'%3E%3Cpath d='M2 51.27L78.326 2l-8.03 63.359-37.093-12.414z'/%3E%3Cpath d='M33.047 70.746l.157-17.802L78.326 2 33.203 52.944l10.314 3.39z'/%3E%3C/svg%3E"); +} +#container-5 { + background-repeat: no-repeat; + background-size: 80px; + background-position: 50% 65%; + background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMjkgMTI5IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMjkgMTI5IiB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiPgogIDxnPgogICAgPGc+CiAgICAgIDxwYXRoIGQ9Im0xOC43LDEyMi41aDkxLjZjMi4zLDAgNC4xLTEuOCA0LjEtNC4xdi0xMDcuOWMwLTIuMy0xLjgtNC4xLTQuMS00LjFoLTY4LjdjLTAuMywwLTAuNywwLTEsMC4xLTAuMSwwLTAuMiwwLjEtMC4yLDAuMS0wLjMsMC4xLTAuNSwwLjItMC44LDAuMy0wLjEsMC4xLTAuMiwwLjEtMC4zLDAuMi0wLjMsMC4yLTAuNiwwLjQtMC44LDAuN2wtMjIuOSwyN2MtMC4zLDAuMy0wLjUsMC43LTAuNywxLjEtMC4xLDAuMS0wLjEsMC4zLTAuMSwwLjQtMC4xLDAuMy0wLjEsMC42LTAuMiwwLjkgMCwwLjEgMCwwLjEgMCwwLjJ2ODAuOWMtMS4wNjU4MWUtMTQsMi40IDEuOSw0LjIgNC4xLDQuMnptMTguOC0xMDAuOHYxMS44aC0xMGwxMC0xMS44em0tMTQuNywxOS45aDE4LjhjMi4zLDAgNC4xLTEuOCA0LjEtNC4xdi0yMi45aDYwLjV2OTkuN2gtODMuNHYtNzIuN3oiIGZpbGw9IiMwMDAwMDAiLz4KICAgICAgPHBhdGggZD0ibTk0LDUwLjVoLTU5Yy0yLjMsMC00LjEsMS44LTQuMSw0LjEgMCwyLjMgMS44LDQuMSA0LjEsNC4xaDU5YzIuMywwIDQuMS0xLjggNC4xLTQuMSAwLTIuMy0xLjgtNC4xLTQuMS00LjF6IiBmaWxsPSIjMDAwMDAwIi8+CiAgICAgIDxwYXRoIGQ9Im05NCw3MC4zaC01OWMtMi4zLDAtNC4xLDEuOC00LjEsNC4xIDAsMi4zIDEuOCw0LjEgNC4xLDQuMWg1OWMyLjMsMCA0LjEtMS44IDQuMS00LjEgMC0yLjItMS44LTQuMS00LjEtNC4xeiIgZmlsbD0iIzAwMDAwMCIvPgogICAgPC9nPgogIDwvZz4KPC9zdmc+Cg==) +} +#container-5 input[type="file"] { + width: 0.1px; + height: 0.1px; + opacity: 0; + position: absolute; + z-index: -1; +} + +#container-5 input[type="file"] + label { + display: inline-block; + padding: 12px 24px; + background: var(--accent-color, #4a90e2); + color: white; + border-radius: 4px; + cursor: pointer; + margin: 1rem 0; + font-weight: 500; + transition: background 0.2s ease; +} + +#container-5 input[type="file"] + label:hover { + background: var(--accent-hover-color, #357abd); +} + + input[type="file"]#fileselector4 + label:hover { + background: var(--accent-hover-color, #357abd); +} +input[type="file"]#fileselector4 { + width: 0.1px; + height: 0.1px; + opacity: 0; + position: absolute; + z-index: -1; +} + +input[type="file"]#fileselector4 + label { + display: inline-block; + padding: 12px 24px; + background: var(--accent-color, #4a90e2); + color: white; + border-radius: 4px; + cursor: pointer; + margin: 1rem 0; + font-weight: 500; + transition: background 0.2s ease; +} + + +.file-manager { + max-width: 400px; + background: #ddda; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + padding: 16px; + margin: 10px; + transition: all 0.3s ease; +} + +.file-manager.minimized { + max-width: 300px; + padding: 8px; + min-height: 40px; +} + +.file-manager.minimized .file-list, +.file-manager.minimized .file-upload-zone { + display: none; +} + +#activeShares .file-list { + max-height: 400px; + overflow-y: auto; +} + +#activeShares .header-controls { + display: flex; + gap: 8px; +} + +#activeShares .file-manager-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid #eee; + gap: 12px; +} + +#activeShares .file-upload-zone { + border: 2px dashed #ccc; + border-radius: 4px; + padding: 20px; + text-align: center; + margin-bottom: 16px; + cursor: pointer; + transition: border-color 0.3s; +} + +#activeShares .file-upload-zone:hover { + border-color: #666; +} + +#activeShares .file-list { + max-height: 400px; + overflow-y: auto; +} + +#activeShares .file-item { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 12px; + align-items: center; + padding: 12px; + border-bottom: 1px solid #eee; +} + +#activeShares .file-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +#activeShares .file-name { + font-weight: 500; +} + +#activeShares .file-size { + color: #666; + font-size: 0.9em; +} + +#activeShares .download-info { + font-size: 0.85em; + color: #666; + margin-top: 4px; +} + +#activeShares .file-size { + color: #666; + font-size: 0.9em; +} + +#activeShares .file-progress { + position: relative; + height: 4px; + background: #eee; + border-radius: 2px; + overflow: hidden; + margin: 4px 0; +} + +#activeShares .progress-bar { + position: absolute; + left: 0; + top: 0; + height: 100%; + background: #4CAF50; + transition: width 0.3s; +} + +#activeShares .progress-text { + font-size: 0.9em; + color: #666; +} + +#activeShares .transfer-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +#activeShares .no-files { + text-align: center; + color: #666; + padding: 20px; +} + +#activeShares .transfer-item { + background: #f5f5f5; + padding: 8px; + border-radius: 4px; + margin-top: 4px; +} + +#activeShares .transfer-speed { + font-size: 0.9em; + color: #666; +} + +#activeShares .button-primary { + background: #4CAF50; + color: white; +} + +#activeShares .button { + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; + transition: background 0.3s; +} + +#activeShares .button-primary { + background: #4CAF50; + color: white; +} + +#activeShares .button-secondary { + background: #666; + color: white; +} + +#activeShares .button-danger { + background: #f44336; + color: white; +} + +#activeShares .button:hover { + opacity: 0.9; +} + +.controlVideoBox { + position:relative; +} + +.canvasStats{ + background-color: black; + width: 100%; + height: 50px; + border-radius: 4px; +} +.manualInput{ + width: 75px; + display: inline-block; + overflow: scroll; + margin: 4px 0 4px 4px; + padding: 1px 4px; + color:black; +} + +#add_screen { + padding-bottom: 20px; +} +.soloButton{ + display: flex; + flex-wrap: wrap; + font-size: 0.7em; +} +.soloButton button { + margin: 5px 0px 0px 0px; + box-shadow: 1px 1px 3px rgba(0,0,0,0.75); + background-color: #ecfaff; + border-radius: 2px; + width: 100%; +} + +.lowerRaisedHand{ + margin: auto; + margin-top: 5px; + margin-left: 5px; + margin-bottom: 5px; + background-color: yellow!important; + width: 100%; + color:black!important; +} + +.removeFromQueue{ + margin: auto; + margin-top: 5px; + margin-left: 5px; + margin-bottom: 5px; + background-color: #ff00b8!important; + width: 100% +} + +.float { + opacity: 0.8; + min-width: 45px; + min-height: 45px; + height: 100%; + box-shadow: 1px 1px 3px rgba(0,0,0,0.75); + color: #FFF; + border-radius: 8px; + text-align: center; + margin: 5px; + pointer-events: auto; + outline:none; + padding: 5px 5px; +} +.float2 { + opacity: 0.8; + min-width: 45px; + min-height: 45px; + height: 100%; + background-color: #8888; + box-shadow: 1px 1px 3px rgba(0,0,0,0.75); + color: #FFF; + border-radius: 38px; + text-align: center; + z-index: 10; + margin: 5px; + pointer-events: auto; + outline:none; + padding: 5px 5px; +} + +.rotate225 { + transform: rotate(135deg); + position: relative; + top: 1px; +} + +.controlVideoBox video[data-rotated='90']{ + transform: rotate(90deg) translate(9px,-80px) !important; + height: 260px; + width:80px; +} +.controlVideoBox video[data-rotated='180']{ + transform: rotate(180deg) !important; +} +.controlVideoBox video[data-rotated='270']{ + transform: rotate(270deg) translate(-9px,80px) !important; + height: 260px; + width:80px; +} + +#previewWebcam.rotate{ + max-width: 30vh; +} +#previewWebcam.rotate{ + max-width: 30vh; +} + +.myVideo { + box-shadow: rgb(88, 88, 88) 0px 0px 5px 1px; + width: var(--myvideo-width); + max-width: 800px !important; + max-height: 100% !important; + height: var(--myvideo-height) !important; + display: block !important; + margin: auto auto !important; + position: relative !important; + top: 50% !important; + background-color: var(--myvideo-background); + object-fit: var(--fit-style); + max-width: var(--myvideo-max-width) !important; +} +#calendarButton { + cursor: pointer; + z-index: 1; + display: none; +} +#calendar a { + display: block; + margin:10px; +} +#translateButton { + cursor: pointer; + z-index: 1; +} +#helpButton { + cursor: pointer; + z-index: 1; +} +.controlsGrid button[data-action-type="solo-video"]>i{ + color: #b3b300; +} +iframe { + z-index: 2; +} + +#mutebutton.bigbutton { + bottom: 100px; + padding: 100px; + position: fixed; + display: block; + box-sizing: unset; +} +.bigbutton #mutetoggle { + bottom: 20px; + right: 0; + top: unset; +} +.bigbutton .bigbuttontext { + border-radius: 0; + margin: 3px; + display: block; + font-size:200%; +} + +@media only screen and (max-height: 600px) { + .bigbutton { + padding: 80px; + } +} +@media only screen and (max-width: 600px) { + .bigbutton { + padding: 80px; + } +} +@media only screen and (max-height: 500px) { + .bigbutton { + bottom: 80px; + padding: 70px; + } +} +@media only screen and (max-width: 500px) { + .bigbutton { + padding: 70px; + } +} +@media only screen and (max-height: 400px) { + .bigbutton { + padding: 60px; + bottom: 50px; + } +} +@media only screen and (max-width: 300px) { + .bigbutton { + padding: 60px; + } +} +@media only screen and (max-height: 300px) { + .bigbutton { + bottom: 40px; + padding: 50px; + } +} + +@media only screen and (max-width: 390px) { + #translateButton { + display: none; + } + #helpButton { + display: none; + } +} + +.popup .menu { margin: 2px; } + +.toggleSize { + font-size: 32px; + color: white; +} + +img { + max-width: 100%; +} + +/* In-animation, out-animation, and skip-animation are used for the card/buttons on the home page */ +.in-animation { + animation: inlightbox 0.5s forwards; /* @keyframes found in animations.css */ + position: fixed !important; + margin: 0 !important; + z-index: 20; + border-radius: 0 !important; +} +.out-animation { + animation: outlightbox .5s forwards; /* the @keyframes for this animation are dynamically created in lib.js */ + z-index: 20; +} + +.skip-animation { + position: fixed !important; + margin: 0 !important; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: 20; + border-radius: 0 !important; +} +.skip-animation .container-inner{ + display: block; +} + + +.pointer { + cursor: pointer; +} +.modal { + display: none; + position: fixed; + padding-top: 50px; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgb(0, 0, 0); + background-color: rgba(0, 0, 0, 0.5); +} +.modal-content { + position: relative; + background-color: white; + padding: 20px; + margin: auto; + max-width: 400px; +} +.close-btn { + float: right; + color: lightgray; + font-size: 24px; + font-weight: bold; +} +.close-btn:hover { + color: darkgray; +} +#chattoggle{ + top: 0.5px; + position: relative; +} + +.la-phone { + color: red; + top:0.5; +} + +#obsState { + border:#888 solid 2px; + padding:2px 5px; + color: white; + z-index:2; + margin: 0 auto; + background-color: #222D; + display: inline-block; + opacity: 0.8; + border-radius: 4px; + text-align: center; + + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + transform-origin: top center; + +} +#obsState.larger { + padding:2px 10px; + font-size: 30px; +} + +@media only screen and (max-height: 400px){ + #obsState { + transform: translateX(-50%) scale(0.8); + } +} +@media only screen and (max-height: 300px){ + #obsState { + transform: translateX(-50%) scale(0.7); + } +} +@media only screen and (max-width: 620px){ + #obsState { + top:20px; + transform: translateX(-50%) scale(0.7); + } +} +@media only screen and (max-height: 200px){ + #obsState { + transform: translateX(-50%) scale(0.6); + } +} + +@media only screen and (max-width: 400px){ + #obsState { + top:30px; + transform: translateX(-50%) scale(0.6); + display:none!important; + opacity:0; + } +} +@media only screen and (max-width: 300px){ + #obsState { + display:none!important; + opacity:0; + } +} + +#obsState.noheader{ + top:0px; +} +.onair { + box-shadow: inset 0 0 max(10vw, 10vh) green; +} + +.ondeck { + display: block !important; + box-shadow: inset 0 0 max(10vw, 10vh) yellow; + color: black!important; +} + +.recording{ + box-shadow: inset 0 0 max(10vw, 10vh) red +} + +.raisedHand{ + background-color: #DD1A !important; +} +#request_info_prompt{ + z-index: 20; + color: white; + font-size: 30px; + font-size: 3.5vw; + top: 0; + align-self: center; + margin: 25vh 0; + position: absolute; +} + +.holder { + position: relative; + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; + object-fit: contain; + overflow:hidden; + display: flex; + align-items: center; + justify-content: center; + margin: var(--video-margin); + border-radius: var(--video-rounded); + border-width: var(--video-border); + border-color: var(--video-border-color); + background-color: var(--video-holder-color); + border-style: solid; +} + +video { + transition: opacity .25s ease-in-out; + -moz-transition: opacity .25s ease-in-out; + -webkit-transition: opacity .25s ease-in-out; + pointer-events: auto; + background-color: transparent; + border: 0; + margin: 0; + user-select:none; + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ + -webkit-tap-highlight-color:transparent; + outline-style:none; + border-style:solid; + background-size: var(--video-background-image-size); + background-repeat: no-repeat; + background-position: center; + background-image: var(--video-background-image); + +} +video[data-speaking="0"] { + transition: opacity .25s ease-in-out, background-size 0.5s ease; +} +video[data-speaking="1"] { + background-image: var(--video-background-image-talking, var(--video-background-image)); + background-size: var(--video-background-image-size-talking, var(--video-background-image-size)); + transition: background-size 0.5s ease; +} +video[data-speaking="2"] { + background-image: var(--video-background-image-screaming, var(--video-background-image)); + background-size: var(--video-background-image-size-screaming, var(--video-background-image-size)); + transition: background-size 0.5s ease; +} + +.nogb { background-image: unset !important } + +video::-webkit-media-controls-timeline { + display: none; +} + +video::-webkit-media-controls-fullscreen-button { + display: var(--full-screen-button); +} + +video::-webkit-media-controls-timeline-container { + display: none; +} + +audio::-webkit-media-controls-overlay-play-button, video::-webkit-media-controls-overlay-play-button { + display: none; +} + +audio::-webkit-media-controls-play-button, video::-webkit-media-controls-play-button { + display: none; +} + +video::-webkit-media-controls-toggle-closed-captions-button { + display: none; +} + +video.clean::-webkit-media-controls-current-time-display { + display: inherit; +} + +video.clean::-webkit-media-controls-time-remaining-display { + display: inherit; +} + +video.clean::-webkit-media-controls-timeline { + display: inherit; +} + +video.clean::-webkit-media-controls-timeline-container { + display: inherit; +} + +audio.fileshare::-webkit-media-controls-overlay-play-button, video.fileshare::-webkit-media-controls-overlay-play-button { + display: inherit; +} + +audio.fileshare::-webkit-media-controls-play-button, video.fileshare::-webkit-media-controls-play-button { + display: inherit; +} + +video::-webkit-media-controls-panel { + background-color: #0000; +} + +#main.forcecontrols video::-webkit-media-controls-panel { + opacity: 1 !important; + visibility: visible !important; + display: flex !important; +} + +.mirrorControl::-webkit-media-controls-enclosure { + padding: 0px; + height: 30px; + transform: scaleX(-1); + -webkit-transform: scaleX(-1); +} +.popup-screen { + text-align: center; + position: absolute; + display: none; + top:0; + left: 0; + z-index: 7 !important; + padding: 20px; + margin:15px 15px 80px 15px; + width: 80vh !important; + height: 80vh !important; + background-color: #ccc !important; + border: solid 1px #dfdfdf !important; + box-shadow: 1px 1px 2px #cfcfcf !important; +} +.context-menu { + display: none; + position: absolute; + z-index: 996 !important; + padding: 12px 0 !important; + width: 240px !important; + background-color: #fff !important; + border: solid 1px #dfdfdf !important; + box-shadow: 1px 1px 2px #cfcfcf !important; +} + +.darktheme .popup-message { + background-color: var(--discord-grey-1); + border-color: var(--discord-grey-3); + color: var(--discord-text); + box-shadow: 1px 1px 3px black; +} + +.popup-message { + display: none; + text-align: center; + position: absolute; + z-index: 35; + padding: 5px; + border-radius: 4px; + min-width: 180px; + background-color: #fff; + border: solid 1px #dfdfdf; + box-shadow: 1px 1px 2px #cfcfcf; +} + +.darktheme .popup-message ul { + list-style: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAALCAYAAACtWacbAAAAr0lEQVQYlY3QMUoDUBAE0KciNtpI+hQ5gdhZ5QIewwN4EisJpM0RhJRiZSt4A8EynWlCig0j+wttdODzYXZndhj/QlX9frOqeq6q96paZ3aMNW7b8AorTPCBRcijKJrI4gsuscXNSBKnDaZ4xQX2OGtBnL+X7lp5jhNc9x/HR8xybmR5aKeBXQs3cQreMO/BKb46U7JOxlJCP7Uyp++bX+JzdJQ+0kv6SU8/uvsbOACH0VkbmsdQwQAAAABJRU5ErkJggg==); +} + +.popup-message ul { + list-style: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAA/ElEQVQYlTWPwUrEMBCGJ2mUNKZxIbRQ2oPeRT34OH2HPfQ5eugzeRG8rOLdvRVagm5Jm+DSILO0c5kZZub/vyF1XcMWTdNgda2UeqSUSmvt736/P7C2bZ9DCN8hhBMAxEqpJ601H4bhJ4RwxCsqpdwJIe6wieP4Icsy3nWdt9Z+rIdAlFIvWmuJAynlVRRFsCwLTNPknXNfAODoOI6fxhhfliUnhNCqql4xF0Vxg9bISFbmGK2EEJwxdlGy1p7zPOfGGEvX75xz7n2apnOSJBFaee/fEB6/ZBd6Sm8RPk1T3vf9xgLzPB8ppWRbumeM7VAaGQHgb3U4AcDhH7U6eiFtegoRAAAAAElFTkSuQmCC'); + margin-left: 14px; + padding: 5px; +} + +.popup-message li { + text-align: left; + padding-left: unset; + line-height: unset; + margin: 0 0 0 18px; +} + +.context-menu--active { + display: block !important; +} +.context-menu__items { + list-style: none !important; + margin: 0; + padding: 0; +} +.context-menu__item { + display: block; + margin-bottom: 4px !important; +} +.context-menu__item:last-child { + margin-bottom: 0 !important; +} +.context-menu__link { + display: block; + padding: 4px 12px; + color: #0066aa !important; + text-decoration: none; +} +.context-menu__link:hover { + color: #fff !important; + background-color: #0066aa !important; +} +.context-menu__tip { + margin-left: 15px; + color: #777; + margin-top: 10px; + padding-top: 10px; + position: relative; + top: 7px; +} +#bufferSliderValue{ + margin-left:10px; +} +.context-menu__link:hover > #bufferSliderValue { + color: #fff; +} + +/* Submenu support for context menu */ +.context-menu__item--has-submenu { + position: relative; +} +.context-menu__item--has-submenu > .context-menu__link::after { + content: '\276F'; + float: right; + margin-left: 10px; + font-size: 0.8em; +} +.context-menu__submenu { + display: none; + position: absolute; + left: 100%; + top: 0; + z-index: 997 !important; + padding: 12px 0 !important; + width: 200px !important; + background-color: #fff !important; + border: solid 1px #dfdfdf !important; + box-shadow: 1px 1px 2px #cfcfcf !important; + list-style: none !important; + margin: 0; +} +.context-menu__item--has-submenu:hover > .context-menu__submenu { + display: block; +} +.context-menu__submenu--left { + left: auto; + right: 100%; +} + +/* Dark theme for context menu and submenus */ +.darktheme .context-menu { + background-color: var(--discord-grey-1) !important; + border-color: var(--discord-grey-3) !important; + box-shadow: 1px 1px 3px black !important; +} +.darktheme .context-menu__link { + color: var(--discord-text) !important; +} +.darktheme .context-menu__link:hover { + background-color: var(--discord-grey-3) !important; +} +.darktheme .context-menu__submenu { + background-color: var(--discord-grey-1) !important; + border-color: var(--discord-grey-3) !important; + box-shadow: 1px 1px 3px black !important; +} + +.selectedContentEffectsImage{ + box-shadow: 0 0 10px #2c3554; + outline: 2px solid black; +} + +.multiselect .multiselect-trigger:hover { + cursor: pointer; + cursor: hand; + text-decoration: none; +} +/* .multiselect .multiselect-trigger.open { + border-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.multiselect .multiselect-trigger.closed { + border-bottom: 1px solid #ccc; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} */ + +span[data-resolution]:hover, span[data-bitrate]:hover { + color:lightblue; + text-decoration: underline; +} +.gear_microphone{ + user-select: none; + float: right; + height: 0; + padding: 0; + top: 6px; + position: relative; +} +.gear_microphone.gearflat{ + top: -1px; +} +.gear_microphone>input{ + top: 1px; + position: relative; +} +#micStereoMonoInput3{ + width: 10px; + height: 11px; +} +#headphonesDiv3 { + text-align: left; + margin: 17px 0 0 0; + width: 463px; + padding: 10px 10px; + vertical-align: middle; +} +#headphonesDiv { + width: 463px; + padding: 4px; +} +.selected { + border: solid 3px black; + padding: 4px; +} +#audioMenu { + margin: 15px 0 0 0; +} + +#minipreview > #videosource { + height:auto!important; + width:auto!important; + max-height:100%!important; + max-width:100%!important; + border-radius: 0!important; +} + +#videoSource3 { + width: calc(100% - 50px); +} + +#videoSourceSelect { + padding:2px 0; + min-height: 24px; +} +#gear_webcam{ + cursor: pointer; + display: inline-block; + padding: 0 0 0 3px; +} +.gone { + position:absolute; + top: -150px; +} +.grabLinks { + display: inline-flex; + cursor: grab; + font-weight: bold; + font-size: 1em; + padding: 10px; + margin: 5px 0; + word-break: break-all; +} +.grabLinks a:hover { + color: black !important; +} +.grabLinks a:active { + color: black !important; +} +.grabLinks a:link { + color: black !important; +} + +.hidden { + display: none !important; + visibility: hidden; + width: 0px; + height: 0px; + opacity: 0; +} +.hidden2 { + display: none !important; + visibility: hidden; + width: 0px; + height: 0px; + opacity: 0; +} + +.grabLinks a:visited { + color: black !important; +} + +.permahide { + display: none!important; + visibility: hidden; + width:0px; + height:0px; + opacity: 0; + background: #0000; + color: #0000; + font-size: 0em; + pointer-events:none; +} + +.multiselect .multiselect-contents { + display: block; + margin: 0; + font-size: 95%; + padding: 2px 3px 0px; + border-top: 0; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + text-align: left; +} + +.multiselect .multiselect-contents li { + list-style: none; + overflow: hidden; +} +.select .select-trigger:hover { + cursor: pointer; + cursor: hand; + text-decoration: none; +} +.select .select-trigger.open { + border-bottom: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.select .select-trigger.closed { + border-bottom: 1px solid #ccc; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} +.select .select-contents { + display: none; + margin: 0; + padding: 0 24px 24px; + border: 1px solid #ccc; + border-top: 0; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} +.select .select-contents li { + list-style: none; +} +::-webkit-input-placeholder { + color: #555 !important; +} +::-moz-placeholder { + color: #555 !important; +} +:-ms-input-placeholder { + color: #555 !important; +} +:-moz-placeholder { + color: #555 !important; +} +label { + font-weight: 400; +} + +#screenshare { + height: 300px; + display: inline-block; + max-height: 50vh; + max-width: 50vh; + border: 0; + margin: 0; + margin-bottom:15px; + padding: 0; + text-shadow: unset; + box-shadow: unset; + text-decoration: none; + border-image-width: 0; + background-size: contain; + background-color: rgba(0, 0, 0, 0); +} + +.debugStats { + font-size: 0.8rem; + list-style-type: none; + left: 50px; + top: 0px; + width: 300px; + min-height: 200px; + max-height: 99vh; + overflow-y: auto; + background-color: rgba(0, 0, 0, 0.95); + position: absolute; + z-index: 20; + color: white; + padding: 20px; + border: 2px solid #1d1d1d; + padding-bottom: 100px!important; + margin-bottom: 100px!important; +} +.debugStats::-webkit-scrollbar { + width: 0.5em; +} +.debugStats::-webkit-scrollbar-track { + background: black; + border-radius: 10px; +} + +.debugStats::-webkit-scrollbar-thumb { + background: rgb(119, 119, 119); + border-radius: 10px; +} + +.debugStats::-webkit-scrollbar-thumb:hover { + background: rgb(158, 158, 158); + ; +} +.debugStats h1 { + font-size: 1rem; + text-align: left; + text-transform: uppercase; + margin-bottom: 10px; + margin-top: -5px; +} +.debugStats h2 { + font-size: 0.8rem; + text-align: left; + text-transform: uppercase; + margin-top: 10px; + white-space: nowrap; + text-overflow: ellipsis; + display: block; + overflow: hidden; +} +.viewstats::-webkit-scrollbar-track { + box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); +} +.debugStats li { + display: flex; + margin: 5px 0px; +} +.debugStats li:nth-child(even) { + background: rgba(33, 33, 33, 0.8); + padding: 2px 0px; +} +.debugStats li span:first-child { + flex: 1; + white-space: nowrap; +} +.debugStats li span:last-child { + flex: 1; + text-align: right; + max-height: 49px; + overflow: auto; +} +.debugStats .close { + font-weight: bold; + color: white; + display: block; + background: none; + padding: 0; + margin: 0; + font-size: 1.5rem; + border: none; + top: 10px; + right: 10px; +} +.debugStats button:not(.close) { + margin: 10px 0px; + padding: 10px 0px; + background: #263250; + color: white; + border-radius: 0; + width: 100%; + font-weight: bold; + border-bottom: 2px solid #364c84; +} + +.directorMargins { + margin: var(--director-margin); +} + +.hideLinksClass { + background-color: var(--container-color); + width:1191px; + padding: 10px; + margin: 5px 10px 10px 10px; + max-width:100%; +} +.directorContainer { + background-color: var(--container-color); + margin: 10px 0px 10px 10px; + padding: 10px; + max-width: min(100%, 1191px); +} + +#directorLinksButton{ + cursor: pointer; +} +.directorContainer.half { + background-color: var(--container-color); + padding: 10px 10px; + width: min(100%, 591px); +} +.directorBlock { + padding: 10px 10px 5px 10px; + margin: var(--regular-margin); + color: white; + position:relative; + max-width: 100%; + overflow: hidden; + display: block; + min-height: 174px; +} +.directorBlock:nth-child(1) { + background-color: var(--blue-accent); +} +.directorBlock:nth-child(2) { + background-color: var(--green-accent); +} +.directorBlock:nth-child(3) { + background-color: var(--olive-accent); +} +.directorBlock:nth-child(4) { + background-color: var(--red-accent); +} +.directorBlock button { + margin: 10px; + box-shadow: unset; + border:0; + border-radius:0; + font-size: 1.1em; + padding: 6px 9px 4px 9px; + background-color: #2a2a2a; + color:white; + max-width:25%; +} +.directorBlock button i { + margin-right: 5px; +} +a.task { + width: 100%; +} + +.directorBlock h2 { + text-transform: uppercase; + margin-bottom: 10px; + margin-left: 5px; + font-size:1.2em; +} +.shift { + display: inline-block; + position: relative; + margin: 0 0 0 4px; + padding: 0; + min-width: 25px; + font-size: 0.8em; + top: -4.9px; + color: white; +} +.shift>i { + cursor: pointer; + width: 10px; + margin: 0 auto; + left: -1.1px; + position: relative; +} +.shift.locked>i{ + display: none; +} +.shift.locked>span{ + margin-left: 7px; +} +div#roomnotes2 { + background: var(--container-color); + padding: 10px !important; + margin: 0 var(--regular-margin) 10px var(--regular-margin); + width: 100%; +} + +#yourDirectorStatus, #yourDirectorStatusSS { + color: var(--discord-text); +} +.directorBlue{ + background-color: #5c7785 !important; + display: var(--show-codirectors) !important; + +} +.directorBox{ + background-color: #606383 !important; + display: var(--show-codirectors) !important; +} + +/* ---- DIRECTORS PAGE - Guest Controls Box ---- */ +.controlsGrid { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.controlsGrid .group { + width: 100%; + display: flex; + flex-direction: column; + gap: 5px; +} + +.controlsGrid .row { + width: 100%; + display: flex; + flex-wrap: wrap; + gap: 5px; +} +.controlsGrid .row > .row { + margin: 0; +} +.controlsGrid .row.two > * { + flex: 1 33%; +} +.controlsGrid .row.three > * { + flex: 1 25%; +} +.controlsGrid .row.four > * { + flex: 1 20%; +} +.controlsGrid .row.six > * { + flex: 1 13%; +} + +.controlsGrid .row.eight > * { + flex: 1 10%; +} + + +.controlsGrid button { + display: flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 4px; + border-radius: 2px; + gap: 2px; + box-shadow: 1px 1px 3px rgba(0,0,0,0.25); + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + text-transform: lowercase; +} + +.controlsGrid button span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.controlsGrid button i { + font-size: 15px; +} + +.controlsGrid button[data-action-type="mute-guest"] { + min-width: calc(33.3% - 5px); + box-shadow: inset 0px 0px 1px 0px #ff0000b5, 1px 1px 3px rgba(0,0,0,0.25); +} +/* Button icons: Red */ +.controlsGrid button[data-action-type="mute-scene"] i, +.controlsGrid button[data-action-type="mute-guest"] i, +.controlsGrid button[data-action-type="hangup"] i, +.controlsGrid button[data-action-type="remote-global-record"] i, +.controlsGrid button[data-action-type="local-global-record"] i, +.controlsGrid button[data-action-type="recorder-local"] i, +.controlsGrid button[data-action-type="recorder-google-drive-remote"] i, +.controlsGrid button[data-action-type="recorder-remote"] i { + color: #e30000; +} +/* Button icons: Green */ +.controlsGrid button[data-action-type="addToScene"] i, +.controlsGrid button[data-action-type="solo-chat"] i { + color: #03a303; +} +/* Button icons: Blue */ +.controlsGrid button[data-action-type=""] i { + color: #00f; +} + +/* Specitic CSS for different elements inside the guest control-buttons */ +.darktheme .controlsGrid .director-message-box { + background-color: var(--discord-grey-3); + border: 1px solid var(--discord-grey-8); +} + +.darktheme .controlsGrid .director-message-box button { + background-color: var(--discord-grey-6); +} + +.controlsGrid .director-message-box { + display: flex; + flex-wrap: wrap; + gap: 5px; + flex: 1 100% !important; + padding: 5px; + background: rgba(0, 0, 0, .15); + border-radius: 4px; + max-width: 100%; +} + +.darktheme .controlsGrid .director-message-box textarea { + background-color: var(--discord-grey-6); + border: 1px solid var(--discord-grey-7); + color: var(--discord-text); +} + +.controlsGrid .director-message-box textarea { + flex: 1 100%; + padding: 5px; + border-radius: 4px; + outline: none; +} + +.controlsGrid .director-message-box .message-close { + flex: 1; +} +.controlsGrid .director-message-box .message-send { + flex: 1 33%; +} + +.controlsGrid .tooltip { + flex: 1 calc(50% - 10px); + display: flex; + align-items: center; +} +.controlsGrid .tooltip input[type=range] { + margin: 0; +} +.controlsGrid .tooltip .tooltiptext { + height: 18px; + line-height: 1.2; + top: 3px !important; + left: 100% !important; + background-color: #e6a0a0; + border: 1px solid rgba(0,0,0,1); + border-radius: 4px; + font-size: 12px; +} + +.controlsGrid .hideDropMenu{ + justify-content: left; + width: 100%; + box-shadow:unset; + background-color: #0000; + border:0; + color: #fcfcfc; +} + +.darktheme .controlsGrid .orderspan { + color: var(--discord-text); +} + +.controlsGrid .orderspan { + font-size: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + text-align: center; + position: relative; + user-select: none; + + color: #fcfcfc; +} +.controlsGrid .orderspan .order-label { + width: max-content; +} + +/* Hightlights for buttons in the guest control-buttons */ +/* Dark theme */ +.darktheme .controlsGrid .btn-HL-yellow { + background-color: var(--darktheme-yellow); +} +.darktheme .controlsGrid .btn-HL-peach { + background-color: var(--darktheme-brown); +} +.darktheme .controlsGrid .btn-HL-green { + background-color: var(--darktheme-green); +} +.darktheme .controlsGrid .btn-HL-blue { + background-color: var(--darktheme-blue); +} +.darktheme .controlsGrid .btn-HL-paleblue { + background-color: rgb(41 54 72); +} +.darktheme .controlsGrid .btn-HL-orange { + background-color: rgb(185, 104, 42); +} +.darktheme .controlsGrid .btn-HL-purple { + background-color: rgb(111, 72, 139); +} +.darktheme .controlsGrid .btn-HL-red { + background-color: rgb(159, 62, 56); +} +.darktheme .controlsGrid .btn-HL-teal { + background-color: rgb(54, 140, 140); +} +/* Light theme */ +.controlsGrid .btn-HL-yellow { + background-color: rgb(255, 229, 127); +} +.controlsGrid .btn-HL-peach { + background-color: rgb(228, 203, 189); +} +.controlsGrid .btn-HL-green { + background-color: rgb(189, 228, 199); +} +.controlsGrid .btn-HL-blue { + background-color: rgb(170, 204, 248); +} +.controlsGrid .btn-HL-paleblue { + background-color: rgb(170, 204, 248); +} +.controlsGrid .btn-HL-orange { + background-color: rgb(255, 187, 115); +} +.controlsGrid .btn-HL-purple { + background-color: rgb(208, 186, 233); +} +.controlsGrid .btn-HL-red { + background-color: rgb(255, 153, 153); +} +.controlsGrid .btn-HL-teal { + background-color: rgb(131, 215, 215); +} + +/* Hides buttons that are supposed to be hidden when &novice is added to URL */ +.controlsGrid .advanced.hide { + display: none; +} + +.appmode #head1 , .appmode #head1a { + display:flex; +} + +.appmode #head1 input, .appmode #head1a input{ + width: 100%; + padding: 10px; + margin: 5px; + font-size: 125%; +} +#widget { + position: absolute; + width: var(--widget-width); + max-width: 75%!important; + height: 100%; + right: 0; + top: 0; +} +#widget.left{ + right: unset!important; + left: 0!important; +} +#directorlayout.widget { + position: absolute; + width: 75%; + left: 0; +} +#directorlayout.widget.left { + left: unset!important; + right: 0!important; +} +#localMuteElement{ + top: 1vh; + right: 1vh; +} +#localVoiceMeter{ + width: 10px; + height: 10px; + top: 8px; + right: 10px; +} +.controlCenterBox{ + display: flex; + flex-direction: column; + gap: 2.5px; + padding: 5px; +} + +.darktheme .controlCenterBox .flexBreak { + border-bottom: 1px double var(--discord-grey-7); +} + +.controlCenterBox .flexBreak { + width: 100%; + border-bottom: 1px #ccc double; + margin: 0; + position: relative; + user-select: none; + filter: blur(2px); +} + +.darktheme .controlCenterBox .flexBreak span { + text-shadow: 0px 0px 1px var(--discord-text); + background-color: var(--discord-grey-3); + color: var(--discord-text); +} +#hangupContainer { + font-size: 500%; + text-align: center; + margin: auto auto; + display: flex; + height: 100%; + width: 100%; + vertical-align: middle; + flex-wrap: wrap; + align-content: center; + flex-direction: column; + justify-content: center; + align-items: stretch; +} +.controlCenterBox .flexBreak span { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-transform: uppercase; + font-size: 10px; + text-shadow: 0px 0px 1px var(--discord-text); + color: var(--discord-text); + background-color: #7E7E7E; + padding: 0px 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.contolboxLabel { + float: left; + top: 2px; + margin-left: 5px; + position: relative; + cursor: pointer; +} +.fullwindowButton { + transition: opacity 0.3s; + width: 4vh; + height: 4vh; + max-width: 30px; + max-height: 30px; + min-width: 15px; + min-height: 15px; + position: absolute; + display: none; + z-index: 6; + right: 4vh; + top : 4vh; + color: white; + cursor: pointer; +} +.fullwindowButtonimg { + user-select: none; + background-color: #0007; + width: 4vh; +} + + +.pull-right { + float: right; + right: 0; +} +.pull-left { + float: left; + left: 0; +} +.streamID { + text-align: right; + margin: 5px 5px 2px 0px; + font-size: 0.7em; + text-overflow: ellipsis; + overflow: hidden; + position: relative; + width: 227px; + display: inline-block; + color: white; +} +.streamID i { + margin-left: 1px; + font-size: 1.3em; + position: relative; + top: 1px; +} + +.vidcon>h2 { + font-size: 1em; + margin-top: 20px; +} +#pptbackbutton { + margin-left: 20; +} +#pptnextbutton { + background-color: #007900; +} +#pptbackbutton:active { + -webkit-box-shadow: inset 0px 0px 17px #4b4b4b; + -moz-box-shadow: inset 0px 0px 17px #4b4b4b; + box-shadow: inset 0px 0px 17px #4b4b4b; + outline: none; +} +#pptnextbutton:active { + -webkit-box-shadow: inset 0px 0px 17px #4b4b4b; + -moz-box-shadow: inset 0px 0px 17px #4b4b4b; + box-shadow: inset 0px 0px 17px #4b4b4b; + outline: none; +} + +.darktheme div#guestFeeds { + background-color: var(--container-color) +} + +div#guestFeeds { + background-color: var(--container-color); + padding: 5px 0 15px 20px; + display: inline-block; + margin: 0px var(--regular-margin) 80px var(--regular-margin); +} + +div#guestFeeds:empty { + display: none; +} +#hiddenElements{ + visibility:hidden; + left:-9999; + top:-9999; + width:0px; + height:0px; + display: block; +} +#reshare, #inviteLinkURL { + font-weight: bold; + color: #afa !important; + cursor: grab; + background-color: #0000; + font-size: 115%; + min-width: 335px; + max-width: 800px +} +a#reshare , a#inviteLinkURL{ + white-space: nowrap; + margin: 0; + padding: 0; + display: inline; +} +#copythisurl+i, #copythisurl2+i { + display: inline; + font-size: 130%; +} +#joinroomID { + border-radius: 0; + padding: 5px; +} +#joinroomID+button { + margin: 0px var(--regular-margin); +} + +#joinbyURL { + border-radius: 0; + padding: 5px; +} +#joinbyURL+button { + margin: 0px var(--regular-margin); +} + +.appmode #joinroomID+button { + width: 110px; +} + +.appmode #joinbyURL+button { + width: 110px; +} + #guestTips { margin: 0 auto 15px auto; max-width: 463px; @@ -4704,7 +4704,7 @@ a#reshare , a#inviteLinkURL{ border: 1px solid #cccccc; padding: 10px; } - + #guestTips p { font-weight: bold; margin-bottom: 10px; @@ -4733,3288 +4733,3288 @@ a#reshare , a#inviteLinkURL{ #guestTips .guest-tip-item span { line-height: 1.4em; } - -.video-label { - position: absolute; - bottom: 0.6vh; - left: 0.5vh; - margin: 0px; - color: white; - padding: 5px 10px; - background: rgba(0, 0, 0, .5); - font-size: 1em; - pointer-events:none; -} -.video-label:empty { - display: none; -} - -.video-label>span:nth-child(2) { - font-size: 50%; - display: block; - text-align: center; -} -.video-label>span:nth-child(3) { - font-size: 25%; - display: block; - text-align: center; - line-height: 200%; -} -.video-label>span:nth-child(4) { - font-size: 25%; - display: block; - text-align: center; - line-height: 200%; -} -.video-label>span:nth-child(5) { - font-size: 25%; - display: block; - text-align: center; - line-height: 200%; -} - -.video-label.zoom { - bottom: 0; - left: 0; - pointer-events:none; -} - -.video-label.teams { - background: rgba(0, 0, 0, .4); - pointer-events:none; - border-radius: 5px; -} - -.video-label.skype { - bottom: 2vh; - left: 50%; - transform: translateX(-50%); - background: rgba(0, 0, 0, .8); - pointer-events:none; - border-radius: 5px; - font-size: 0.8em; -} - -.video-label.ninjablue { - bottom: 5%; - left: 0; - background: #141926; - padding: 10px 5%; -} - -.video-label.toprounded { - top: 0; - bottom: unset; - background: rgb(0 0 0 / 70%); - padding: 10px 5%; - left: 50%; - transform: translateX(-50%); - width: 50%; - text-align: center; - border-bottom-left-radius: 50px; - border-bottom-right-radius: 50px; - text-transform: uppercase; - letter-spacing: 3; - box-shadow: 0px 0px 10px #00000059; - font-size: 0.7em -} - -.video-label.rounded { - bottom: 0; - background: rgb(0 0 0 / 70%); - padding: 0px 27px; - left: 0; - border-top-right-radius: 20px; - box-shadow: 0px 0px 10px #00000059; - font-size: 0.7em; - font-weight: bold; -} - -.video-label.fire { - text-shadow: 0 -1px 4px #FFF, 0 -2px 10px #ff0, 0 -10px 20px #ff8000, 0 -18px 40px #F00; - font-weight: bold; - bottom: 2vh; - left: 0; - width: 100%; - text-align: center; -} - -.video-meter { - padding:0.5vh; - display: block; - width: min(1vh, 1vw); - height: min(1vh, 1vw); - min-width:10px; - min-height:10px; - top: 2vh; - right: 2vh; - background-color: #00c300; - position:absolute; - border-radius: 2vh; - pointer-events:none; - border: 1px black solid; - z-Index: 2; -} - -.video-meter-2 { - padding:0; - display: block; - width: 100%; - height: 100%; - min-width:10px; - min-height:10px; - top: 0; - right: 0; - background-color:unset; - position:absolute; - border-radius: 5px; - pointer-events:none; - border: 5px green solid; - z-Index: 2; -} - -.video-meter-director { - width: 10px; - height: 10px; - top: 8px; - right: 10px; -} -.video-meter2 { - display: block; - padding:0; - width: 4px; - height:0%; - min-width:2px; - bottom: 1px; - right: 7px; - background-color: #0000; - position:absolute; - border-radius: 2vh; - pointer-events:none; - border: 1px black solid; - transition: height 0.1s ease, background-color 0.1s ease; - z-Index: 2; -} - -.hasMedia > .video-meter2 { - display: block; -} -.hasMedia > .video-meter-2 { - display: block; -} -.hasMedia > .video-meter { - display: block; -} - -#voiceMeterTemplate{ - display: none; -} -#voiceMeterTemplate2{ - display: none; -} - -#userList{ - line-height: 1.3em; -} - -#userList > div > .video-meter { - padding: 5px; - margin-left: 5px; - top: 0; - right: 0; - position: relative; - display: inline-block; -} -.PPTActive { - box-shadow: 0px 0px 10px green; -} - - -.video-mute-state { - top: 2vh; - right: 2vh; - position: absolute; - color: white; - border-radius: 2vh; - background-color: #b11313; - padding: 2px 2px 2px 1px; - z-index: 2; -} - -.video-mute-state.unmuted { - background-color: green; -} - -.video-mute-state .la-microphone { - display: none; -} - -.video-mute-state .la-microphone-slash { - display: inline-block; -} - -.video-mute-state.unmuted .la-microphone { - display: inline-block; -} - -.video-mute-state.unmuted .la-microphone-slash { - display: none; -} - - - -.video-mute-state-userlist { - display: inline-block; - color:white; - border-radius: 2vh; - background-color: #b11313; - padding: 2.2px 1.5px 2px 2px; - margin: 0 0 0 5px; -} -.volume-control-userlist { - display: inline-block; - color:white; - border-radius: 2vh; - background-color: #262c3e; - padding: 2.2px 1.5px 2px 2px; - margin: 0 0 0 5px; - cursor: pointer; - border: 1px solid #9a9393; -} - -.volume-control-userlist input[type=range][orient=vertical] { - display: block; - color: white; - border-radius: 2vh; - -webkit-appearance: slider-vertical; - background-color: #262c3e; - cursor: pointer; - width: 17px; - margin: 0px 1px 4px 0; - padding: 0; -} - -#closedList_connectUsers{ - - cursor: pointer; - color: #d9e4eb; - padding: 2px; - margin: 2px 2px 0 0; - font-size: 140%; -} - -.screenshareNotActive{ - opacity: 0.5; - box-shadow: inset 0 0 50px #290f07; -} -#help_directors_room{ - cursor: pointer; -} - -.iframeblob{ - padding-top:18px; - text-align: left; - width: 600px; - display: block; - margin: auto; -} -#shareScreenGear{ - display: none; -} - -div.message-card { - padding: 10px; - display: block; - padding-left: 1em; - align-items: center; - width: 600px; - margin: 0 auto; - position: relative; - padding-left: 60px; - margin: 20px auto; - box-shadow: 0px 5px 10px -5px #a9a9a9; -} - -div.message-card a { - color: rgb(0 77 218); - font-weight: bold; - text-decoration: underline; -} - -.message-card ul { - border: unset !important; - background-color: unset !important; - color: unset !important; - list-style: outside; -} - -.warning.message-card { - border-left: 4px solid #eff150; - background: #fffded; -} -.info.message-card { - border-left: 4px solid #aacefd; - background: #e6e8f0; -} -.darktheme #guestTips { - background-color: var(--discord-grey-7); -} -.darktheme #guestTips .las { - color: #FFF; -} -.darktheme .message-card { - background-color: #000; -} -.darktheme input[type='file'] { - background-color: #000; -} -.message-card h1 { - display: block; - font-size: 110%; - text-align: left; -} - -.message-card p { - display: block; - text-align: left; - margin-top: 10px; -} - -div.message-card:before { - font-family: 'Line Awesome Free'; - font-weight: 900; - font-size: 2em; - margin-right: 0.5em; - position: absolute; - top: 6px; - left: 10px; -} - -div.message-card.warning:before { - content: "\f071"; -} - -div.message-card.info:before { - content: "\f05a"; -} - - -.video-label.floating3d { - text-transform: uppercase; - display: block; - color: #FFFFFF; - text-shadow: 0 1px 0 #CCCCCC, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 10px 10px rgba(0,0,0,.2), 0 20px 20px rgba(0,0,0,.15); - color: #FFFFFF; - animation-name: floating; - animation-duration: 5s; - animation-iteration-count: infinite; - animation-timing-function: ease-in-out; - width: 100%; - font-size: 5em; - font-weight:bold; - text-align: center; - bottom: 4vh; - position: absolute; -} - -.director-link-icons { - font-size: 1.5em; - float: left; - bottom: 4px; - position: relative; - margin-right: 9px; -} - -.switch { - position: relative; - margin:5px 5px 2px 5px; - width: 40px; - height: 24px; - bottom:20px; - border-radius: 2px; - display: inline-block; -} - -.switch input { - width: 0; - height: 0; - opacity: 0; -} - -.slider { - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - -webkit-transition: .3s; - transition: .3s; - position: absolute; -} - -.slider:before { - content: ""; - height: 17px; - width: 17px; - left: 3px; - bottom: 3px; - background-color: white; - -webkit-transition: .3s; - transition: .3s; - position: absolute; -} - -input:checked + .slider { - background-color: #86b98f; -} - -input:focus + .slider { - box-shadow: 0 0 1px #86b98f; -} - -input:checked + .slider:before { - -webkit-transform: translateX(16px); - -ms-transform: translateX(16px); - transform: translateX(16px); -} -#remoteOBSControl { - user-select: none; - z-index: 25; - max-height: calc(100vh - 116px); - overflow-y: auto; -} - -#remoteOBSControl button { - margin: 5px; - padding:10px; -} - -.fullscreenOBSControl #remoteOBSControl { - width: 100%; - height: 100%; - flex-direction: row; - flex-wrap: nowrap; - align-content: center; - justify-content: center; - align-items: center; - max-height: 100%; -} - -.fullscreenOBSControl #remoteOBSControl .promptModalInner { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - flex-wrap: wrap; - align-content: center; - justify-content: center; - align-items: center; -} - -.fullscreenOBSControl #remoteOBSControl .modalClose { - display: none; -} - -.fullscreenOBSControl #header { - display: none; -} - - -.darktheme #promptModal, .darktheme .customModelPopup, .darktheme .promptModal { - background-color: var(--discord-grey-5); - color: var(--discord-text); -} - -#promptModal, .customModelPopup, .promptModal { - position: absolute; - background-color: rgb(221 221 221); - box-shadow: 0 0 30px 10px #0000005c; - color: black; - font-size: 1.0em; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border-radius: 10px; - font-weight: bold; - z-index:31; - width:550px; - max-width:100%; - overflow: hidden; - overflow-wrap: break-word; -} - -#bufferSettings{ - position: absolute; - background-color: #ddddddee; - box-shadow: 0 0 30px 10px #0000005c; - color: black; - font-size: 1.0em; - bottom: 5%; - left: 50%; - transform: translate(-50%, 0%); - border-radius: 10px; - font-weight: bold; - z-index:31; - width:550px; - max-width:100%; - overflow: hidden; - overflow-wrap: break-word; -} - -/* PTZ Controls Modal */ -#ptzControlsModal { - position: absolute; - background-color: #ddddddee; - box-shadow: 0 0 30px 10px #0000005c; - color: black; - font-size: 1.0em; - bottom: 5%; - left: 50%; - transform: translate(-50%, 0%); - border-radius: 10px; - font-weight: bold; - z-index: 31; - width: 380px; - max-width: 95%; - overflow: hidden; - overflow-wrap: break-word; -} -.darktheme #ptzControlsModal { - background-color: var(--discord-grey-1); - color: var(--discord-text); -} -.ptz-control-group { - margin: 12px 0; - display: flex; - align-items: center; - gap: 10px; -} -.ptz-control-group label { - min-width: 45px; - text-align: right; -} -.ptz-slider { - flex: 1; - min-width: 150px; -} -.ptz-value { - min-width: 40px; - text-align: left; - font-weight: normal; -} -.ptz-shortcuts { - margin-top: 15px; - padding-top: 12px; - border-top: 1px solid #aaa; - font-size: 0.85em; - font-weight: normal; -} -.ptz-shortcuts p { - margin: 6px 0; -} -.ptz-shortcuts kbd { - background-color: #eee; - border: 1px solid #ccc; - border-radius: 3px; - padding: 2px 5px; - font-family: monospace; - font-size: 0.9em; -} -.darktheme .ptz-shortcuts { - border-top-color: var(--discord-grey-3); -} -.darktheme .ptz-shortcuts kbd { - background-color: var(--discord-grey-2); - border-color: var(--discord-grey-3); - color: var(--discord-text); -} - -#publishSettings{ - position: absolute; - background-color: #ddddddee; - box-shadow: 0 0 30px 10px #0000005c; - color: black; - font-size: 1.0em; - bottom: calc(50% - 130px); - left: 50%; - transform: translate(-50%, 0%); - border-radius: 10px; - font-weight: bold; - z-index:31; - width:680px; - max-width:100%; - overflow: hidden; - overflow-wrap: break-word; -} -.largeTextEntry { - width: 90%; - margin: 10px 5%; - font-size: .8em; - padding: 0.4em; - display: block; - -} -.promptModalInner { - position: relative; - padding: 1em; - width: 100%; -} - -.promptModalMessage { - position: relative; - display: block; - width: 93%; - margin: 0 5%; -} - -#iframe_source{ - width: 100%; - height: 100%; - margin: auto; - border: 10px dashed rgb(64 65 62) -} - -iframe.insecure { - border: 10px dashed rgb(64 65 62)!important; -} -.effects-controls { - margin: 1rem 0; -} - -.zoom-control-group { - margin: 1rem 0; - display: flex; - align-items: center; - gap: 1rem; -} - -.zoom-slider, -.effect-slider { - flex: 1; - min-width: 200px; - max-width: 350px; -} - -.zoom-value { - min-width: 3rem; - text-align: right; -} - -.effect-selector-group { - margin: 1rem 0; -} -.startupWarning{ - max-width:100%; - display: block; - width: 463px; - border-left: 4px solid #eff150; - background: #fffded; - padding: 10px; - align-items: center; - position: relative; - margin: 17px auto 20px auto; - box-shadow: 0px 5px 10px -5px #a9a9a9; - text-align: left; -} -.startupWarning > p { - text-align: left; - display: inline-block; - padding-left: 38px; - -} -.startupWarning > i { - position: absolute; - font-size: 2em; - padding: 2px 0 0 0; -} - -.darktheme .startupWarning{ - background: black!important; - box-shadow: 0px 5px 10px -5px #a5a566; - color: white!important; -} - -.container-inner>div, .container-inner>span>div, .container-inner button:not(.gowebcam), .message-card { - border-radius: 5px; - box-shadow: 10px 8px 32px -10px #8883; -} - -.cameraTip { - width: 100%; - display: block; - border-left: 4px solid #50cef1; - background: #dbf0ff; - padding: 10px; - align-items: center; - position: relative; - margin: 8px auto 0px auto; - box-shadow: 0px 5px 10px -5px #a9a9a9; - text-align: left; - font-size: 97%; - white-space:normal; - min-height: 44px; - border-radius: 5px; -} -.cameraTip > p { - text-align: left; - display: inline-block; - padding-left: 32px; - vertical-align: middle; - -} -.cameraTip > i { - position: absolute; - font-size: 1.5em; - padding: 2px 0 0 0; -} - -#consentWarning{ - margin: 0 auto 20px auto; -} - -#consentWarning2{ - margin: 0px auto 10px auto; -} - -#alertModal { - position: absolute; - background-color: rgb(221 221 221); - box-shadow: 0 0 30px 10px #0000005c; - color: black; - font-size: 1.2em; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - border-radius: 10px; - font-weight: bold; - z-index:32; - overflow-wrap: break-word; - min-width: min(90vw, 320px); -} - -#connectUsers{ - float: right; - display: none; - position: absolute; - max-width: 400px; - min-width: 150px; - max-height: 80%; - background-color: #08090e; - z-index: 5; - padding: 10px; - right: 20px; - bottom: 120px; - box-shadow: 2px 2px #313131; - border-radius: 5px; - border: 1px solid #252525; - opacity: 0.7; - color: white; -} - -#alertModal a:link { - color: blue; -} - -#alertModal a:visited { - color: blue; -} - -#alertModal a:hover { - color: blue; -} - - #alertModal a:active { - color: blue; -} - -.alertModalInner { - position: relative; - padding: 2em; - user-select: none; -} - -.modalClose { - position: absolute; - top: -4px; - right: 4px; - cursor: pointer; - font-weight: bolder; - font-size: 1.8em; -} - -#modalBackdrop { - background: var(--background-color); - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 0; - opacity: 0.8; -} -#modalBackdrop.darktheme{ - background-color: var(--dark-background-color); -} - -.modalBackdrop { - background: var(--background-color); - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 0; - opacity: 0.8; -} - -.modalBackdrop.darktheme{ - background-color: var(--dark-background-color); -} - -.opaqueBackdrop{ - background: var(--background-color); - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 0; - opacity: 1.0; -} -.opaqueBackdrop.darktheme{ - background-color: var(--dark-background-color); -} - -.alertModalMessage>select{ - font-size: 100%; -} - -.iframeDetails { - margin: 10px; - position: relative; - word-break: break-all; - max-height: 500px; - overflow: hidden; -} -.desktop-capturer-selection { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100vh; - background: rgba(30,30,30,.75); - color: #fff; - z-index: 10000000; - display: flex; - align-items: center; - justify-content: center; -} -.desktop-capturer-selection__scroller { - width: 100%; - max-height: 100vh; - overflow-y: auto; -} -.desktop-capturer-selection__list { - max-width: calc(100% - 100px); - margin: 50px; - padding: 0; - display: flex; - flex-wrap: wrap; - list-style: none; - overflow: hidden; - justify-content: center; -} -.desktop-capturer-selection__item { - display: flex; - margin: 4px; -} -.desktop-capturer-selection__btn { - display: flex; - flex-direction: column; - align-items: stretch; - width: 145px; - margin: 0; - border: 0; - border-radius: 3px; - padding: 4px; - background: #252626; - text-align: left; - transition: background-color .15s, box-shadow .15s; -} -.desktop-capturer-selection__btn:hover, -.desktop-capturer-selection__btn:focus { - background: rgba(98,100,167,.8); -} -.desktop-capturer-selection__thumbnail { - width: 100%; - height: 81px; - object-fit: cover; -} -.desktop-capturer-selection__name { - margin: 6px 0 6px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -} - -/* New styles for audio capture checkboxes */ -.desktop-capturer-selection__audio-option { - margin-top: 5px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; -} -.desktop-capturer-selection__audio-option label { - display: flex; - align-items: center; - cursor: pointer; - padding: 4px; - border-radius: 3px; - background: rgba(60,60,60,0.5); -} -.desktop-capturer-selection__audio-option label:hover { - background: rgba(80,80,80,0.7); -} -.desktop-capturer-selection__audio-option input { - margin-right: 5px; - width: 14px; - height: 14px; -} -.desktop-capturer-selection__audio-option span { - display: flex; - align-items: center; -} -.desktop-capturer-selection__audio-option i { - margin-right: 3px; - font-size: 14px; -} -.video-zoom-slider0 { - position: absolute; - bottom: 45px; - left: 10px; - right: 10px; - height: 40px; - background: rgba(0,0,0,0.5); - display: flex; - align-items: center; - padding: 0 15px; - opacity: 0.3; - transition: opacity 0.3s; - z-index: 5; -} - -.video-zoom-slider0:hover, -.video-container:hover .video-zoom-slider { - opacity: 1; -} - -.video-zoom-slider0 input[type="range"] { - width: 100%; - height: 6px; - -webkit-appearance: none; - background: rgba(255,255,255,0.2); - border-radius: 3px; - cursor: pointer; -} - -.video-zoom-slider0 input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 20px; - height: 20px; - background: white; - border-radius: 50%; -} - -.video-zoom-slider0 input[type="range"]::-moz-range-thumb { - width: 20px; - height: 20px; - background: white; - border: none; - border-radius: 50%; - cursor: pointer; -} - -.video-ptz-controls { - position: absolute; - bottom: 45px; - left: 10px; - right: 10px; - background: rgba(0,0,0,0.5); - display: flex; - flex-direction: column; - padding: 10px 15px; - opacity: 0.3; - transition: opacity 0.3s; - z-index: 5; -} - -.video-ptz-controls:hover, -.video-container:hover .video-ptz-controls { - opacity: 1; -} - -.video-zoom-slider, -.video-pan-slider, -.video-tilt-slider { - display: flex; - align-items: center; - margin: 5px 0; - width: 100%; -} - -.video-ptz-controls label { - color: white; - font-size: 12px; - margin-right: 10px; - min-width: 40px; -} - -.video-ptz-controls input[type="range"] { - flex: 1; - height: 6px; - -webkit-appearance: none; - background: rgba(255,255,255,0.2); - border-radius: 3px; - cursor: pointer; -} - -.video-ptz-controls input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 20px; - height: 20px; - background: white; - border-radius: 50%; -} - -.video-ptz-controls input[type="range"]::-moz-range-thumb { - width: 20px; - height: 20px; - background: white; - border: none; - border-radius: 50%; - cursor: pointer; -} - -.makeSmallerDirectorRoom{ - max-width: calc(100% - 415px); - min-width: min(calc(100% - 395px), 75%); -} -.pinToSide{ - top: unset; - left: unset; - right: 0; - transform: unset; - border-radius: unset; - height: 100%; - max-width: min(25%, 415px); - min-width: 395px; - overflow-y: auto; - position: fixed; -} -#roomSettings{ - max-height: 100%; - overflow: auto; -} - -body.darktheme { - color: white; - scrollbar-color: #000 #333; -} -body.darktheme form>label{ - color: white; -} -body.darktheme .column>h2{ - color: #b6b6b6; -} -body.darktheme .column { - border-color: rgba(255, 255, 255, 0.1); -} -body.darktheme .column:hover { - border-color: rgba(255, 255, 255, 0.15); -} -body.darktheme .directorsgrid .vidcon > .las { - background-color: #424242; -} -body.darktheme h2 { - color: #DDD; -} -body.darktheme .column .las { - color: var(--discord-text); -} -body.darktheme label { - color: var(--discord-text); -} -body.darktheme #roomHeader{ - color: var(--discord-text); -} -body.darktheme div.multiselect { - background-color: var(--discord-grey-7); -} -body.darktheme .audioMenu{ - background-color: var(--discord-grey-7); -} -body.darktheme #avatarDiv{ - background-color: var(--discord-grey-7); -} -body.darktheme .selected { - border: solid 3px #f8f7f7; -} -body.darktheme #selectAvatarImage label { - color: #f8f7f7; -} -body.darktheme .avatarLabel { - color: #f8f7f7; -} -body.darktheme .directorsgrid .vidcon { - background-color: var(--discord-grey-3); -} -body.darktheme .promptModal { - background-color: var(--discord-grey-5); - color: var(--discord-text); -} -body.darktheme .infoblob{ - color: #CCC; -} -body.darktheme .outMessage{ - background-color: #7f89a7; -} -body.darktheme .inMessage { - background-color: #7e7d7d; -} -body.darktheme .actionMessage { - background-color: #b1b1b1; -} -body.darktheme #chatInput{ - background-color: var(--discord-grey-7); - color: var(--discord-text) -} -body.darktheme .alertModal{ - background-color: #ccc; - filter:brightness(0.85); -} -body.darktheme .directorContainer{ - filter: brightness(0.85); -} -body.darktheme .cameraTip{ - background-color: #27354b; - color: #e5dbdb; -} -.containerGreen{ - background-color: #649166!important; -} -body.darktheme .containerGreen{ - background-color: #243824!important; -} -body.darktheme .startupWarning>.las { - color:white!important; -} - -/* -//////////////////////////////////////////// -// INTERMEDIATE STYLING - PRE FLEX-SWITCH // -//////////////////////////////////////////// -*/ - -button { - background-color: var(--lighttheme-2); - color: var(--lighttheme-text); - filter: brightness(1); - display: inline-flex; - align-items: center; - justify-content: center; - user-select: none; - cursor: pointer; - padding: 4px 6px; - border-radius: 4px; - border: 1px solid var(--lighttheme-6); -} -button:hover { - filter: brightness(0.98); -} -button i { - font-size: 130%; -} -.darktheme :not(.promptModalInner) > button { - background-color: var(--discord-grey-5); - border: 1px solid var(--discord-grey-8); - color: var(--discord-text); -} -.darktheme :not(.promptModalInner) > button:hover { - filter: brightness(1.05); -} - -.gobutton { - font-size: 110%; - padding: 10px 15px; - border: 2px solid #7ea8c8; - cursor: pointer; - background-color: #99bfd9; - color: #000; - font-weight: 700; - border-radius: 6px; - transition: all 0.15s ease-in-out; -} -.gobutton:hover { - background-color: #8bb2d4; - border-color: #6d9bbb; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} -.gobutton:active { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} -.darktheme .gobutton { - background-color: #6B8698; - color: #000; - border: 2px solid #556a7a; -} -.darktheme .gobutton:hover { - background-color: #7792a5; - border-color: #4a5d6c; -} - -.chevron::before { - border-style: solid; - border-width: 0.14em 0.14em 0 0; - content: ''; - display: inline-block; - height: 0.32em; - transform: rotate(-45deg); - width: 0.32em; - margin: 0 7px 0 3px; -} - -.chevron.bottom::before { - transform: rotate(135deg); - bottom: 3px; - position: relative; -} - -.chevron.right::before { - transform: rotate(45deg); -} -#chevarrow4::before ,#chevarrow3::before { - position:relative; - bottom:1px; -} -#chevarrow4.bottom::before ,#chevarrow3.bottom::before { - bottom:2px; -} - -button.white { - padding: 6px 10px 4px 9px; - cursor: pointer; - border-radius: 2px; - background-color: #fff; - color: #000; - border: 1px solid #000; -} - -select { - background-color: var(--lighttheme-1); - color: var(--lighttheme-text); - border-radius: 4px; - font-size: 16px; - padding: 4px 6px; - outline: 0; - cursor: pointer; - font-size:95%; - text-overflow: ellipsis; - max-width: 100%; - border: 1px solid var(--lighttheme-6); - transition: border-color 0.15s ease-in-out; -} -select:hover { - border-color: var(--lighttheme-5); -} -select:focus { - border-color: #99bfd9; -} -.darktheme select { - background-color: var(--light-grey); - border: 1px solid var(--discord-grey-8); - color: var(--near-black); - border-radius: 4px; -} -.darktheme ul { - /* background-color: var(--discord-grey-3); - border: 1px solid var(--discord-grey-8); - color: var(--discord-text);*/ -} -ul { - /* background-color: var(--lighttheme-1); - border: 1px solid var(--lighttheme-6); - color: var(--lighttheme-text);;*/ - border-radius: 4px; -} - -li { - margin: 0.1em 0; - padding-left: 0.1em; - line-height: 1.3em; -} - -input[type='text'], input[type='password'] { - background-color: var(--lighttheme-1); - border: 1px solid var(--lighttheme-6); - color: var(--lighttheme-text); - border-radius: 4px; - padding: 6px 8px; - height: 30px; - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -input[type='text']:focus, input[type='password']:focus { - outline: none; - border-color: #99bfd9; - box-shadow: 0 0 0 2px rgba(153, 191, 217, 0.2); -} -input::placeholder { - color: var(--lighttheme-4) !important; -} -input[type='checkbox'], input[type='radio'] { - -ms-transform: scale(1.4); - /* IE */ - -moz-transform: scale(1.4); - /* FF */ - -webkit-transform: scale(1.4); - /* Safari and Chrome */ - -o-transform: scale(1.4); - /* Opera */ - transform: scale(1.4); - padding: 5px; - margin: 3px; - - cursor: pointer; -} - -.darktheme :not(.promptModalInner) > input:not([type='range']) { - border: 1px solid var(--discord-grey-8); - border-radius: 4px; -} - -.darktheme :not(.promptModalInner) > input::placeholder { - color: var(--discord-grey-8) !important; -} - -.darktheme .card { - background-color: var(--discord-grey-7); -} - -.card { - background-color: var(--lighttheme-3); -} - -.darktheme .container-inner { - background-color: var(--discord-grey-7); -} - -.container-inner { - display: none; - background-color: var(--lighttheme-3); - min-height: calc(100% - 100px); - margin-bottom:30px; -} - -.title { - text-align: left; - user-select: none; - display: inline-block; -} - -.darktheme #addPasswordBasic, -.darktheme #avatarDiv, -.darktheme #avatarDiv2, -.darktheme #avatarDiv3, -.darktheme #audioScreenShare1, -.darktheme #audioMenu, -.darktheme #audioMenu2, -.darktheme #effectsDiv, -.darktheme #effectsDiv2, -.darktheme #effectsDiv3, -.darktheme #grabDirectorSoloLinkParent, -.darktheme #videoMenu, -.darktheme #videoMenu2, -.darktheme #videoMenu3, -.darktheme #headphonesDiv, -.darktheme #headphonesDiv2, -.darktheme #headphonesDiv3, -.darktheme #videoSettings, -.darktheme #videoSettings2, -.darktheme #popupSelector_user_settings, -.darktheme .invite_setting_group { - background-color: var(--discord-grey-5); - border: 1px solid var(--discord-grey-8); -} -.darktheme .largeDarkIcon{ - color: var(--dark-background-color)!important; -} -#addPasswordBasic, -#avatarDiv, -#avatarDiv2, -#avatarDiv3, -#audioScreenShare1, -#audioMenu, -#audioMenu2, -#effectsDiv, -#effectsDiv2, -#effectsDiv3, -#grabDirectorSoloLinkParent, -#videoMenu, -#videoMenu2, -#videoMenu3, -#headphonesDiv, -#headphonesDiv2, -#headphonesDiv3, -#videoSettings, -#videoSettings2, -#popupSelector_user_settings, -.invite_setting_group { - display: inline-block; - margin-top: 15px; - width: 463px; - max-width: 100%; - padding: 10px; - border-radius: 4px; - text-align: left; -} -#addPasswordBasic, -#avatarDiv, -#avatarDiv2, -#avatarDiv3, -#audioScreenShare1, -#audioMenu, -#audioMenu2, -#effectsDiv, -#effectsDiv2, -#effectsDiv3, -#grabDirectorSoloLinkParent, -#videoMenu, -#videoMenu2, -#videoMenu3, -#headphonesDiv, -#headphonesDiv2, -#headphonesDiv3, -#videoSettings, -#videoSettings2, -.invite_setting_group { - background-color: var(--lighttheme-2); - border: 1px solid var(--lighttheme-4); -} - -#videoSettings, #videoSettings2 { - width: 463px; - padding: 10px; - margin-top: -1px; - border-radius: 0px 0px 4px 4px; - text-align: center; -} -#videoSettings2{ - margin-top: 10px; - font-size: 95%; -} -#videoMenu .title{ - display: inline-block; - padding:0; -} -#videoMenu { - padding: 10px 10px 9px 10px; - vertical-align: middle; - text-align: left; - margin-top: 0px; -} -.disable { - opacity: 0.5; - cursor: not-allowed; -} - -.darktheme .invite_setting_group { - color: var(--discord-text); -} - -.invite_setting_group { - color: var(--lighttheme-text); -} - -.invite_setting_item { - margin: 10px 0px 0px 0; -} - -.invite_setting_group_links { - margin-top: 10px; -} - -#popupSelector { - background: linear-gradient(6deg, rgba(221, 221, 221, 0) 4%, rgb(0,0,0, 0.5) 30%, #646878A0 100%); - transition: all 0.2s linear 0s; - padding: 10px; - position: fixed; - top: 0px; - height: 100%; - width: 505px; - max-width: 100%; - right: -500px; - overflow: auto; - z-index: 4; - padding-bottom: 80px; -} -#popupSelector label { - color: black; -} -.darktheme #popupSelector label { - color: white; -} -#popupSelector .multiselect-contents label{ - color: black; -} -#popupSelector select { - padding: 4px; -} -.darktheme #popupSelector{ - background-color: #0004; -} -.popupSelector_constraints{ - margin-top: 10px; -} - -.popupSelector_constraints input[type='range'] { - margin-bottom: 10px; -} - -#popupSelector_constraints_video > div, #popupSelector_constraints_audio > div { - padding: 10px 10px 10px 0; -} -#popupSelector_constraints_audio label { - color:white; -} -#popupSelector_constraints_video label { - color:white; -} -#popupSelector_user_settings label { - color:white; -} -ul#audioSource{ - background-color: var(--lighttheme-1); - margin-top: 7px; - min-height: 24px; -} -ul#audioSource label{ - background-color: var(--lighttheme-1); - border: 0; -} -ul#audioSource label{ - color:black; -} -ul#audioSource3{ - background-color: var(--lighttheme-1); - border: 1px solid var(--lighttheme-6); -} -.darktheme ul#audioSource{ - background-color: var(--light-grey); - margin-top: 7px; -} -.darktheme ul#audioSource label{ - background-color: var(--light-grey); - border: 0; -} -.darktheme ul#audioSource3{ - background-color: var(--light-grey); - border: 1px solid var(--discord-grey-8); -} - -.darktheme #grabDirectorSoloLink { - background-color: var(--discord-grey-3); - border: 1px solid var(--discord-grey-8); -} -#refreshVideoButton > i{ - cursor: pointer; - font-size: 120%; - position: relative; - top: 3px; -} -#webcamquality3{ - margin-top: 10px; -} -#grabDirectorSoloLink { - display: inline-block; - font-size: 16px; - padding: 2px 6px; - width: 100%; - outline: 0; - border-radius: 4px; - background-color: var(--lighttheme-1); - border: 1px solid var(--lighttheme-6); -} - -#grabDirectorSoloLinkParent { - margin-bottom: 10px; -} - -/* INITIALLY HIDDEN */ -#advancedOptionsAudio, -#advancedOptionsCamera, -#effectsDiv { - display: none; -} -#advancedOptionsAudio, -#advancedOptionsCamera, -#advancedOptionsGeneral, -button.toggleSettings, -#effectsDiv { - padding: 10px; -} -#videoname1, #passwordRoom { - height:unset; -} - -#audioMenu ul { - list-style:none; -} - -.generalButton{ - border-radius: 3px; - padding: 5px; - max-width: 216px; - margin: 5px; -} - -.roomnotes { - margin-top: 10px; -} -.randomRoomName{ - position: absolute; - margin: 3px; -} -.randomRoomName:active{ - animation: shake 0.2s; - animation-iteration-count: once; -} -.randomRoomName:hover{ - -webkit-box-shadow: 0px 0px 4px #000; -} - -.pressed { - background: #1e0000 !important; - -webkit-box-shadow: inset 0px 0px 1px #b90000; - -moz-box-shadow: inset 0px 0px 1px #b90000; - box-shadow: inset 0px 0px 1px #b90000; - outline: none; - color: white; -} -/* ANIMATIONS */ - -/* Shake animation */ -.shake { - animation: shake 0.5s; - animation-iteration-count: once; -} -@keyframes shake { - 0% { transform: translate(1px, 1px) rotate(0deg); } - 10% { transform: translate(-1px, -2px) rotate(-1deg); } - 20% { transform: translate(-3px, 0px) rotate(1deg); } - 30% { transform: translate(3px, 2px) rotate(0deg); } - 40% { transform: translate(1px, -1px) rotate(1deg); } - 50% { transform: translate(-1px, 2px) rotate(-1deg); } - 60% { transform: translate(-3px, 1px) rotate(0deg); } - 70% { transform: translate(3px, 1px) rotate(-1deg); } - 80% { transform: translate(-1px, -1px) rotate(1deg); } - 90% { transform: translate(1px, 2px) rotate(0deg); } - 100% { transform: translate(1px, -2px) rotate(-1deg); } -} - - -/* Spin animation */ -@keyframes spin-animation { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(359deg); - } -} - - -/* Flip 180 animations */ -/* NOTE: At least used by the flip camera button in control-bar on mobile */ -.flip { - animation: flip180 2s; - animation-iteration-count: 1; -} -@keyframes flip180 { - 0% {transform: rotate(0);} - 100% {transform: rotate(180deg);} -} - -.flip2 { - animation: flip1802 2s; - animation-iteration-count: 1; -} -@keyframes flip1802 { - 0% {transform: rotate(180deg)} - 100% {transform: rotate(360deg);} -} - - -/* Blink-warn / Blink-alert animations */ -/* NOTE: At least used by .battery */ -@keyframes blink-warn { - 0% { opacity: 0; } - 50% { opacity: 1; } - 100% { opacity: 0; } -} -@keyframes blink-alert { - 0% { opacity: 0; } - 50% { opacity: 1; } - 100% { opacity: 0; } -} - - -/* Floating animation */ -/* NOTE: At least used by .video-label */ -@keyframes floating { - 0% { transform: translate(0, 0px); } - 50% { transform: translate(0, 15%); } - 100% { transform: translate(0, -0px); } -} - - -/* Pulsating animations */ -/* NOTE: .pulsate is at least used by #mutetoggle */ -.pulsate { - box-shadow: 0 0 0 0 rgba(14, 19, 26, 1); - transform: scale(1); - animation: pulse 2s infinite; -} -@keyframes pulse { - 0% { - transform: scale(1); - box-shadow: 0 0 0 0 rgba(14, 19, 26, 0.7); - } - 15% { - transform: scale(1.2); - box-shadow: 0 0 0 10px rgba(2, 3, 4, 0); - } - 50% { - transform: scale(1.0); - box-shadow: 0 0 0 0 rgba(14, 19, 26, 0); - } - 85% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(14, 19, 26, 0); - } - 100% { - transform: scale(1); - box-shadow: 0 0 0 0 rgba(14, 19, 26, 0); - } -} -@keyframes pulsate { - 0% { box-shadow: 0 0 31px #244e1c44; transform: scale(1.0);} - 50% { box-shadow: 0 0 17px #0004; transform: scale(0.99);} - 100% { box-shadow: 0 0 31px #244e1c44; transform: scale(1.0);} -} -@-webkit-keyframes pulsate { - 0% { box-shadow: 0 0 31px #244e1c44; transform: scale(1.0);} - 50% { box-shadow: 0 0 17px #0004; transform: scale(0.99);} - 100% { box-shadow: 0 0 31px #244e1c44; transform: scale(1.0);} -} - - -/* Lightbox open animation */ -/* NOTE: For opening lightboxes on homepage, and a sidenote, the "outlightbox" equivalent for closeing - the lightboxes is found in lib.js */ -@keyframes inlightbox { - 50% { - width: 100%; - left: 0; - height: 200px; - } - - 100% { - height: 100%; - width: 100%; - top: 0; - left: 0; - } -} - - -/* Fade-in animation */ -.fadein { - animation: fadeIn var(--fadein-speed); - -webkit-animation: fadeIn var(--fadein-speed); - -moz-animation: fadeIn var(--fadein-speed); - -o-animation: fadeIn var(--fadein-speed); - -ms-animation: fadeIn var(--fadein-speed); - animation-iteration-count: 1; -} -@keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} -@-moz-keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} -@-webkit-keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} -@-o-keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} -@-ms-keyframes fadeIn { - 0% {opacity:0;} - 100% {opacity:1;} -} - - -/* Fade-out animation */ -.fadeout { - animation: fadeout 1s; - opacity: 0!important; -} -.partialFadeout{ - opacity: 0.2 !important; -} -@keyframes fadeout { - 0% { - opacity: 1 - } - 100% { - opacity: 0 - } -} - - -/* Greyout animation */ -.greyout { - animation: greyout 3s; - opacity: 0.3!important; -} -@keyframes greyout { - 0% { - opacity: 1 - } - 100% { - opacity: 0.3 - } -} - - -/** - * Below lsits the classes and font styles for the font-awesome icons. - */ - -@font-face { - font-family: 'Line Awesome Free'; - font-style: normal; - font-weight: 900; - font-display: auto; - src: url("lineawesome/fonts/la-solid-900.eot"); - src: url("lineawesome/fonts/la-solid-900.eot?#iefix") format("embedded-opentype"), url("lineawesome/fonts/la-solid-900.woff2") format("woff2"), url("lineawesome/fonts/la-solid-900.woff") format("woff"), url("lineawesome/fonts/la-solid-900.ttf") format("truetype"), url("lineawesome/fonts/la-solid-900.svg#lineawesome") format("svg"); -} -.las { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - display: inline-block; - font-style: normal; - font-variant: normal; - text-rendering: auto; - line-height: 1; } -.las { - font-family: 'Line Awesome Free'; - font-weight: 900; } -.la-bell:before { - content: "\f0f3"; } -.la-bell-slash:before { - content: "\f1f6"; } -.la-long-arrow-alt-right:before { - content: "\f30b"; } -.la-paperclip:before { - content: "\f0c6"; } -.la-window-close:before { - content: "\f410"; } -.la-stream:before { - content: "\f550"; } -.la-file-upload:before { - content: "\f574"; } -.la-comment-alt:before { - content: "\f27a"; } -.la-tv:before { - content: "\f26c"; } -.la-volume-up:before { - content: "\f028"; } -.la-comment-dots:before { - content: "\f4ad"; } -.la-microphone:before { - content: "\f130"; } -.la-microphone-alt:before { - content: "\f3c9"; } -.la-video:before { - content: "\f03d"; } -.la-desktop:before { - content: "\f108"; } -.la-tv:before { - content: "\f26c"; } -.la-window-maximize:before { - content: "\f2d0"; } -.la-sync-alt:before { - content: "\f2f1"; } -.la-users-cog:before { - content: "\f509"; } -.la-cog:before { - content: "\f013"; } -.la-phone:before { - content: "\f095"; } -.la-gamepad:before { - content: "\f11b"; } -.la-user-slash:before { - content: "\f506"; } -.la-skull-crossbones:before { - content: "\f714"; } -.la-hand-paper:before { - content: "\f256"; } -.la-phone-slash:before { - content: "\f3dd;"; } -.la-dot-circle:before { - content: "\f192"; } -.la-bug:before { - content: "\f188"; } -.la-question-circle:before { - content: "\f059"; } -.la-language:before { - content: "\f1ab"; } -.la-calendar:before { - content: "\f073"; } -.la-exclamation-circle:before { - content: "\f06a"; } -.la-plug:before { - content: "\f1e6"; } -.la-ethernet:before { - content: "\f796"; } -.la-headphones:before { - content: "\f025"; } -.la-robot:before { - content: "\f544"; } -.la-info-circle:before { - content: "\f05a"; } -.la-play:before { - content: "\f04b"; } -.la-gamepad:before { - content: "\f11b"; } -.la-file-video:before { - content: "\f1c8"; } -.la-blender:before { - content: "\f517"; } -.la-heartbeat:before { - content: "\f21e"; } -.la-code-branch:before { - content: "\f126"; } -.la-info:before { - content: "\f129"; } -.la-square:before { - content: "\f0c8"; } -.la-play-circle:before { - content: "\f144"; } -.la.la-hdd-o:before { - content: "\f0a0"; } -.la-key:before { - content: "\f084"; } -.la-broadcast-tower:before { - content: "\f519"; } -.la-clock:before { - content: "\f017"; } -.la-tachometer-alt:before { - content: "\f3fd"; } -.la-fire-alt:before { - content: "\f7e4"; } -.la-book-open:before { - content: "\f518"; } -.la-caret-down:before { - content: "\f0d7"; } -.la-comments:before { - content: "\f086"; } -.la-caret-right:before { - content: "\f0da"; } -.la-copy:before { - content: "\f0c5"; } -.la-tools:before { - content: "\f7d9"; } -.la-th-large:before { - content: "\f009"; } -.la-user-circle:before { - content: "\f2bd"; } -.la-paper-plane:before { - content: "\f1d8"; } -.la-envelope:before { - content: "\f0e0"; } -.la-sign-out-alt:before { - content: "\f2f5"; } -.la-angle-right:before { - content: "\f105"; } -.la-angle-left:before { - content: "\f104"; } -.la-external-link-square-alt:before { - content: "\f360"; } -.la-plus-square:before { - content: "\f0fe"; } -.la-microphone-slash:before { - content: "\f131"; } -.la-user:before { - content: "\f007"; } -.la-video-slash:before { - content: "\f4e2"; } -.la-volume-off:before { - content: "\f026"; } -.la-eye-slash:before { - content: "\f070"; } -.la-eye:before { - content: "\f06e"; } -.la-minus:before { - content: "\f068"; } -.la-minus-circle:before { - content: "\f056"; } -.la-window-minimize:before { - content: "\f2d1"; } -.la-hat-wizard:before { - content: "\f6e8"; } -.la-plus:before { - content: "\f067"; } -.la-sync:before { - content: "\f021"; } -.la-circle:before { - content: "\f111"; } -.la-chevron-left:before { - content: "\f053"; } -.la-chevron-right:before { - content: "\f054"; } -.la-binoculars:before { - content: "\f1e5"; } -.la-user-cog:before { - content: "\f4fe"; } -.la-stop-circle:before { - content: "\f28d"; } -.la-redo-alt:before { - content: "\f2f9"; } -.la-sliders-h:before { - content: "\f1de"; } -.la-compress-arrows-alt:before { - content: "\f78c"; } -.la-users:before { - content: "\f0c0"; } -.la-spinner:before { - content: "\f110"; } -.la-external-link:before { - content: "\f35d"; } -.la-pen:before { - content: "\f304"; } -.la-external-link-alt:before { - content: "\f35d"; } -.la-times:before { - content: "\f00d"; } -.la-volume-mute:before { - content: "\f6a9"; } -.la-plug:before { - content: "\f1e6"; } -.la-reply:before { - content: "\f3e5"; } -.la-expand-arrows-alt:before { - content: "\f31e"; } -.la-headset:before { - content: "\f590"; } -.la-check:before { - content: "\f00c"; } -.la-exclamation:before { - content: "\f12a"; } -.la-chevron-down:before { - content: "\f078"; } -.la-music:before { - content: "\f001"; } -.la-thumbtack:before { - content: "\f08d"; } -.la-hdd:before { - content: "\f0a0"; } -.la-signal:before { - content: "\f012"; } -.la-unlock:before { - content: "\f023"; } -.la-lock-open:before { - content: "\f3c1"; } -.la-theater-masks:before { - content: "\f630"; } -.la-compact-disc:before { - content: "\f51f"; } -.la-random:before { - content: "\f074"; } -.la-moon:before { - content: "\f186"; } -.la-mobile:before { - content: "\f10b"; } -.la-podcast:before { - content: "\f2ce"; } -.la-chalkboard:before { - content: "\f51b"; } -.la-project-diagram:before { - content: "\f542"; } -.la-qrcode:before { - content: "\f029"; } -.la-times-circle:before { - content: "\f057"; } -.la-sun:before { - content: "\f185"; } - - -/* Minimal visual refresh overrides — non-invasive, CSS-only. */ -/* CHANGES BELOW MADE 2025-08-22 */ - -/* 1) Palette and radii overrides via existing tokens */ -:root { - --button-radius: 6px; - --lighttheme-2: #f6f7f9; /* surfaces */ - --lighttheme-3: #eceef2; /* containers */ - --lighttheme-4: #d9dde5; /* borders */ - --lighttheme-6: #c3c7d0; /* subtle lines */ - --lighttheme-text: #111; - - /* keep original semantics; just slightly modernize */ - --container-color: #3a3d45; /* dark surface used in many cards */ - - /* global accent (aligned with about.html) */ - --accent-color: #56a5ea; - --accent-hover-color: #7AACD2; - - /* unified link colors (light theme) — aligned with about.html accent */ - --a-link: #3e8fd8; /* base: #4299e1 softened */ - --a-visited: #3e8fd8; - --a-hover: #56a5ea; /* lighter hover */ - --a-focus: #56a5ea; - --a-active: #2f7ec4; - - /* lighter links used in specific sections */ - --a-lighter-link: #2d6aa6; - --a-lighter-visited: #2d6aa6; - --a-lighter-hover: #3e7fbd; - --a-lighter-focus: #3e7fbd; - --a-lighter-active: #246295; -} - -@media (prefers-color-scheme: dark) { - :root { - /* soften Discord greys slightly */ - --discord-grey-5: #2f3237; - --discord-grey-7: #3a3d45; - --discord-text: hsl(210 10% 92% / 1); - /* muted icon color to match subdued typography */ - --muted-icon: #c8ccd4; - /* unified link colors (dark theme) */ - --a-dark-link: #9ec7ee; /* tinted to about.html accent */ - --a-dark-visited: #9ec7ee; - --a-dark-hover: #b9d8f6; - --a-dark-focus: #b9d8f6; - --a-dark-active: #8abbe8; - --a-darker-link: #bcd8f4; - --a-darker-visited: #bcd8f4; - --a-darker-hover: #cfe6fb; - --a-darker-focus: #cfe6fb; - --a-darker-active: #a9cdef; - /* accent for dark UI */ - --accent-color: #498dc2; - --accent-hover-color: #6FA5CF; - } -} - -/* 14) SSO options panel (light/dark aware) */ -.sso-options-panel { - margin-top: 8px; - padding: 10px; - border-radius: 8px; - max-width: 470px; - background: var(--lighttheme-2); - border: 1px solid var(--lighttheme-4); - color: var(--lighttheme-text); -} - -body.darktheme .sso-options-panel { - background: var(--discord-grey-5); - border: 1px solid var(--discord-grey-7); - color: var(--discord-text); -} - -.sso-options-panel label { - color: inherit; -} - -.sso-options-input { - width: 100%; - padding: 6px 8px; - border-radius: 6px; - border: 1px solid var(--lighttheme-4); - background: #fff; - color: var(--lighttheme-text); -} - -body.darktheme .sso-options-input { - border: 1px solid var(--discord-grey-7); - background: var(--discord-grey-3); - color: var(--discord-text); -} - -.sso-options-note { - margin-top: 8px; - font-size: 12px; - color: #444; -} - -body.darktheme .sso-options-note { - color: var(--muted-icon); -} - -/* 2) Typographic refresh (safe defaults) */ -body { - font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - /* subtle depth */ - background-image: radial-gradient(1200px 800px at 10% -10%, rgba(255,255,255,0.025), transparent 60%), - radial-gradient(1000px 600px at 110% 0%, rgba(0,0,0,0.08), transparent 60%); -} - -/* 3) Cards and columns (landing containers) */ -.column.card, -.column { - /* Use solid backgrounds to preserve icon contrast and avoid transparency */ - background-color: var(--lighttheme-3) !important; - backdrop-filter: saturate(120%) blur(2px); - border: 1px solid var(--lighttheme-4); - border-radius: 14px; - box-shadow: 0 2px 10px rgba(0,0,0,0.08), 0 1px 0 rgba(255,255,255,0.02) inset; -} -.darktheme .column.card, -.darktheme .column { - background-color: var(--discord-grey-7) !important; - border: 1px solid #4a4d55; - box-shadow: 0 2px 12px rgba(0,0,0,0.35), 0 1px 0 rgba(255,255,255,0.02) inset; -} - -.column:hover { - transform: translateY(-2px); - box-shadow: 0 10px 24px rgba(0,0,0,0.15); -} - -.column { - padding: 25px 17px; -} -/* Prevent hover-lift effect when a tile is expanded full-window */ -.skip-animation, -.in-animation { - transition: none !important; -} -.skip-animation:hover, -.in-animation:hover { - transform: none !important; - box-shadow: none !important; -} - -/* 4) Container content panels */ -.container-inner { - padding: 14px 16px; - border-radius: 12px; -} -/* preserve existing backgrounds from main.css; no overrides here */ - -/* 5) Buttons and inputs (respect existing tokens) */ -button { - border-radius: var(--button-radius); - transition: box-shadow .15s ease, transform .08s ease, filter .15s ease; -} -button:hover { - box-shadow: 0 6px 14px rgba(0,0,0,0.12); -} -button:active { - transform: translateY(1px); -} - -/* Accessible focus treatment */ -:where(a, button, [role="button"], input, select, textarea):focus-visible { - outline: 2px solid var(--a-hover); - outline-offset: 2px; -} -.darktheme :where(a, button, [role="button"], input, select, textarea):focus-visible { - outline-color: var(--a-dark-hover); -} - -input[type="text"], input[type="search"], input[type="number"], select { - border: 1px solid var(--lighttheme-4); - background: var(--lighttheme-2); - color: var(--lighttheme-text); - border-radius: 10px; - padding: 6px 10px; -} -.darktheme input[type="text"], -.darktheme input[type="search"], -.darktheme input[type="number"], -.darktheme select { - border: 1px solid #4a4d55; - background: var(--discord-grey-5); - color: var(--discord-text); -} - -/* 17) Radios/checkboxes: make checked state feel active */ -input[type="radio"], -input[type="checkbox"] { - accent-color: var(--accent-color); -} -.darktheme input[type="radio"], -.darktheme input[type="checkbox"] { - accent-color: var(--accent-color); -} -input[type="radio"] + label, -input[type="checkbox"] + label { cursor: pointer; } -input[type="radio"]:checked + label, -input[type="checkbox"]:checked + label { - color: var(--a-link); - font-weight: 600; -} -/* Override: do not color labels on selection — only the control gets color */ -input[type="radio"]:checked + label, -input[type="checkbox"]:checked + label { - color: inherit !important; - font-weight: inherit !important; -} -.darktheme input[type="radio"]:checked + label, -.darktheme input[type="checkbox"]:checked + label { - color: inherit !important; -} - -/* Higher-contrast placeholders (theme-aware) */ -input::placeholder, -textarea::placeholder { - color: #6b7686 !important; /* slightly darker than default for readability */ -} -.darktheme :not(.promptModalInner) > input::placeholder, -.darktheme :not(.promptModalInner) > textarea::placeholder { - color: var(--muted-icon, #c8ccd4) !important; /* brighter on dark for contrast */ -} - -/* Vendor placeholder fallbacks */ -::-webkit-input-placeholder { color: #6b7686 !important; } -::-moz-placeholder { color: #6b7686 !important; } -:-ms-input-placeholder { color: #6b7686 !important; } -:-moz-placeholder { color: #6b7686 !important; } -.darktheme ::-webkit-input-placeholder { color: var(--muted-icon, #c8ccd4) !important; } -.darktheme ::-moz-placeholder { color: var(--muted-icon, #c8ccd4) !important; } -.darktheme :-ms-input-placeholder { color: var(--muted-icon, #c8ccd4) !important; } -.darktheme :-moz-placeholder { color: var(--muted-icon, #c8ccd4) !important; } - - -/* 13) High-contrast treatment for copy links (reshare) */ -/* The anchor element itself carries .grabLinks; ensure we target it directly */ -a.grabLinks:link, -a.grabLinks:visited { - color: var(--a-link) !important; -} -.darktheme a.grabLinks:link, -.darktheme a.grabLinks:visited { - color: var(--a-dark-link); - text-shadow: 0 1px 0 rgba(0,0,0,0.6); -} -a.grabLinks:hover, -a.grabLinks:active { - color: var(--a-hover) !important; -} -.darktheme a.grabLinks:hover, -.darktheme a.grabLinks:active { - color: var(--a-dark-hover) !important; -} - -/* Copy-this-URL label: increase contrast subtly */ -#copythisurl { color: #5b6574 !important; } -.darktheme #copythisurl { color: var(--muted-icon) !important; } - -/* 13b) Fix dark-mode audio source multiselect contrast */ -.darktheme ul#audioSource, -.darktheme ul#audioSource3 { - background-color: var(--discord-grey-6) !important; - border: 1px solid var(--discord-grey-8) !important; -} -.darktheme ul#audioSource label, -.darktheme ul#audioSource3 label { - background-color: transparent !important; - color: var(--discord-text) !important; -} - -/* Ensure selected audio source labels don't turn white */ -ul#audioSource input:checked + label, -ul#audioSource3 input:checked + label { - color: #111 !important; -} -.darktheme ul#audioSource input:checked + label, -.darktheme ul#audioSource3 input:checked + label { - color: var(--discord-text) !important; -} - -/* 8) Director/Guest toolbar polish (glass, non-invasive) */ -#subControlButtons { - background-color: var(--container-color); - backdrop-filter: blur(8px) saturate(120%); - -webkit-backdrop-filter: blur(8px) saturate(120%); - border: 1px solid rgba(0,0,0,0.08); - box-shadow: 0 8px 24px rgba(0,0,0,0.15); -} -.darktheme #subControlButtons { - background-color: rgba(28, 31, 36, 0.6); - border: 1px solid rgba(255,255,255,0.08); - box-shadow: 0 10px 28px rgba(0,0,0,0.35); -} - -#subControlButtons div, #subControlButtons span button { - border-radius: 10px; - box-shadow: 0 1px 2px rgba(0,0,0,0.25); -} -#subControlButtons div:hover { border-radius: 8px; } - -/* Controls grid buttons: consistent shape & hover */ -.controlsGrid button { - box-shadow: 0 1px 2px rgba(0,0,0,0.2); - transition: transform .08s ease, box-shadow .15s ease, filter .15s ease; -} -.controlsGrid button:hover { - transform: translateY(-1px); - box-shadow: 0 6px 14px rgba(0,0,0,0.18); -} - -/* Director message box: glass pane */ -.controlsGrid .director-message-box { - background: rgba(255,255,255,0.6); - backdrop-filter: blur(6px) saturate(120%); - -webkit-backdrop-filter: blur(6px) saturate(120%); - border: 1px solid rgba(0,0,0,0.08); -} -.darktheme .controlsGrid .director-message-box { - background: rgba(58,61,69,0.55); - border: 1px solid rgba(255,255,255,0.08); -} -.darktheme .controlsGrid .director-message-box textarea { - background-color: rgba(64,66,73,0.7); - backdrop-filter: blur(2px); -} - -/* 9) Optional: utility class for explicit glass usage (not applied anywhere yet) */ -.glass { - background-color: rgba(255,255,255,0.6); - backdrop-filter: blur(10px) saturate(120%); - -webkit-backdrop-filter: blur(10px) saturate(120%); - border: 1px solid rgba(0,0,0,0.08); - box-shadow: 0 10px 28px rgba(0,0,0,0.18); -} -.darktheme .glass { - background-color: rgba(28,31,36,0.6); - border: 1px solid rgba(255,255,255,0.08); -} - -/* 13) Close buttons and director link icons — unify intensity */ -.close-btn, .overlayCloseBtn { color: #5b6574; } -.close-btn:hover, .overlayCloseBtn:hover { color: var(--a-hover); } -.darktheme .close-btn, .darktheme .overlayCloseBtn { color: var(--muted-icon); } -.darktheme .close-btn:hover, .darktheme .overlayCloseBtn:hover { color: var(--a-dark-hover); } - -.director-link-icons .las { color: #5b6574; } -.director-link-icons .las:hover { color: var(--a-hover); } -.darktheme .director-link-icons .las { color: var(--muted-icon); } -.darktheme .director-link-icons .las:hover { color: var(--a-dark-hover); } - -/* 14) Modal glass and alert polish (safe: background/border only) */ -.modal-content, -.alertModalInner { - background-color: rgba(255,255,255,0.80) !important; - backdrop-filter: blur(8px) saturate(120%); - -webkit-backdrop-filter: blur(8px) saturate(120%); - border: 1px solid rgba(0,0,0,0.08) !important; - border-radius: 10px !important; -} -.darktheme .modal-content, -.darktheme .alertModalInner { - background-color: rgba(230,230,230,0.70) !important; - border: 1px solid rgba(255,255,255,0.08) !important; -} - -/* 15) Chat send button harmony */ -.chatBarInputButton { - background-color: var(--accent-color) !important; - color: #000 !important; - border: 1px solid #2f7ec4 !important; - border-radius: 8px !important; -} -.chatBarInputButton:hover { - background-color: var(--accent-hover-color) !important; -} - -/* 16) Solo link accent harmonization */ -a.soloLink:link, a.soloLink:visited { color: var(--a-link); } -.darktheme a.soloLink:link, .darktheme a.soloLink:visited { color: var(--a-dark-link); } -a.soloLink:hover { color: var(--a-hover); } -.darktheme a.soloLink:hover { color: var(--a-dark-hover); } - -/* 10) Header glass — subtle and safe */ -#header { - background-color: rgba(15, 19, 29, 0.60) !important; - backdrop-filter: blur(6px) saturate(120%); - -webkit-backdrop-filter: blur(6px) saturate(120%); - border-bottom: 1px solid rgba(255,255,255,0.06); -} -.darktheme #header { - background-color: rgba(14, 17, 24, 0.60) !important; -} - -/* 11) Chat module glass and cohesion */ -#chatModule { - background-color: rgba(255,255,255,0.75) !important; - backdrop-filter: blur(10px) saturate(120%); - -webkit-backdrop-filter: blur(10px) saturate(120%); - border: 1px solid rgba(0,0,0,0.08) !important; - border-radius: 10px !important; - overflow: hidden; -} -.darktheme #chatModule { - background-color: rgba(28,31,36,0.65) !important; - border: 1px solid rgba(255,255,255,0.08) !important; -} -.chat-header { - background-color: transparent !important; - border-bottom: 1px solid rgba(0,0,0,0.08); -} -.darktheme .chat-header { border-bottom-color: rgba(255,255,255,0.08); } -.chat-input-area { background-color: transparent !important; } - -/* Chat links harmonized with accent */ -div#chatBody a { - color: var(--a-link) !important; - background-color: rgba(62,143,216,0.1) !important; - border: 1px solid currentColor !important; -} -.darktheme div#chatBody a { - color: var(--a-dark-link) !important; - background-color: rgba(158,199,238,0.10) !important; - border-color: var(--a-dark-link) !important; -} - -/* 12) Primary CTA harmony */ -.gobutton { - background-color: var(--accent-color) !important; - border-color: #2f7ec4 !important; -} -.gobutton:hover { - background-color: var(--accent-hover-color) !important; - border-color: #2f7ec4 !important; -} - -/* 20) Message cards — softer backgrounds and accents */ -.message-card { border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); } -.warning.message-card { background: #fffbe8; border-left: 4px solid #f2df75; } -.info.message-card { background: #eef3fb; border-left: 4px solid #9fc4f5; } -.darktheme .message-card { background-color: #2f3237; box-shadow: 0 8px 24px rgba(0,0,0,0.35); } -.darktheme .warning.message-card { background: #3a3d45; border-left-color: #f2df75; } -.darktheme .info.message-card { background: #363b46; border-left-color: #9fc4f5; } - -/* 21) Tooltips — readability and polish */ -.tooltip .tooltiptext { - background-color: rgba(15,19,29,0.92) !important; - color: #e8eef6 !important; - border: 1px solid rgba(255,255,255,0.06); - border-radius: 8px; - box-shadow: 0 10px 24px rgba(0,0,0,0.25); -} -.darktheme .tooltip .tooltiptext { background-color: rgba(34,37,44,0.95) !important; } - -/* 22) Sliders — theme-aware styling */ -input[type="range"] { -webkit-appearance: none; background: transparent; min-height: 24px; } -input[type="range"]:focus { outline: none; } -/* WebKit */ -input[type="range"]::-webkit-slider-runnable-track { height: 6px; background: var(--lighttheme-4); border-radius: 999px; } -input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--accent-color); border: 1px solid #2f7ec4; margin-top: -5px; box-shadow: 0 1px 2px rgba(0,0,0,0.3); } -/* Firefox */ -input[type="range"]::-moz-range-track { height: 6px; background: var(--lighttheme-4); border-radius: 999px; } -input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--accent-color); border: 1px solid #2f7ec4; box-shadow: 0 1px 2px rgba(0,0,0,0.3); } -input[type="range"]::-moz-range-progress { height: 6px; background: var(--accent-color); border-radius: 999px; } -.darktheme input[type="range"]::-webkit-slider-runnable-track { background: var(--discord-grey-6); } -.darktheme input[type="range"]::-moz-range-track { background: var(--discord-grey-6); } -.darktheme input[type="range"]::-moz-range-progress { background: var(--a-dark-link); } -.darktheme input[type="range"]::-webkit-slider-thumb { background: var(--a-dark-link); border-color: #6b8db3; } -.darktheme input[type="range"]::-moz-range-thumb { background: var(--a-dark-link); border-color: #6b8db3; } - -/* 23) Non-expanded tiles — prevent scrollbars */ -.column.card:not(.skip-animation):not(.in-animation) { overflow-y: hidden !important; } - -/* 25) Create a Room (#container-1) — reduce boldness, improve contrast */ -.darktheme #container-1 th b, -.darktheme #container-1 td b { font-weight: 400 !important; } -.darktheme #container-1 th, -.darktheme #container-1 td, -.darktheme #container-1 label { color: #fff !important; } -.darktheme #container-1 [data-translate="guests-only-see-director"], -.darktheme #container-1 [data-translate="scenes-can-see-director"] { color: #fff !important; font-weight: 400 !important; } - -.invite_setting_group { - padding: 20px; -} - -#videoname4 { - padding: 25px; -} - -/* Subtle vertical balance for all tile headings */ -.column.card > i { margin-top: 17px!important;} - -.directorContainer, #guestFeeds, .hideLinksClass { - border-radius: 4px; - backdrop-filter: blur(10px) saturate(120%); - -webkit-backdrop-filter: blur(10px) saturate(120%); - border: 1px solid rgba(0,0,0,0.08); - box-shadow: 0 10px 28px rgba(0,0,0,0.18); -} - -/* Tipping Feature Styles */ -.tipModal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.75); - display: flex; - align-items: center; - justify-content: center; - z-index: 100000; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); -} - -.tipModalInner { - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); - border-radius: 16px; - padding: 28px; - max-width: 420px; - width: 90%; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1); - color: #fff; - position: relative; -} - -.tipModalInner h3 { - margin: 0 0 20px 0; - font-size: 1.3em; - font-weight: 600; - text-align: center; - color: #fff; -} - -.tipCloseBtn { - position: absolute; - top: 12px; - right: 12px; - background: rgba(255, 255, 255, 0.1); - border: none; - color: #fff; - width: 32px; - height: 32px; - border-radius: 50%; - cursor: pointer; - font-size: 18px; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.2s; -} - -.tipCloseBtn:hover { - background: rgba(255, 255, 255, 0.2); -} - -.tipAmounts { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 10px; - margin-bottom: 18px; -} - -.tipAmountBtn { - background: rgba(255, 255, 255, 0.08); - border: 2px solid transparent; - color: #fff; - padding: 14px 8px; - border-radius: 10px; - cursor: pointer; - font-size: 1.1em; - font-weight: 600; - transition: all 0.2s; -} - -.tipAmountBtn:hover { - background: rgba(255, 255, 255, 0.15); - transform: translateY(-2px); -} - -.tipAmountBtn.selected { - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); - border-color: rgba(255, 255, 255, 0.3); - box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4); -} - -.tipAmountBtn.custom { - font-size: 0.9em; - font-weight: 500; -} - -.tipCustomInput { - width: 100%; - padding: 14px 16px; - background: rgb(0, 100, 100) !important; - border: 2px solid rgba(255, 255, 255, 0.3); - border-radius: 10px; - color: #fff !important; - color: white !important; - font-size: 1em; - margin-bottom: 12px; - box-sizing: border-box; - transition: border-color 0.2s, background 0.2s; - -webkit-text-fill-color: #fff !important; - -webkit-text-fill-color: white !important; - caret-color: #fff; - color-scheme: dark; -} - -.tipCustomInput:focus { - outline: none; - border-color: rgba(100, 255, 218, 0.7); - background: rgb(0, 100, 100) !important; - color: #fff !important; - color: white !important; - -webkit-text-fill-color: #fff !important; - -webkit-text-fill-color: white !important; - color-scheme: dark; -} - -.tipCustomInput::placeholder { - color: rgba(255, 255, 255, 0.6); - -webkit-text-fill-color: rgba(255, 255, 255, 0.6); -} - -/* Override browser autofill styles */ -.tipCustomInput:-webkit-autofill, -.tipCustomInput:-webkit-autofill:hover, -.tipCustomInput:-webkit-autofill:focus, -.tipCustomInput:-webkit-autofill:active { - -webkit-box-shadow: 0 0 0 30px rgb(0, 100, 100) inset !important; - -webkit-text-fill-color: #fff !important; - caret-color: #fff !important; -} - -/* Ensure number inputs also style correctly */ -.tipCustomInput[type="number"] { - -moz-appearance: textfield; -} - -.tipCustomInput[type="number"]::-webkit-inner-spin-button, -.tipCustomInput[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.tipInputGroup { - margin-bottom: 12px; -} - -.tipInputGroup label { - display: block; - margin-bottom: 6px; - font-size: 0.85em; - color: rgba(255, 255, 255, 0.7); -} - -.tipCardElement { - background: rgb(0, 120, 120); - padding: 14px 16px; - border-radius: 10px; - margin-bottom: 10px; - min-height: 24px; -} - -.tipCardElement label { - color: rgba(255, 255, 255, 0.9); -} - -.tipStripeBadge { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 6px; - margin-bottom: 16px; - font-size: 11px; - color: rgba(255, 255, 255, 0.6); -} - -.tipStripeBadge svg { - height: 16px; - width: auto; -} - -.tipConfirmBtn { - width: 100%; - padding: 16px; - background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); - border: none; - border-radius: 10px; - color: #fff; - font-size: 1.1em; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 4px 15px rgba(56, 239, 125, 0.3); -} - -.tipConfirmBtn:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(56, 239, 125, 0.4); -} - -.tipConfirmBtn:disabled { - background: rgba(255, 255, 255, 0.2); - cursor: not-allowed; - box-shadow: none; -} - -.tipConfirmBtn.processing { - background: rgba(255, 255, 255, 0.3); -} - -.tipError { - background: rgba(255, 82, 82, 0.2); - border: 1px solid rgba(255, 82, 82, 0.5); - color: #ff6b6b; - padding: 12px; - border-radius: 8px; - margin-bottom: 14px; - font-size: 0.9em; - text-align: center; -} - -.tipSuccess { - background: rgba(56, 239, 125, 0.2); - border: 1px solid rgba(56, 239, 125, 0.5); - color: #38ef7d; - padding: 12px; - border-radius: 8px; - margin-bottom: 14px; - font-size: 0.9em; - text-align: center; -} - -.tipPoweredBy { - text-align: center; - margin-top: 16px; - font-size: 0.75em; - color: rgba(255, 255, 255, 0.4); -} - -.tipPoweredBy a { - color: rgba(255, 255, 255, 0.6); - text-decoration: none; -} - -.tipPoweredBy a:hover { - color: rgba(255, 255, 255, 0.8); -} - -.tipTotal { - text-align: center; - margin-bottom: 14px; - font-size: 1.1em; - color: rgba(255, 255, 255, 0.9); -} - -.tipTotal span:last-child { - font-weight: 700; - background: linear-gradient(135deg, #f093fb, #f5576c); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.tipModalHeader { - text-align: center; - margin-bottom: 20px; -} - -.tipModalHeader h3 { - margin: 0; - font-size: 1.3em; -} - -/* Tip Button in Video Controls */ -.tipButton { - background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%) !important; - border: none !important; - position: relative; - overflow: hidden; -} - -.tipButton::before { - content: ''; - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - background: linear-gradient(45deg, transparent, rgba(255,255,255,0.3), transparent); - transform: rotate(45deg); - animation: tipButtonShine 3s infinite; -} - -@keyframes tipButtonShine { - 0% { left: -50%; } - 100% { left: 150%; } -} - -/* Tip Chat Message Styling */ -.tip-notification { - display: inline-flex; - align-items: center; - flex-wrap: wrap; - gap: 6px; - padding: 8px 12px; - background: linear-gradient(135deg, rgba(240, 147, 251, 0.15) 0%, rgba(245, 87, 108, 0.15) 100%); - border-radius: 8px; - border-left: 3px solid #f093fb; -} - -.tip-icon { - font-size: 1.2em; - margin-right: 4px; -} - -.tip-amount { - font-weight: 700; - font-size: 1.15em; - background: linear-gradient(135deg, #f093fb, #f5576c); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.tip-sender { - font-weight: 600; - color: #fff; -} - -.tip-message { - font-style: italic; - color: rgba(255, 255, 255, 0.85); - margin-left: 4px; -} - -/* Chat message container for tips */ -.message.tip-message-container { - background: linear-gradient(135deg, rgba(240, 147, 251, 0.08) 0%, rgba(245, 87, 108, 0.08) 100%); - border-radius: 8px; - margin: 4px 0; - padding: 4px; -} - -/* Loading spinner for tip processing */ -.tipSpinner { - display: inline-block; - width: 18px; - height: 18px; - border: 2px solid rgba(255, 255, 255, 0.3); - border-top-color: #fff; - border-radius: 50%; - animation: tipSpin 0.8s linear infinite; - margin-right: 8px; - vertical-align: middle; -} - -@keyframes tipSpin { - to { transform: rotate(360deg); } -} - -/* Mix dropdown styles for director audio source selection */ -.mix-dropdown { - position: absolute; - background: #2a2a2a; - border: 1px solid #555; - border-radius: 4px; - padding: 8px; - z-index: 1000; - min-width: 220px; - max-height: 300px; - overflow-y: auto; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); - left: 0; - top: 100%; - margin-top: 4px; -} - -.mix-dropdown-header { - color: #aaa; - font-size: 11px; - text-transform: uppercase; - padding: 4px 8px; - border-bottom: 1px solid #444; - margin-bottom: 4px; -} - -.mix-dropdown-section { - margin-bottom: 8px; -} - -.mix-dropdown-section-title { - color: #888; - font-size: 10px; - padding: 4px 8px; - text-transform: uppercase; -} - -.mix-dropdown-item { - display: flex; - align-items: center; - padding: 6px 8px; - cursor: pointer; - border-radius: 3px; - color: #ddd; - font-size: 12px; -} - -.mix-dropdown-item:hover { - background: #3a3a3a; -} - -.mix-dropdown-item input[type="checkbox"] { - margin-right: 8px; - cursor: pointer; -} - -.mix-dropdown-item label { - cursor: pointer; - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.mix-dropdown-container { - position: relative; -} - -/* Dark theme adjustments */ -.darktheme .mix-dropdown { - background: #1a1a1a; - border-color: #444; -} - -/* Tip Icon Overlay on Videos (two-way opt-in) */ -.tipIconOverlay { - position: absolute; - top: 8px; - left: 8px; - cursor: pointer; - z-index: 10; - display: flex; - align-items: center; - gap: 6px; -} - -.tipIconOverlay:hover { - transform: scale(1.05); -} - -/* Heart icon container - CSS heart shape */ -.tipHeart { - position: relative; - width: 36px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); - transition: opacity 0.8s ease, transform 0.8s ease; - flex-shrink: 0; -} - -/* Heart shape using CSS */ -.tipHeart::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 36px; - height: 32px; - background: rgba(229, 57, 53, 0.95); - clip-path: path('M18 28 C4 18, 0 8, 9 3 C14 0, 18 4, 18 8 C18 4, 22 0, 27 3 C36 8, 32 18, 18 28 Z'); -} - -/* Dollar sign inside heart */ -.tipDollar { - position: relative; - z-index: 1; - color: white; - font-size: 16px; - font-weight: 700; - font-family: Arial, sans-serif; - line-height: 1; - margin-top: -2px; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); -} - -.tipIconOverlay:hover .tipHeart::before { - background: rgba(229, 57, 53, 1); -} - -/* "Send a Tip" tooltip label */ -.tipLabel { - background: rgba(0, 0, 0, 0.85); - color: white; - padding: 5px 10px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - white-space: nowrap; - opacity: 0; - transform: translateX(-5px); - transition: opacity 0.2s ease, transform 0.2s ease; - pointer-events: none; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); -} - -/* Show label state (via JS class) */ -.tipIconOverlay.showLabel .tipLabel { - opacity: 1; - transform: translateX(0); -} - -/* Show tooltip on hover (immediate) */ -.tipIconOverlay:hover .tipLabel { - opacity: 1; - transform: translateX(0); -} - -/* QR code container - positioned below heart for OBS/scene mode */ -.tipQR { - position: absolute; - top: 0; - left: 0; - background: white; - border-radius: 6px; - padding: 4px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); - opacity: 0; - transform: scale(0.8); - transition: opacity 0.8s ease, transform 0.8s ease; - pointer-events: none; -} - -/* Show QR code state (hides heart, shows QR) */ -.tipIconOverlay.showQR .tipHeart, -.tipIconOverlay.showQR .tipLabel { - opacity: 0; - transform: scale(0.8); -} - -.tipIconOverlay.showQR .tipQR { - opacity: 1; - transform: scale(1); - pointer-events: auto; -} - -/* Smaller heart when no QR mode */ -.tipIconOverlay.noQR .tipHeart { - width: 30px; - height: 27px; -} - -.tipIconOverlay.noQR .tipHeart::before { - width: 30px; - height: 27px; - clip-path: path('M15 24 C3 15, 0 7, 7.5 2.5 C12 0, 15 3, 15 6.5 C15 3, 18 0, 22.5 2.5 C30 7, 27 15, 15 24 Z'); -} - -.tipIconOverlay.noQR .tipDollar { - font-size: 14px; -} - -/* Hide tip icon in clean mode */ -body.cleanOutput .tipIconOverlay, -.cleanOutput .tipIconOverlay { - display: none !important; -} - -/* Tip Onboarding Modal - matches .promptModal style */ -#tipOnboardingModal { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: rgb(221 221 221); - box-shadow: 0 0 30px 10px #0000005c; - color: black; - font-weight: bold; - border-radius: 10px; - padding: 0; - max-width: 500px; - width: 90%; - z-index: 9999; - overflow: hidden; -} - -.darktheme #tipOnboardingModal { - background-color: var(--discord-grey-5); - color: var(--discord-text); -} - -#tipOnboardingModal .promptModalInner { - padding: 1em; -} - -#tipOnboardingModal h2 { - margin: 0 0 12px 0; - font-size: 1.1em; -} - -#tipOnboardingModal h4 { - margin: 16px 0 6px 0; - font-size: 0.95em; -} - -#tipOnboardingModal p { - margin: 6px 0; - font-weight: normal; - font-size: 0.9em; -} - -#tipOnboardingModal code { - background: rgba(0,0,0,0.1); - padding: 2px 5px; - border-radius: 3px; - font-family: monospace; -} - -.darktheme #tipOnboardingModal code { - background: rgba(255,255,255,0.1); -} - -.tipOnboardingBtn { - display: inline-block; - background: #7289da; - color: white !important; - padding: 8px 16px; - border-radius: 5px; - text-decoration: none; - margin: 6px 0; - font-weight: bold; - font-size: 0.9em; -} - -.tipOnboardingBtn:hover { - background: #5b6eae; - text-decoration: none; -} + +.video-label { + position: absolute; + bottom: 0.6vh; + left: 0.5vh; + margin: 0px; + color: white; + padding: 5px 10px; + background: rgba(0, 0, 0, .5); + font-size: 1em; + pointer-events:none; +} +.video-label:empty { + display: none; +} + +.video-label>span:nth-child(2) { + font-size: 50%; + display: block; + text-align: center; +} +.video-label>span:nth-child(3) { + font-size: 25%; + display: block; + text-align: center; + line-height: 200%; +} +.video-label>span:nth-child(4) { + font-size: 25%; + display: block; + text-align: center; + line-height: 200%; +} +.video-label>span:nth-child(5) { + font-size: 25%; + display: block; + text-align: center; + line-height: 200%; +} + +.video-label.zoom { + bottom: 0; + left: 0; + pointer-events:none; +} + +.video-label.teams { + background: rgba(0, 0, 0, .4); + pointer-events:none; + border-radius: 5px; +} + +.video-label.skype { + bottom: 2vh; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, .8); + pointer-events:none; + border-radius: 5px; + font-size: 0.8em; +} + +.video-label.ninjablue { + bottom: 5%; + left: 0; + background: #141926; + padding: 10px 5%; +} + +.video-label.toprounded { + top: 0; + bottom: unset; + background: rgb(0 0 0 / 70%); + padding: 10px 5%; + left: 50%; + transform: translateX(-50%); + width: 50%; + text-align: center; + border-bottom-left-radius: 50px; + border-bottom-right-radius: 50px; + text-transform: uppercase; + letter-spacing: 3; + box-shadow: 0px 0px 10px #00000059; + font-size: 0.7em +} + +.video-label.rounded { + bottom: 0; + background: rgb(0 0 0 / 70%); + padding: 0px 27px; + left: 0; + border-top-right-radius: 20px; + box-shadow: 0px 0px 10px #00000059; + font-size: 0.7em; + font-weight: bold; +} + +.video-label.fire { + text-shadow: 0 -1px 4px #FFF, 0 -2px 10px #ff0, 0 -10px 20px #ff8000, 0 -18px 40px #F00; + font-weight: bold; + bottom: 2vh; + left: 0; + width: 100%; + text-align: center; +} + +.video-meter { + padding:0.5vh; + display: block; + width: min(1vh, 1vw); + height: min(1vh, 1vw); + min-width:10px; + min-height:10px; + top: 2vh; + right: 2vh; + background-color: #00c300; + position:absolute; + border-radius: 2vh; + pointer-events:none; + border: 1px black solid; + z-Index: 2; +} + +.video-meter-2 { + padding:0; + display: block; + width: 100%; + height: 100%; + min-width:10px; + min-height:10px; + top: 0; + right: 0; + background-color:unset; + position:absolute; + border-radius: 5px; + pointer-events:none; + border: 5px green solid; + z-Index: 2; +} + +.video-meter-director { + width: 10px; + height: 10px; + top: 8px; + right: 10px; +} +.video-meter2 { + display: block; + padding:0; + width: 4px; + height:0%; + min-width:2px; + bottom: 1px; + right: 7px; + background-color: #0000; + position:absolute; + border-radius: 2vh; + pointer-events:none; + border: 1px black solid; + transition: height 0.1s ease, background-color 0.1s ease; + z-Index: 2; +} + +.hasMedia > .video-meter2 { + display: block; +} +.hasMedia > .video-meter-2 { + display: block; +} +.hasMedia > .video-meter { + display: block; +} + +#voiceMeterTemplate{ + display: none; +} +#voiceMeterTemplate2{ + display: none; +} + +#userList{ + line-height: 1.3em; +} + +#userList > div > .video-meter { + padding: 5px; + margin-left: 5px; + top: 0; + right: 0; + position: relative; + display: inline-block; +} +.PPTActive { + box-shadow: 0px 0px 10px green; +} + + +.video-mute-state { + top: 2vh; + right: 2vh; + position: absolute; + color: white; + border-radius: 2vh; + background-color: #b11313; + padding: 2px 2px 2px 1px; + z-index: 2; +} + +.video-mute-state.unmuted { + background-color: green; +} + +.video-mute-state .la-microphone { + display: none; +} + +.video-mute-state .la-microphone-slash { + display: inline-block; +} + +.video-mute-state.unmuted .la-microphone { + display: inline-block; +} + +.video-mute-state.unmuted .la-microphone-slash { + display: none; +} + + + +.video-mute-state-userlist { + display: inline-block; + color:white; + border-radius: 2vh; + background-color: #b11313; + padding: 2.2px 1.5px 2px 2px; + margin: 0 0 0 5px; +} +.volume-control-userlist { + display: inline-block; + color:white; + border-radius: 2vh; + background-color: #262c3e; + padding: 2.2px 1.5px 2px 2px; + margin: 0 0 0 5px; + cursor: pointer; + border: 1px solid #9a9393; +} + +.volume-control-userlist input[type=range][orient=vertical] { + display: block; + color: white; + border-radius: 2vh; + -webkit-appearance: slider-vertical; + background-color: #262c3e; + cursor: pointer; + width: 17px; + margin: 0px 1px 4px 0; + padding: 0; +} + +#closedList_connectUsers{ + + cursor: pointer; + color: #d9e4eb; + padding: 2px; + margin: 2px 2px 0 0; + font-size: 140%; +} + +.screenshareNotActive{ + opacity: 0.5; + box-shadow: inset 0 0 50px #290f07; +} +#help_directors_room{ + cursor: pointer; +} + +.iframeblob{ + padding-top:18px; + text-align: left; + width: 600px; + display: block; + margin: auto; +} +#shareScreenGear{ + display: none; +} + +div.message-card { + padding: 10px; + display: block; + padding-left: 1em; + align-items: center; + width: 600px; + margin: 0 auto; + position: relative; + padding-left: 60px; + margin: 20px auto; + box-shadow: 0px 5px 10px -5px #a9a9a9; +} + +div.message-card a { + color: rgb(0 77 218); + font-weight: bold; + text-decoration: underline; +} + +.message-card ul { + border: unset !important; + background-color: unset !important; + color: unset !important; + list-style: outside; +} + +.warning.message-card { + border-left: 4px solid #eff150; + background: #fffded; +} +.info.message-card { + border-left: 4px solid #aacefd; + background: #e6e8f0; +} +.darktheme #guestTips { + background-color: var(--discord-grey-7); +} +.darktheme #guestTips .las { + color: #FFF; +} +.darktheme .message-card { + background-color: #000; +} +.darktheme input[type='file'] { + background-color: #000; +} +.message-card h1 { + display: block; + font-size: 110%; + text-align: left; +} + +.message-card p { + display: block; + text-align: left; + margin-top: 10px; +} + +div.message-card:before { + font-family: 'Line Awesome Free'; + font-weight: 900; + font-size: 2em; + margin-right: 0.5em; + position: absolute; + top: 6px; + left: 10px; +} + +div.message-card.warning:before { + content: "\f071"; +} + +div.message-card.info:before { + content: "\f05a"; +} + + +.video-label.floating3d { + text-transform: uppercase; + display: block; + color: #FFFFFF; + text-shadow: 0 1px 0 #CCCCCC, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 10px 10px rgba(0,0,0,.2), 0 20px 20px rgba(0,0,0,.15); + color: #FFFFFF; + animation-name: floating; + animation-duration: 5s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + width: 100%; + font-size: 5em; + font-weight:bold; + text-align: center; + bottom: 4vh; + position: absolute; +} + +.director-link-icons { + font-size: 1.5em; + float: left; + bottom: 4px; + position: relative; + margin-right: 9px; +} + +.switch { + position: relative; + margin:5px 5px 2px 5px; + width: 40px; + height: 24px; + bottom:20px; + border-radius: 2px; + display: inline-block; +} + +.switch input { + width: 0; + height: 0; + opacity: 0; +} + +.slider { + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .3s; + transition: .3s; + position: absolute; +} + +.slider:before { + content: ""; + height: 17px; + width: 17px; + left: 3px; + bottom: 3px; + background-color: white; + -webkit-transition: .3s; + transition: .3s; + position: absolute; +} + +input:checked + .slider { + background-color: #86b98f; +} + +input:focus + .slider { + box-shadow: 0 0 1px #86b98f; +} + +input:checked + .slider:before { + -webkit-transform: translateX(16px); + -ms-transform: translateX(16px); + transform: translateX(16px); +} +#remoteOBSControl { + user-select: none; + z-index: 25; + max-height: calc(100vh - 116px); + overflow-y: auto; +} + +#remoteOBSControl button { + margin: 5px; + padding:10px; +} + +.fullscreenOBSControl #remoteOBSControl { + width: 100%; + height: 100%; + flex-direction: row; + flex-wrap: nowrap; + align-content: center; + justify-content: center; + align-items: center; + max-height: 100%; +} + +.fullscreenOBSControl #remoteOBSControl .promptModalInner { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-content: center; + justify-content: center; + align-items: center; +} + +.fullscreenOBSControl #remoteOBSControl .modalClose { + display: none; +} + +.fullscreenOBSControl #header { + display: none; +} + + +.darktheme #promptModal, .darktheme .customModelPopup, .darktheme .promptModal { + background-color: var(--discord-grey-5); + color: var(--discord-text); +} + +#promptModal, .customModelPopup, .promptModal { + position: absolute; + background-color: rgb(221 221 221); + box-shadow: 0 0 30px 10px #0000005c; + color: black; + font-size: 1.0em; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 10px; + font-weight: bold; + z-index:31; + width:550px; + max-width:100%; + overflow: hidden; + overflow-wrap: break-word; +} + +#bufferSettings{ + position: absolute; + background-color: #ddddddee; + box-shadow: 0 0 30px 10px #0000005c; + color: black; + font-size: 1.0em; + bottom: 5%; + left: 50%; + transform: translate(-50%, 0%); + border-radius: 10px; + font-weight: bold; + z-index:31; + width:550px; + max-width:100%; + overflow: hidden; + overflow-wrap: break-word; +} + +/* PTZ Controls Modal */ +#ptzControlsModal { + position: absolute; + background-color: #ddddddee; + box-shadow: 0 0 30px 10px #0000005c; + color: black; + font-size: 1.0em; + bottom: 5%; + left: 50%; + transform: translate(-50%, 0%); + border-radius: 10px; + font-weight: bold; + z-index: 31; + width: 380px; + max-width: 95%; + overflow: hidden; + overflow-wrap: break-word; +} +.darktheme #ptzControlsModal { + background-color: var(--discord-grey-1); + color: var(--discord-text); +} +.ptz-control-group { + margin: 12px 0; + display: flex; + align-items: center; + gap: 10px; +} +.ptz-control-group label { + min-width: 45px; + text-align: right; +} +.ptz-slider { + flex: 1; + min-width: 150px; +} +.ptz-value { + min-width: 40px; + text-align: left; + font-weight: normal; +} +.ptz-shortcuts { + margin-top: 15px; + padding-top: 12px; + border-top: 1px solid #aaa; + font-size: 0.85em; + font-weight: normal; +} +.ptz-shortcuts p { + margin: 6px 0; +} +.ptz-shortcuts kbd { + background-color: #eee; + border: 1px solid #ccc; + border-radius: 3px; + padding: 2px 5px; + font-family: monospace; + font-size: 0.9em; +} +.darktheme .ptz-shortcuts { + border-top-color: var(--discord-grey-3); +} +.darktheme .ptz-shortcuts kbd { + background-color: var(--discord-grey-2); + border-color: var(--discord-grey-3); + color: var(--discord-text); +} + +#publishSettings{ + position: absolute; + background-color: #ddddddee; + box-shadow: 0 0 30px 10px #0000005c; + color: black; + font-size: 1.0em; + bottom: calc(50% - 130px); + left: 50%; + transform: translate(-50%, 0%); + border-radius: 10px; + font-weight: bold; + z-index:31; + width:680px; + max-width:100%; + overflow: hidden; + overflow-wrap: break-word; +} +.largeTextEntry { + width: 90%; + margin: 10px 5%; + font-size: .8em; + padding: 0.4em; + display: block; + +} +.promptModalInner { + position: relative; + padding: 1em; + width: 100%; +} + +.promptModalMessage { + position: relative; + display: block; + width: 93%; + margin: 0 5%; +} + +#iframe_source{ + width: 100%; + height: 100%; + margin: auto; + border: 10px dashed rgb(64 65 62) +} + +iframe.insecure { + border: 10px dashed rgb(64 65 62)!important; +} +.effects-controls { + margin: 1rem 0; +} + +.zoom-control-group { + margin: 1rem 0; + display: flex; + align-items: center; + gap: 1rem; +} + +.zoom-slider, +.effect-slider { + flex: 1; + min-width: 200px; + max-width: 350px; +} + +.zoom-value { + min-width: 3rem; + text-align: right; +} + +.effect-selector-group { + margin: 1rem 0; +} +.startupWarning{ + max-width:100%; + display: block; + width: 463px; + border-left: 4px solid #eff150; + background: #fffded; + padding: 10px; + align-items: center; + position: relative; + margin: 17px auto 20px auto; + box-shadow: 0px 5px 10px -5px #a9a9a9; + text-align: left; +} +.startupWarning > p { + text-align: left; + display: inline-block; + padding-left: 38px; + +} +.startupWarning > i { + position: absolute; + font-size: 2em; + padding: 2px 0 0 0; +} + +.darktheme .startupWarning{ + background: black!important; + box-shadow: 0px 5px 10px -5px #a5a566; + color: white!important; +} + +.container-inner>div, .container-inner>span>div, .container-inner button:not(.gowebcam), .message-card { + border-radius: 5px; + box-shadow: 10px 8px 32px -10px #8883; +} + +.cameraTip { + width: 100%; + display: block; + border-left: 4px solid #50cef1; + background: #dbf0ff; + padding: 10px; + align-items: center; + position: relative; + margin: 8px auto 0px auto; + box-shadow: 0px 5px 10px -5px #a9a9a9; + text-align: left; + font-size: 97%; + white-space:normal; + min-height: 44px; + border-radius: 5px; +} +.cameraTip > p { + text-align: left; + display: inline-block; + padding-left: 32px; + vertical-align: middle; + +} +.cameraTip > i { + position: absolute; + font-size: 1.5em; + padding: 2px 0 0 0; +} + +#consentWarning{ + margin: 0 auto 20px auto; +} + +#consentWarning2{ + margin: 0px auto 10px auto; +} + +#alertModal { + position: absolute; + background-color: rgb(221 221 221); + box-shadow: 0 0 30px 10px #0000005c; + color: black; + font-size: 1.2em; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 10px; + font-weight: bold; + z-index:32; + overflow-wrap: break-word; + min-width: min(90vw, 320px); +} + +#connectUsers{ + float: right; + display: none; + position: absolute; + max-width: 400px; + min-width: 150px; + max-height: 80%; + background-color: #08090e; + z-index: 5; + padding: 10px; + right: 20px; + bottom: 120px; + box-shadow: 2px 2px #313131; + border-radius: 5px; + border: 1px solid #252525; + opacity: 0.7; + color: white; +} + +#alertModal a:link { + color: blue; +} + +#alertModal a:visited { + color: blue; +} + +#alertModal a:hover { + color: blue; +} + + #alertModal a:active { + color: blue; +} + +.alertModalInner { + position: relative; + padding: 2em; + user-select: none; +} + +.modalClose { + position: absolute; + top: -4px; + right: 4px; + cursor: pointer; + font-weight: bolder; + font-size: 1.8em; +} + +#modalBackdrop { + background: var(--background-color); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 0; + opacity: 0.8; +} +#modalBackdrop.darktheme{ + background-color: var(--dark-background-color); +} + +.modalBackdrop { + background: var(--background-color); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 0; + opacity: 0.8; +} + +.modalBackdrop.darktheme{ + background-color: var(--dark-background-color); +} + +.opaqueBackdrop{ + background: var(--background-color); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 0; + opacity: 1.0; +} +.opaqueBackdrop.darktheme{ + background-color: var(--dark-background-color); +} + +.alertModalMessage>select{ + font-size: 100%; +} + +.iframeDetails { + margin: 10px; + position: relative; + word-break: break-all; + max-height: 500px; + overflow: hidden; +} +.desktop-capturer-selection { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: rgba(30,30,30,.75); + color: #fff; + z-index: 10000000; + display: flex; + align-items: center; + justify-content: center; +} +.desktop-capturer-selection__scroller { + width: 100%; + max-height: 100vh; + overflow-y: auto; +} +.desktop-capturer-selection__list { + max-width: calc(100% - 100px); + margin: 50px; + padding: 0; + display: flex; + flex-wrap: wrap; + list-style: none; + overflow: hidden; + justify-content: center; +} +.desktop-capturer-selection__item { + display: flex; + margin: 4px; +} +.desktop-capturer-selection__btn { + display: flex; + flex-direction: column; + align-items: stretch; + width: 145px; + margin: 0; + border: 0; + border-radius: 3px; + padding: 4px; + background: #252626; + text-align: left; + transition: background-color .15s, box-shadow .15s; +} +.desktop-capturer-selection__btn:hover, +.desktop-capturer-selection__btn:focus { + background: rgba(98,100,167,.8); +} +.desktop-capturer-selection__thumbnail { + width: 100%; + height: 81px; + object-fit: cover; +} +.desktop-capturer-selection__name { + margin: 6px 0 6px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +/* New styles for audio capture checkboxes */ +.desktop-capturer-selection__audio-option { + margin-top: 5px; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} +.desktop-capturer-selection__audio-option label { + display: flex; + align-items: center; + cursor: pointer; + padding: 4px; + border-radius: 3px; + background: rgba(60,60,60,0.5); +} +.desktop-capturer-selection__audio-option label:hover { + background: rgba(80,80,80,0.7); +} +.desktop-capturer-selection__audio-option input { + margin-right: 5px; + width: 14px; + height: 14px; +} +.desktop-capturer-selection__audio-option span { + display: flex; + align-items: center; +} +.desktop-capturer-selection__audio-option i { + margin-right: 3px; + font-size: 14px; +} +.video-zoom-slider0 { + position: absolute; + bottom: 45px; + left: 10px; + right: 10px; + height: 40px; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + padding: 0 15px; + opacity: 0.3; + transition: opacity 0.3s; + z-index: 5; +} + +.video-zoom-slider0:hover, +.video-container:hover .video-zoom-slider { + opacity: 1; +} + +.video-zoom-slider0 input[type="range"] { + width: 100%; + height: 6px; + -webkit-appearance: none; + background: rgba(255,255,255,0.2); + border-radius: 3px; + cursor: pointer; +} + +.video-zoom-slider0 input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; +} + +.video-zoom-slider0 input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: white; + border: none; + border-radius: 50%; + cursor: pointer; +} + +.video-ptz-controls { + position: absolute; + bottom: 45px; + left: 10px; + right: 10px; + background: rgba(0,0,0,0.5); + display: flex; + flex-direction: column; + padding: 10px 15px; + opacity: 0.3; + transition: opacity 0.3s; + z-index: 5; +} + +.video-ptz-controls:hover, +.video-container:hover .video-ptz-controls { + opacity: 1; +} + +.video-zoom-slider, +.video-pan-slider, +.video-tilt-slider { + display: flex; + align-items: center; + margin: 5px 0; + width: 100%; +} + +.video-ptz-controls label { + color: white; + font-size: 12px; + margin-right: 10px; + min-width: 40px; +} + +.video-ptz-controls input[type="range"] { + flex: 1; + height: 6px; + -webkit-appearance: none; + background: rgba(255,255,255,0.2); + border-radius: 3px; + cursor: pointer; +} + +.video-ptz-controls input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; +} + +.video-ptz-controls input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + background: white; + border: none; + border-radius: 50%; + cursor: pointer; +} + +.makeSmallerDirectorRoom{ + max-width: calc(100% - 415px); + min-width: min(calc(100% - 395px), 75%); +} +.pinToSide{ + top: unset; + left: unset; + right: 0; + transform: unset; + border-radius: unset; + height: 100%; + max-width: min(25%, 415px); + min-width: 395px; + overflow-y: auto; + position: fixed; +} +#roomSettings{ + max-height: 100%; + overflow: auto; +} + +body.darktheme { + color: white; + scrollbar-color: #000 #333; +} +body.darktheme form>label{ + color: white; +} +body.darktheme .column>h2{ + color: #b6b6b6; +} +body.darktheme .column { + border-color: rgba(255, 255, 255, 0.1); +} +body.darktheme .column:hover { + border-color: rgba(255, 255, 255, 0.15); +} +body.darktheme .directorsgrid .vidcon > .las { + background-color: #424242; +} +body.darktheme h2 { + color: #DDD; +} +body.darktheme .column .las { + color: var(--discord-text); +} +body.darktheme label { + color: var(--discord-text); +} +body.darktheme #roomHeader{ + color: var(--discord-text); +} +body.darktheme div.multiselect { + background-color: var(--discord-grey-7); +} +body.darktheme .audioMenu{ + background-color: var(--discord-grey-7); +} +body.darktheme #avatarDiv{ + background-color: var(--discord-grey-7); +} +body.darktheme .selected { + border: solid 3px #f8f7f7; +} +body.darktheme #selectAvatarImage label { + color: #f8f7f7; +} +body.darktheme .avatarLabel { + color: #f8f7f7; +} +body.darktheme .directorsgrid .vidcon { + background-color: var(--discord-grey-3); +} +body.darktheme .promptModal { + background-color: var(--discord-grey-5); + color: var(--discord-text); +} +body.darktheme .infoblob{ + color: #CCC; +} +body.darktheme .outMessage{ + background-color: #7f89a7; +} +body.darktheme .inMessage { + background-color: #7e7d7d; +} +body.darktheme .actionMessage { + background-color: #b1b1b1; +} +body.darktheme #chatInput{ + background-color: var(--discord-grey-7); + color: var(--discord-text) +} +body.darktheme .alertModal{ + background-color: #ccc; + filter:brightness(0.85); +} +body.darktheme .directorContainer{ + filter: brightness(0.85); +} +body.darktheme .cameraTip{ + background-color: #27354b; + color: #e5dbdb; +} +.containerGreen{ + background-color: #649166!important; +} +body.darktheme .containerGreen{ + background-color: #243824!important; +} +body.darktheme .startupWarning>.las { + color:white!important; +} + +/* +//////////////////////////////////////////// +// INTERMEDIATE STYLING - PRE FLEX-SWITCH // +//////////////////////////////////////////// +*/ + +button { + background-color: var(--lighttheme-2); + color: var(--lighttheme-text); + filter: brightness(1); + display: inline-flex; + align-items: center; + justify-content: center; + user-select: none; + cursor: pointer; + padding: 4px 6px; + border-radius: 4px; + border: 1px solid var(--lighttheme-6); +} +button:hover { + filter: brightness(0.98); +} +button i { + font-size: 130%; +} +.darktheme :not(.promptModalInner) > button { + background-color: var(--discord-grey-5); + border: 1px solid var(--discord-grey-8); + color: var(--discord-text); +} +.darktheme :not(.promptModalInner) > button:hover { + filter: brightness(1.05); +} + +.gobutton { + font-size: 110%; + padding: 10px 15px; + border: 2px solid #7ea8c8; + cursor: pointer; + background-color: #99bfd9; + color: #000; + font-weight: 700; + border-radius: 6px; + transition: all 0.15s ease-in-out; +} +.gobutton:hover { + background-color: #8bb2d4; + border-color: #6d9bbb; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} +.gobutton:active { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.darktheme .gobutton { + background-color: #6B8698; + color: #000; + border: 2px solid #556a7a; +} +.darktheme .gobutton:hover { + background-color: #7792a5; + border-color: #4a5d6c; +} + +.chevron::before { + border-style: solid; + border-width: 0.14em 0.14em 0 0; + content: ''; + display: inline-block; + height: 0.32em; + transform: rotate(-45deg); + width: 0.32em; + margin: 0 7px 0 3px; +} + +.chevron.bottom::before { + transform: rotate(135deg); + bottom: 3px; + position: relative; +} + +.chevron.right::before { + transform: rotate(45deg); +} +#chevarrow4::before ,#chevarrow3::before { + position:relative; + bottom:1px; +} +#chevarrow4.bottom::before ,#chevarrow3.bottom::before { + bottom:2px; +} + +button.white { + padding: 6px 10px 4px 9px; + cursor: pointer; + border-radius: 2px; + background-color: #fff; + color: #000; + border: 1px solid #000; +} + +select { + background-color: var(--lighttheme-1); + color: var(--lighttheme-text); + border-radius: 4px; + font-size: 16px; + padding: 4px 6px; + outline: 0; + cursor: pointer; + font-size:95%; + text-overflow: ellipsis; + max-width: 100%; + border: 1px solid var(--lighttheme-6); + transition: border-color 0.15s ease-in-out; +} +select:hover { + border-color: var(--lighttheme-5); +} +select:focus { + border-color: #99bfd9; +} +.darktheme select { + background-color: var(--light-grey); + border: 1px solid var(--discord-grey-8); + color: var(--near-black); + border-radius: 4px; +} +.darktheme ul { + /* background-color: var(--discord-grey-3); + border: 1px solid var(--discord-grey-8); + color: var(--discord-text);*/ +} +ul { + /* background-color: var(--lighttheme-1); + border: 1px solid var(--lighttheme-6); + color: var(--lighttheme-text);;*/ + border-radius: 4px; +} + +li { + margin: 0.1em 0; + padding-left: 0.1em; + line-height: 1.3em; +} + +input[type='text'], input[type='password'] { + background-color: var(--lighttheme-1); + border: 1px solid var(--lighttheme-6); + color: var(--lighttheme-text); + border-radius: 4px; + padding: 6px 8px; + height: 30px; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +input[type='text']:focus, input[type='password']:focus { + outline: none; + border-color: #99bfd9; + box-shadow: 0 0 0 2px rgba(153, 191, 217, 0.2); +} +input::placeholder { + color: var(--lighttheme-4) !important; +} +input[type='checkbox'], input[type='radio'] { + -ms-transform: scale(1.4); + /* IE */ + -moz-transform: scale(1.4); + /* FF */ + -webkit-transform: scale(1.4); + /* Safari and Chrome */ + -o-transform: scale(1.4); + /* Opera */ + transform: scale(1.4); + padding: 5px; + margin: 3px; + + cursor: pointer; +} + +.darktheme :not(.promptModalInner) > input:not([type='range']) { + border: 1px solid var(--discord-grey-8); + border-radius: 4px; +} + +.darktheme :not(.promptModalInner) > input::placeholder { + color: var(--discord-grey-8) !important; +} + +.darktheme .card { + background-color: var(--discord-grey-7); +} + +.card { + background-color: var(--lighttheme-3); +} + +.darktheme .container-inner { + background-color: var(--discord-grey-7); +} + +.container-inner { + display: none; + background-color: var(--lighttheme-3); + min-height: calc(100% - 100px); + margin-bottom:30px; +} + +.title { + text-align: left; + user-select: none; + display: inline-block; +} + +.darktheme #addPasswordBasic, +.darktheme #avatarDiv, +.darktheme #avatarDiv2, +.darktheme #avatarDiv3, +.darktheme #audioScreenShare1, +.darktheme #audioMenu, +.darktheme #audioMenu2, +.darktheme #effectsDiv, +.darktheme #effectsDiv2, +.darktheme #effectsDiv3, +.darktheme #grabDirectorSoloLinkParent, +.darktheme #videoMenu, +.darktheme #videoMenu2, +.darktheme #videoMenu3, +.darktheme #headphonesDiv, +.darktheme #headphonesDiv2, +.darktheme #headphonesDiv3, +.darktheme #videoSettings, +.darktheme #videoSettings2, +.darktheme #popupSelector_user_settings, +.darktheme .invite_setting_group { + background-color: var(--discord-grey-5); + border: 1px solid var(--discord-grey-8); +} +.darktheme .largeDarkIcon{ + color: var(--dark-background-color)!important; +} +#addPasswordBasic, +#avatarDiv, +#avatarDiv2, +#avatarDiv3, +#audioScreenShare1, +#audioMenu, +#audioMenu2, +#effectsDiv, +#effectsDiv2, +#effectsDiv3, +#grabDirectorSoloLinkParent, +#videoMenu, +#videoMenu2, +#videoMenu3, +#headphonesDiv, +#headphonesDiv2, +#headphonesDiv3, +#videoSettings, +#videoSettings2, +#popupSelector_user_settings, +.invite_setting_group { + display: inline-block; + margin-top: 15px; + width: 463px; + max-width: 100%; + padding: 10px; + border-radius: 4px; + text-align: left; +} +#addPasswordBasic, +#avatarDiv, +#avatarDiv2, +#avatarDiv3, +#audioScreenShare1, +#audioMenu, +#audioMenu2, +#effectsDiv, +#effectsDiv2, +#effectsDiv3, +#grabDirectorSoloLinkParent, +#videoMenu, +#videoMenu2, +#videoMenu3, +#headphonesDiv, +#headphonesDiv2, +#headphonesDiv3, +#videoSettings, +#videoSettings2, +.invite_setting_group { + background-color: var(--lighttheme-2); + border: 1px solid var(--lighttheme-4); +} + +#videoSettings, #videoSettings2 { + width: 463px; + padding: 10px; + margin-top: -1px; + border-radius: 0px 0px 4px 4px; + text-align: center; +} +#videoSettings2{ + margin-top: 10px; + font-size: 95%; +} +#videoMenu .title{ + display: inline-block; + padding:0; +} +#videoMenu { + padding: 10px 10px 9px 10px; + vertical-align: middle; + text-align: left; + margin-top: 0px; +} +.disable { + opacity: 0.5; + cursor: not-allowed; +} + +.darktheme .invite_setting_group { + color: var(--discord-text); +} + +.invite_setting_group { + color: var(--lighttheme-text); +} + +.invite_setting_item { + margin: 10px 0px 0px 0; +} + +.invite_setting_group_links { + margin-top: 10px; +} + +#popupSelector { + background: linear-gradient(6deg, rgba(221, 221, 221, 0) 4%, rgb(0,0,0, 0.5) 30%, #646878A0 100%); + transition: all 0.2s linear 0s; + padding: 10px; + position: fixed; + top: 0px; + height: 100%; + width: 505px; + max-width: 100%; + right: -500px; + overflow: auto; + z-index: 4; + padding-bottom: 80px; +} +#popupSelector label { + color: black; +} +.darktheme #popupSelector label { + color: white; +} +#popupSelector .multiselect-contents label{ + color: black; +} +#popupSelector select { + padding: 4px; +} +.darktheme #popupSelector{ + background-color: #0004; +} +.popupSelector_constraints{ + margin-top: 10px; +} + +.popupSelector_constraints input[type='range'] { + margin-bottom: 10px; +} + +#popupSelector_constraints_video > div, #popupSelector_constraints_audio > div { + padding: 10px 10px 10px 0; +} +#popupSelector_constraints_audio label { + color:white; +} +#popupSelector_constraints_video label { + color:white; +} +#popupSelector_user_settings label { + color:white; +} +ul#audioSource{ + background-color: var(--lighttheme-1); + margin-top: 7px; + min-height: 24px; +} +ul#audioSource label{ + background-color: var(--lighttheme-1); + border: 0; +} +ul#audioSource label{ + color:black; +} +ul#audioSource3{ + background-color: var(--lighttheme-1); + border: 1px solid var(--lighttheme-6); +} +.darktheme ul#audioSource{ + background-color: var(--light-grey); + margin-top: 7px; +} +.darktheme ul#audioSource label{ + background-color: var(--light-grey); + border: 0; +} +.darktheme ul#audioSource3{ + background-color: var(--light-grey); + border: 1px solid var(--discord-grey-8); +} + +.darktheme #grabDirectorSoloLink { + background-color: var(--discord-grey-3); + border: 1px solid var(--discord-grey-8); +} +#refreshVideoButton > i{ + cursor: pointer; + font-size: 120%; + position: relative; + top: 3px; +} +#webcamquality3{ + margin-top: 10px; +} +#grabDirectorSoloLink { + display: inline-block; + font-size: 16px; + padding: 2px 6px; + width: 100%; + outline: 0; + border-radius: 4px; + background-color: var(--lighttheme-1); + border: 1px solid var(--lighttheme-6); +} + +#grabDirectorSoloLinkParent { + margin-bottom: 10px; +} + +/* INITIALLY HIDDEN */ +#advancedOptionsAudio, +#advancedOptionsCamera, +#effectsDiv { + display: none; +} +#advancedOptionsAudio, +#advancedOptionsCamera, +#advancedOptionsGeneral, +button.toggleSettings, +#effectsDiv { + padding: 10px; +} +#videoname1, #passwordRoom { + height:unset; +} + +#audioMenu ul { + list-style:none; +} + +.generalButton{ + border-radius: 3px; + padding: 5px; + max-width: 216px; + margin: 5px; +} + +.roomnotes { + margin-top: 10px; +} +.randomRoomName{ + position: absolute; + margin: 3px; +} +.randomRoomName:active{ + animation: shake 0.2s; + animation-iteration-count: once; +} +.randomRoomName:hover{ + -webkit-box-shadow: 0px 0px 4px #000; +} + +.pressed { + background: #1e0000 !important; + -webkit-box-shadow: inset 0px 0px 1px #b90000; + -moz-box-shadow: inset 0px 0px 1px #b90000; + box-shadow: inset 0px 0px 1px #b90000; + outline: none; + color: white; +} +/* ANIMATIONS */ + +/* Shake animation */ +.shake { + animation: shake 0.5s; + animation-iteration-count: once; +} +@keyframes shake { + 0% { transform: translate(1px, 1px) rotate(0deg); } + 10% { transform: translate(-1px, -2px) rotate(-1deg); } + 20% { transform: translate(-3px, 0px) rotate(1deg); } + 30% { transform: translate(3px, 2px) rotate(0deg); } + 40% { transform: translate(1px, -1px) rotate(1deg); } + 50% { transform: translate(-1px, 2px) rotate(-1deg); } + 60% { transform: translate(-3px, 1px) rotate(0deg); } + 70% { transform: translate(3px, 1px) rotate(-1deg); } + 80% { transform: translate(-1px, -1px) rotate(1deg); } + 90% { transform: translate(1px, 2px) rotate(0deg); } + 100% { transform: translate(1px, -2px) rotate(-1deg); } +} + + +/* Spin animation */ +@keyframes spin-animation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } +} + + +/* Flip 180 animations */ +/* NOTE: At least used by the flip camera button in control-bar on mobile */ +.flip { + animation: flip180 2s; + animation-iteration-count: 1; +} +@keyframes flip180 { + 0% {transform: rotate(0);} + 100% {transform: rotate(180deg);} +} + +.flip2 { + animation: flip1802 2s; + animation-iteration-count: 1; +} +@keyframes flip1802 { + 0% {transform: rotate(180deg)} + 100% {transform: rotate(360deg);} +} + + +/* Blink-warn / Blink-alert animations */ +/* NOTE: At least used by .battery */ +@keyframes blink-warn { + 0% { opacity: 0; } + 50% { opacity: 1; } + 100% { opacity: 0; } +} +@keyframes blink-alert { + 0% { opacity: 0; } + 50% { opacity: 1; } + 100% { opacity: 0; } +} + + +/* Floating animation */ +/* NOTE: At least used by .video-label */ +@keyframes floating { + 0% { transform: translate(0, 0px); } + 50% { transform: translate(0, 15%); } + 100% { transform: translate(0, -0px); } +} + + +/* Pulsating animations */ +/* NOTE: .pulsate is at least used by #mutetoggle */ +.pulsate { + box-shadow: 0 0 0 0 rgba(14, 19, 26, 1); + transform: scale(1); + animation: pulse 2s infinite; +} +@keyframes pulse { + 0% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(14, 19, 26, 0.7); + } + 15% { + transform: scale(1.2); + box-shadow: 0 0 0 10px rgba(2, 3, 4, 0); + } + 50% { + transform: scale(1.0); + box-shadow: 0 0 0 0 rgba(14, 19, 26, 0); + } + 85% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(14, 19, 26, 0); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(14, 19, 26, 0); + } +} +@keyframes pulsate { + 0% { box-shadow: 0 0 31px #244e1c44; transform: scale(1.0);} + 50% { box-shadow: 0 0 17px #0004; transform: scale(0.99);} + 100% { box-shadow: 0 0 31px #244e1c44; transform: scale(1.0);} +} +@-webkit-keyframes pulsate { + 0% { box-shadow: 0 0 31px #244e1c44; transform: scale(1.0);} + 50% { box-shadow: 0 0 17px #0004; transform: scale(0.99);} + 100% { box-shadow: 0 0 31px #244e1c44; transform: scale(1.0);} +} + + +/* Lightbox open animation */ +/* NOTE: For opening lightboxes on homepage, and a sidenote, the "outlightbox" equivalent for closeing + the lightboxes is found in lib.js */ +@keyframes inlightbox { + 50% { + width: 100%; + left: 0; + height: 200px; + } + + 100% { + height: 100%; + width: 100%; + top: 0; + left: 0; + } +} + + +/* Fade-in animation */ +.fadein { + animation: fadeIn var(--fadein-speed); + -webkit-animation: fadeIn var(--fadein-speed); + -moz-animation: fadeIn var(--fadein-speed); + -o-animation: fadeIn var(--fadein-speed); + -ms-animation: fadeIn var(--fadein-speed); + animation-iteration-count: 1; +} +@keyframes fadeIn { + 0% {opacity:0;} + 100% {opacity:1;} +} +@-moz-keyframes fadeIn { + 0% {opacity:0;} + 100% {opacity:1;} +} +@-webkit-keyframes fadeIn { + 0% {opacity:0;} + 100% {opacity:1;} +} +@-o-keyframes fadeIn { + 0% {opacity:0;} + 100% {opacity:1;} +} +@-ms-keyframes fadeIn { + 0% {opacity:0;} + 100% {opacity:1;} +} + + +/* Fade-out animation */ +.fadeout { + animation: fadeout 1s; + opacity: 0!important; +} +.partialFadeout{ + opacity: 0.2 !important; +} +@keyframes fadeout { + 0% { + opacity: 1 + } + 100% { + opacity: 0 + } +} + + +/* Greyout animation */ +.greyout { + animation: greyout 3s; + opacity: 0.3!important; +} +@keyframes greyout { + 0% { + opacity: 1 + } + 100% { + opacity: 0.3 + } +} + + +/** + * Below lsits the classes and font styles for the font-awesome icons. + */ + +@font-face { + font-family: 'Line Awesome Free'; + font-style: normal; + font-weight: 900; + font-display: auto; + src: url("lineawesome/fonts/la-solid-900.eot"); + src: url("lineawesome/fonts/la-solid-900.eot?#iefix") format("embedded-opentype"), url("lineawesome/fonts/la-solid-900.woff2") format("woff2"), url("lineawesome/fonts/la-solid-900.woff") format("woff"), url("lineawesome/fonts/la-solid-900.ttf") format("truetype"), url("lineawesome/fonts/la-solid-900.svg#lineawesome") format("svg"); +} +.las { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; } +.las { + font-family: 'Line Awesome Free'; + font-weight: 900; } +.la-bell:before { + content: "\f0f3"; } +.la-bell-slash:before { + content: "\f1f6"; } +.la-long-arrow-alt-right:before { + content: "\f30b"; } +.la-paperclip:before { + content: "\f0c6"; } +.la-window-close:before { + content: "\f410"; } +.la-stream:before { + content: "\f550"; } +.la-file-upload:before { + content: "\f574"; } +.la-comment-alt:before { + content: "\f27a"; } +.la-tv:before { + content: "\f26c"; } +.la-volume-up:before { + content: "\f028"; } +.la-comment-dots:before { + content: "\f4ad"; } +.la-microphone:before { + content: "\f130"; } +.la-microphone-alt:before { + content: "\f3c9"; } +.la-video:before { + content: "\f03d"; } +.la-desktop:before { + content: "\f108"; } +.la-tv:before { + content: "\f26c"; } +.la-window-maximize:before { + content: "\f2d0"; } +.la-sync-alt:before { + content: "\f2f1"; } +.la-users-cog:before { + content: "\f509"; } +.la-cog:before { + content: "\f013"; } +.la-phone:before { + content: "\f095"; } +.la-gamepad:before { + content: "\f11b"; } +.la-user-slash:before { + content: "\f506"; } +.la-skull-crossbones:before { + content: "\f714"; } +.la-hand-paper:before { + content: "\f256"; } +.la-phone-slash:before { + content: "\f3dd;"; } +.la-dot-circle:before { + content: "\f192"; } +.la-bug:before { + content: "\f188"; } +.la-question-circle:before { + content: "\f059"; } +.la-language:before { + content: "\f1ab"; } +.la-calendar:before { + content: "\f073"; } +.la-exclamation-circle:before { + content: "\f06a"; } +.la-plug:before { + content: "\f1e6"; } +.la-ethernet:before { + content: "\f796"; } +.la-headphones:before { + content: "\f025"; } +.la-robot:before { + content: "\f544"; } +.la-info-circle:before { + content: "\f05a"; } +.la-play:before { + content: "\f04b"; } +.la-gamepad:before { + content: "\f11b"; } +.la-file-video:before { + content: "\f1c8"; } +.la-blender:before { + content: "\f517"; } +.la-heartbeat:before { + content: "\f21e"; } +.la-code-branch:before { + content: "\f126"; } +.la-info:before { + content: "\f129"; } +.la-square:before { + content: "\f0c8"; } +.la-play-circle:before { + content: "\f144"; } +.la.la-hdd-o:before { + content: "\f0a0"; } +.la-key:before { + content: "\f084"; } +.la-broadcast-tower:before { + content: "\f519"; } +.la-clock:before { + content: "\f017"; } +.la-tachometer-alt:before { + content: "\f3fd"; } +.la-fire-alt:before { + content: "\f7e4"; } +.la-book-open:before { + content: "\f518"; } +.la-caret-down:before { + content: "\f0d7"; } +.la-comments:before { + content: "\f086"; } +.la-caret-right:before { + content: "\f0da"; } +.la-copy:before { + content: "\f0c5"; } +.la-tools:before { + content: "\f7d9"; } +.la-th-large:before { + content: "\f009"; } +.la-user-circle:before { + content: "\f2bd"; } +.la-paper-plane:before { + content: "\f1d8"; } +.la-envelope:before { + content: "\f0e0"; } +.la-sign-out-alt:before { + content: "\f2f5"; } +.la-angle-right:before { + content: "\f105"; } +.la-angle-left:before { + content: "\f104"; } +.la-external-link-square-alt:before { + content: "\f360"; } +.la-plus-square:before { + content: "\f0fe"; } +.la-microphone-slash:before { + content: "\f131"; } +.la-user:before { + content: "\f007"; } +.la-video-slash:before { + content: "\f4e2"; } +.la-volume-off:before { + content: "\f026"; } +.la-eye-slash:before { + content: "\f070"; } +.la-eye:before { + content: "\f06e"; } +.la-minus:before { + content: "\f068"; } +.la-minus-circle:before { + content: "\f056"; } +.la-window-minimize:before { + content: "\f2d1"; } +.la-hat-wizard:before { + content: "\f6e8"; } +.la-plus:before { + content: "\f067"; } +.la-sync:before { + content: "\f021"; } +.la-circle:before { + content: "\f111"; } +.la-chevron-left:before { + content: "\f053"; } +.la-chevron-right:before { + content: "\f054"; } +.la-binoculars:before { + content: "\f1e5"; } +.la-user-cog:before { + content: "\f4fe"; } +.la-stop-circle:before { + content: "\f28d"; } +.la-redo-alt:before { + content: "\f2f9"; } +.la-sliders-h:before { + content: "\f1de"; } +.la-compress-arrows-alt:before { + content: "\f78c"; } +.la-users:before { + content: "\f0c0"; } +.la-spinner:before { + content: "\f110"; } +.la-external-link:before { + content: "\f35d"; } +.la-pen:before { + content: "\f304"; } +.la-external-link-alt:before { + content: "\f35d"; } +.la-times:before { + content: "\f00d"; } +.la-volume-mute:before { + content: "\f6a9"; } +.la-plug:before { + content: "\f1e6"; } +.la-reply:before { + content: "\f3e5"; } +.la-expand-arrows-alt:before { + content: "\f31e"; } +.la-headset:before { + content: "\f590"; } +.la-check:before { + content: "\f00c"; } +.la-exclamation:before { + content: "\f12a"; } +.la-chevron-down:before { + content: "\f078"; } +.la-music:before { + content: "\f001"; } +.la-thumbtack:before { + content: "\f08d"; } +.la-hdd:before { + content: "\f0a0"; } +.la-signal:before { + content: "\f012"; } +.la-unlock:before { + content: "\f023"; } +.la-lock-open:before { + content: "\f3c1"; } +.la-theater-masks:before { + content: "\f630"; } +.la-compact-disc:before { + content: "\f51f"; } +.la-random:before { + content: "\f074"; } +.la-moon:before { + content: "\f186"; } +.la-mobile:before { + content: "\f10b"; } +.la-podcast:before { + content: "\f2ce"; } +.la-chalkboard:before { + content: "\f51b"; } +.la-project-diagram:before { + content: "\f542"; } +.la-qrcode:before { + content: "\f029"; } +.la-times-circle:before { + content: "\f057"; } +.la-sun:before { + content: "\f185"; } + + +/* Minimal visual refresh overrides — non-invasive, CSS-only. */ +/* CHANGES BELOW MADE 2025-08-22 */ + +/* 1) Palette and radii overrides via existing tokens */ +:root { + --button-radius: 6px; + --lighttheme-2: #f6f7f9; /* surfaces */ + --lighttheme-3: #eceef2; /* containers */ + --lighttheme-4: #d9dde5; /* borders */ + --lighttheme-6: #c3c7d0; /* subtle lines */ + --lighttheme-text: #111; + + /* keep original semantics; just slightly modernize */ + --container-color: #3a3d45; /* dark surface used in many cards */ + + /* global accent (aligned with about.html) */ + --accent-color: #56a5ea; + --accent-hover-color: #7AACD2; + + /* unified link colors (light theme) — aligned with about.html accent */ + --a-link: #3e8fd8; /* base: #4299e1 softened */ + --a-visited: #3e8fd8; + --a-hover: #56a5ea; /* lighter hover */ + --a-focus: #56a5ea; + --a-active: #2f7ec4; + + /* lighter links used in specific sections */ + --a-lighter-link: #2d6aa6; + --a-lighter-visited: #2d6aa6; + --a-lighter-hover: #3e7fbd; + --a-lighter-focus: #3e7fbd; + --a-lighter-active: #246295; +} + +@media (prefers-color-scheme: dark) { + :root { + /* soften Discord greys slightly */ + --discord-grey-5: #2f3237; + --discord-grey-7: #3a3d45; + --discord-text: hsl(210 10% 92% / 1); + /* muted icon color to match subdued typography */ + --muted-icon: #c8ccd4; + /* unified link colors (dark theme) */ + --a-dark-link: #9ec7ee; /* tinted to about.html accent */ + --a-dark-visited: #9ec7ee; + --a-dark-hover: #b9d8f6; + --a-dark-focus: #b9d8f6; + --a-dark-active: #8abbe8; + --a-darker-link: #bcd8f4; + --a-darker-visited: #bcd8f4; + --a-darker-hover: #cfe6fb; + --a-darker-focus: #cfe6fb; + --a-darker-active: #a9cdef; + /* accent for dark UI */ + --accent-color: #498dc2; + --accent-hover-color: #6FA5CF; + } +} + +/* 14) SSO options panel (light/dark aware) */ +.sso-options-panel { + margin-top: 8px; + padding: 10px; + border-radius: 8px; + max-width: 470px; + background: var(--lighttheme-2); + border: 1px solid var(--lighttheme-4); + color: var(--lighttheme-text); +} + +body.darktheme .sso-options-panel { + background: var(--discord-grey-5); + border: 1px solid var(--discord-grey-7); + color: var(--discord-text); +} + +.sso-options-panel label { + color: inherit; +} + +.sso-options-input { + width: 100%; + padding: 6px 8px; + border-radius: 6px; + border: 1px solid var(--lighttheme-4); + background: #fff; + color: var(--lighttheme-text); +} + +body.darktheme .sso-options-input { + border: 1px solid var(--discord-grey-7); + background: var(--discord-grey-3); + color: var(--discord-text); +} + +.sso-options-note { + margin-top: 8px; + font-size: 12px; + color: #444; +} + +body.darktheme .sso-options-note { + color: var(--muted-icon); +} + +/* 2) Typographic refresh (safe defaults) */ +body { + font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* subtle depth */ + background-image: radial-gradient(1200px 800px at 10% -10%, rgba(255,255,255,0.025), transparent 60%), + radial-gradient(1000px 600px at 110% 0%, rgba(0,0,0,0.08), transparent 60%); +} + +/* 3) Cards and columns (landing containers) */ +.column.card, +.column { + /* Use solid backgrounds to preserve icon contrast and avoid transparency */ + background-color: var(--lighttheme-3) !important; + backdrop-filter: saturate(120%) blur(2px); + border: 1px solid var(--lighttheme-4); + border-radius: 14px; + box-shadow: 0 2px 10px rgba(0,0,0,0.08), 0 1px 0 rgba(255,255,255,0.02) inset; +} +.darktheme .column.card, +.darktheme .column { + background-color: var(--discord-grey-7) !important; + border: 1px solid #4a4d55; + box-shadow: 0 2px 12px rgba(0,0,0,0.35), 0 1px 0 rgba(255,255,255,0.02) inset; +} + +.column:hover { + transform: translateY(-2px); + box-shadow: 0 10px 24px rgba(0,0,0,0.15); +} + +.column { + padding: 25px 17px; +} +/* Prevent hover-lift effect when a tile is expanded full-window */ +.skip-animation, +.in-animation { + transition: none !important; +} +.skip-animation:hover, +.in-animation:hover { + transform: none !important; + box-shadow: none !important; +} + +/* 4) Container content panels */ +.container-inner { + padding: 14px 16px; + border-radius: 12px; +} +/* preserve existing backgrounds from main.css; no overrides here */ + +/* 5) Buttons and inputs (respect existing tokens) */ +button { + border-radius: var(--button-radius); + transition: box-shadow .15s ease, transform .08s ease, filter .15s ease; +} +button:hover { + box-shadow: 0 6px 14px rgba(0,0,0,0.12); +} +button:active { + transform: translateY(1px); +} + +/* Accessible focus treatment */ +:where(a, button, [role="button"], input, select, textarea):focus-visible { + outline: 2px solid var(--a-hover); + outline-offset: 2px; +} +.darktheme :where(a, button, [role="button"], input, select, textarea):focus-visible { + outline-color: var(--a-dark-hover); +} + +input[type="text"], input[type="search"], input[type="number"], select { + border: 1px solid var(--lighttheme-4); + background: var(--lighttheme-2); + color: var(--lighttheme-text); + border-radius: 10px; + padding: 6px 10px; +} +.darktheme input[type="text"], +.darktheme input[type="search"], +.darktheme input[type="number"], +.darktheme select { + border: 1px solid #4a4d55; + background: var(--discord-grey-5); + color: var(--discord-text); +} + +/* 17) Radios/checkboxes: make checked state feel active */ +input[type="radio"], +input[type="checkbox"] { + accent-color: var(--accent-color); +} +.darktheme input[type="radio"], +.darktheme input[type="checkbox"] { + accent-color: var(--accent-color); +} +input[type="radio"] + label, +input[type="checkbox"] + label { cursor: pointer; } +input[type="radio"]:checked + label, +input[type="checkbox"]:checked + label { + color: var(--a-link); + font-weight: 600; +} +/* Override: do not color labels on selection — only the control gets color */ +input[type="radio"]:checked + label, +input[type="checkbox"]:checked + label { + color: inherit !important; + font-weight: inherit !important; +} +.darktheme input[type="radio"]:checked + label, +.darktheme input[type="checkbox"]:checked + label { + color: inherit !important; +} + +/* Higher-contrast placeholders (theme-aware) */ +input::placeholder, +textarea::placeholder { + color: #6b7686 !important; /* slightly darker than default for readability */ +} +.darktheme :not(.promptModalInner) > input::placeholder, +.darktheme :not(.promptModalInner) > textarea::placeholder { + color: var(--muted-icon, #c8ccd4) !important; /* brighter on dark for contrast */ +} + +/* Vendor placeholder fallbacks */ +::-webkit-input-placeholder { color: #6b7686 !important; } +::-moz-placeholder { color: #6b7686 !important; } +:-ms-input-placeholder { color: #6b7686 !important; } +:-moz-placeholder { color: #6b7686 !important; } +.darktheme ::-webkit-input-placeholder { color: var(--muted-icon, #c8ccd4) !important; } +.darktheme ::-moz-placeholder { color: var(--muted-icon, #c8ccd4) !important; } +.darktheme :-ms-input-placeholder { color: var(--muted-icon, #c8ccd4) !important; } +.darktheme :-moz-placeholder { color: var(--muted-icon, #c8ccd4) !important; } + + +/* 13) High-contrast treatment for copy links (reshare) */ +/* The anchor element itself carries .grabLinks; ensure we target it directly */ +a.grabLinks:link, +a.grabLinks:visited { + color: var(--a-link) !important; +} +.darktheme a.grabLinks:link, +.darktheme a.grabLinks:visited { + color: var(--a-dark-link); + text-shadow: 0 1px 0 rgba(0,0,0,0.6); +} +a.grabLinks:hover, +a.grabLinks:active { + color: var(--a-hover) !important; +} +.darktheme a.grabLinks:hover, +.darktheme a.grabLinks:active { + color: var(--a-dark-hover) !important; +} + +/* Copy-this-URL label: increase contrast subtly */ +#copythisurl { color: #5b6574 !important; } +.darktheme #copythisurl { color: var(--muted-icon) !important; } + +/* 13b) Fix dark-mode audio source multiselect contrast */ +.darktheme ul#audioSource, +.darktheme ul#audioSource3 { + background-color: var(--discord-grey-6) !important; + border: 1px solid var(--discord-grey-8) !important; +} +.darktheme ul#audioSource label, +.darktheme ul#audioSource3 label { + background-color: transparent !important; + color: var(--discord-text) !important; +} + +/* Ensure selected audio source labels don't turn white */ +ul#audioSource input:checked + label, +ul#audioSource3 input:checked + label { + color: #111 !important; +} +.darktheme ul#audioSource input:checked + label, +.darktheme ul#audioSource3 input:checked + label { + color: var(--discord-text) !important; +} + +/* 8) Director/Guest toolbar polish (glass, non-invasive) */ +#subControlButtons { + background-color: var(--container-color); + backdrop-filter: blur(8px) saturate(120%); + -webkit-backdrop-filter: blur(8px) saturate(120%); + border: 1px solid rgba(0,0,0,0.08); + box-shadow: 0 8px 24px rgba(0,0,0,0.15); +} +.darktheme #subControlButtons { + background-color: rgba(28, 31, 36, 0.6); + border: 1px solid rgba(255,255,255,0.08); + box-shadow: 0 10px 28px rgba(0,0,0,0.35); +} + +#subControlButtons div, #subControlButtons span button { + border-radius: 10px; + box-shadow: 0 1px 2px rgba(0,0,0,0.25); +} +#subControlButtons div:hover { border-radius: 8px; } + +/* Controls grid buttons: consistent shape & hover */ +.controlsGrid button { + box-shadow: 0 1px 2px rgba(0,0,0,0.2); + transition: transform .08s ease, box-shadow .15s ease, filter .15s ease; +} +.controlsGrid button:hover { + transform: translateY(-1px); + box-shadow: 0 6px 14px rgba(0,0,0,0.18); +} + +/* Director message box: glass pane */ +.controlsGrid .director-message-box { + background: rgba(255,255,255,0.6); + backdrop-filter: blur(6px) saturate(120%); + -webkit-backdrop-filter: blur(6px) saturate(120%); + border: 1px solid rgba(0,0,0,0.08); +} +.darktheme .controlsGrid .director-message-box { + background: rgba(58,61,69,0.55); + border: 1px solid rgba(255,255,255,0.08); +} +.darktheme .controlsGrid .director-message-box textarea { + background-color: rgba(64,66,73,0.7); + backdrop-filter: blur(2px); +} + +/* 9) Optional: utility class for explicit glass usage (not applied anywhere yet) */ +.glass { + background-color: rgba(255,255,255,0.6); + backdrop-filter: blur(10px) saturate(120%); + -webkit-backdrop-filter: blur(10px) saturate(120%); + border: 1px solid rgba(0,0,0,0.08); + box-shadow: 0 10px 28px rgba(0,0,0,0.18); +} +.darktheme .glass { + background-color: rgba(28,31,36,0.6); + border: 1px solid rgba(255,255,255,0.08); +} + +/* 13) Close buttons and director link icons — unify intensity */ +.close-btn, .overlayCloseBtn { color: #5b6574; } +.close-btn:hover, .overlayCloseBtn:hover { color: var(--a-hover); } +.darktheme .close-btn, .darktheme .overlayCloseBtn { color: var(--muted-icon); } +.darktheme .close-btn:hover, .darktheme .overlayCloseBtn:hover { color: var(--a-dark-hover); } + +.director-link-icons .las { color: #5b6574; } +.director-link-icons .las:hover { color: var(--a-hover); } +.darktheme .director-link-icons .las { color: var(--muted-icon); } +.darktheme .director-link-icons .las:hover { color: var(--a-dark-hover); } + +/* 14) Modal glass and alert polish (safe: background/border only) */ +.modal-content, +.alertModalInner { + background-color: rgba(255,255,255,0.80) !important; + backdrop-filter: blur(8px) saturate(120%); + -webkit-backdrop-filter: blur(8px) saturate(120%); + border: 1px solid rgba(0,0,0,0.08) !important; + border-radius: 10px !important; +} +.darktheme .modal-content, +.darktheme .alertModalInner { + background-color: rgba(230,230,230,0.70) !important; + border: 1px solid rgba(255,255,255,0.08) !important; +} + +/* 15) Chat send button harmony */ +.chatBarInputButton { + background-color: var(--accent-color) !important; + color: #000 !important; + border: 1px solid #2f7ec4 !important; + border-radius: 8px !important; +} +.chatBarInputButton:hover { + background-color: var(--accent-hover-color) !important; +} + +/* 16) Solo link accent harmonization */ +a.soloLink:link, a.soloLink:visited { color: var(--a-link); } +.darktheme a.soloLink:link, .darktheme a.soloLink:visited { color: var(--a-dark-link); } +a.soloLink:hover { color: var(--a-hover); } +.darktheme a.soloLink:hover { color: var(--a-dark-hover); } + +/* 10) Header glass — subtle and safe */ +#header { + background-color: rgba(15, 19, 29, 0.60) !important; + backdrop-filter: blur(6px) saturate(120%); + -webkit-backdrop-filter: blur(6px) saturate(120%); + border-bottom: 1px solid rgba(255,255,255,0.06); +} +.darktheme #header { + background-color: rgba(14, 17, 24, 0.60) !important; +} + +/* 11) Chat module glass and cohesion */ +#chatModule { + background-color: rgba(255,255,255,0.75) !important; + backdrop-filter: blur(10px) saturate(120%); + -webkit-backdrop-filter: blur(10px) saturate(120%); + border: 1px solid rgba(0,0,0,0.08) !important; + border-radius: 10px !important; + overflow: hidden; +} +.darktheme #chatModule { + background-color: rgba(28,31,36,0.65) !important; + border: 1px solid rgba(255,255,255,0.08) !important; +} +.chat-header { + background-color: transparent !important; + border-bottom: 1px solid rgba(0,0,0,0.08); +} +.darktheme .chat-header { border-bottom-color: rgba(255,255,255,0.08); } +.chat-input-area { background-color: transparent !important; } + +/* Chat links harmonized with accent */ +div#chatBody a { + color: var(--a-link) !important; + background-color: rgba(62,143,216,0.1) !important; + border: 1px solid currentColor !important; +} +.darktheme div#chatBody a { + color: var(--a-dark-link) !important; + background-color: rgba(158,199,238,0.10) !important; + border-color: var(--a-dark-link) !important; +} + +/* 12) Primary CTA harmony */ +.gobutton { + background-color: var(--accent-color) !important; + border-color: #2f7ec4 !important; +} +.gobutton:hover { + background-color: var(--accent-hover-color) !important; + border-color: #2f7ec4 !important; +} + +/* 20) Message cards — softer backgrounds and accents */ +.message-card { border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); } +.warning.message-card { background: #fffbe8; border-left: 4px solid #f2df75; } +.info.message-card { background: #eef3fb; border-left: 4px solid #9fc4f5; } +.darktheme .message-card { background-color: #2f3237; box-shadow: 0 8px 24px rgba(0,0,0,0.35); } +.darktheme .warning.message-card { background: #3a3d45; border-left-color: #f2df75; } +.darktheme .info.message-card { background: #363b46; border-left-color: #9fc4f5; } + +/* 21) Tooltips — readability and polish */ +.tooltip .tooltiptext { + background-color: rgba(15,19,29,0.92) !important; + color: #e8eef6 !important; + border: 1px solid rgba(255,255,255,0.06); + border-radius: 8px; + box-shadow: 0 10px 24px rgba(0,0,0,0.25); +} +.darktheme .tooltip .tooltiptext { background-color: rgba(34,37,44,0.95) !important; } + +/* 22) Sliders — theme-aware styling */ +input[type="range"] { -webkit-appearance: none; background: transparent; min-height: 24px; } +input[type="range"]:focus { outline: none; } +/* WebKit */ +input[type="range"]::-webkit-slider-runnable-track { height: 6px; background: var(--lighttheme-4); border-radius: 999px; } +input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--accent-color); border: 1px solid #2f7ec4; margin-top: -5px; box-shadow: 0 1px 2px rgba(0,0,0,0.3); } +/* Firefox */ +input[type="range"]::-moz-range-track { height: 6px; background: var(--lighttheme-4); border-radius: 999px; } +input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--accent-color); border: 1px solid #2f7ec4; box-shadow: 0 1px 2px rgba(0,0,0,0.3); } +input[type="range"]::-moz-range-progress { height: 6px; background: var(--accent-color); border-radius: 999px; } +.darktheme input[type="range"]::-webkit-slider-runnable-track { background: var(--discord-grey-6); } +.darktheme input[type="range"]::-moz-range-track { background: var(--discord-grey-6); } +.darktheme input[type="range"]::-moz-range-progress { background: var(--a-dark-link); } +.darktheme input[type="range"]::-webkit-slider-thumb { background: var(--a-dark-link); border-color: #6b8db3; } +.darktheme input[type="range"]::-moz-range-thumb { background: var(--a-dark-link); border-color: #6b8db3; } + +/* 23) Non-expanded tiles — prevent scrollbars */ +.column.card:not(.skip-animation):not(.in-animation) { overflow-y: hidden !important; } + +/* 25) Create a Room (#container-1) — reduce boldness, improve contrast */ +.darktheme #container-1 th b, +.darktheme #container-1 td b { font-weight: 400 !important; } +.darktheme #container-1 th, +.darktheme #container-1 td, +.darktheme #container-1 label { color: #fff !important; } +.darktheme #container-1 [data-translate="guests-only-see-director"], +.darktheme #container-1 [data-translate="scenes-can-see-director"] { color: #fff !important; font-weight: 400 !important; } + +.invite_setting_group { + padding: 20px; +} + +#videoname4 { + padding: 25px; +} + +/* Subtle vertical balance for all tile headings */ +.column.card > i { margin-top: 17px!important;} + +.directorContainer, #guestFeeds, .hideLinksClass { + border-radius: 4px; + backdrop-filter: blur(10px) saturate(120%); + -webkit-backdrop-filter: blur(10px) saturate(120%); + border: 1px solid rgba(0,0,0,0.08); + box-shadow: 0 10px 28px rgba(0,0,0,0.18); +} + +/* Tipping Feature Styles */ +.tipModal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 100000; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.tipModalInner { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border-radius: 16px; + padding: 28px; + max-width: 420px; + width: 90%; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1); + color: #fff; + position: relative; +} + +.tipModalInner h3 { + margin: 0 0 20px 0; + font-size: 1.3em; + font-weight: 600; + text-align: center; + color: #fff; +} + +.tipCloseBtn { + position: absolute; + top: 12px; + right: 12px; + background: rgba(255, 255, 255, 0.1); + border: none; + color: #fff; + width: 32px; + height: 32px; + border-radius: 50%; + cursor: pointer; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; +} + +.tipCloseBtn:hover { + background: rgba(255, 255, 255, 0.2); +} + +.tipAmounts { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 10px; + margin-bottom: 18px; +} + +.tipAmountBtn { + background: rgba(255, 255, 255, 0.08); + border: 2px solid transparent; + color: #fff; + padding: 14px 8px; + border-radius: 10px; + cursor: pointer; + font-size: 1.1em; + font-weight: 600; + transition: all 0.2s; +} + +.tipAmountBtn:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-2px); +} + +.tipAmountBtn.selected { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + border-color: rgba(255, 255, 255, 0.3); + box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4); +} + +.tipAmountBtn.custom { + font-size: 0.9em; + font-weight: 500; +} + +.tipCustomInput { + width: 100%; + padding: 14px 16px; + background: rgb(0, 100, 100) !important; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 10px; + color: #fff !important; + color: white !important; + font-size: 1em; + margin-bottom: 12px; + box-sizing: border-box; + transition: border-color 0.2s, background 0.2s; + -webkit-text-fill-color: #fff !important; + -webkit-text-fill-color: white !important; + caret-color: #fff; + color-scheme: dark; +} + +.tipCustomInput:focus { + outline: none; + border-color: rgba(100, 255, 218, 0.7); + background: rgb(0, 100, 100) !important; + color: #fff !important; + color: white !important; + -webkit-text-fill-color: #fff !important; + -webkit-text-fill-color: white !important; + color-scheme: dark; +} + +.tipCustomInput::placeholder { + color: rgba(255, 255, 255, 0.6); + -webkit-text-fill-color: rgba(255, 255, 255, 0.6); +} + +/* Override browser autofill styles */ +.tipCustomInput:-webkit-autofill, +.tipCustomInput:-webkit-autofill:hover, +.tipCustomInput:-webkit-autofill:focus, +.tipCustomInput:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px rgb(0, 100, 100) inset !important; + -webkit-text-fill-color: #fff !important; + caret-color: #fff !important; +} + +/* Ensure number inputs also style correctly */ +.tipCustomInput[type="number"] { + -moz-appearance: textfield; +} + +.tipCustomInput[type="number"]::-webkit-inner-spin-button, +.tipCustomInput[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.tipInputGroup { + margin-bottom: 12px; +} + +.tipInputGroup label { + display: block; + margin-bottom: 6px; + font-size: 0.85em; + color: rgba(255, 255, 255, 0.7); +} + +.tipCardElement { + background: rgb(0, 120, 120); + padding: 14px 16px; + border-radius: 10px; + margin-bottom: 10px; + min-height: 24px; +} + +.tipCardElement label { + color: rgba(255, 255, 255, 0.9); +} + +.tipStripeBadge { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + margin-bottom: 16px; + font-size: 11px; + color: rgba(255, 255, 255, 0.6); +} + +.tipStripeBadge svg { + height: 16px; + width: auto; +} + +.tipConfirmBtn { + width: 100%; + padding: 16px; + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); + border: none; + border-radius: 10px; + color: #fff; + font-size: 1.1em; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 4px 15px rgba(56, 239, 125, 0.3); +} + +.tipConfirmBtn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(56, 239, 125, 0.4); +} + +.tipConfirmBtn:disabled { + background: rgba(255, 255, 255, 0.2); + cursor: not-allowed; + box-shadow: none; +} + +.tipConfirmBtn.processing { + background: rgba(255, 255, 255, 0.3); +} + +.tipError { + background: rgba(255, 82, 82, 0.2); + border: 1px solid rgba(255, 82, 82, 0.5); + color: #ff6b6b; + padding: 12px; + border-radius: 8px; + margin-bottom: 14px; + font-size: 0.9em; + text-align: center; +} + +.tipSuccess { + background: rgba(56, 239, 125, 0.2); + border: 1px solid rgba(56, 239, 125, 0.5); + color: #38ef7d; + padding: 12px; + border-radius: 8px; + margin-bottom: 14px; + font-size: 0.9em; + text-align: center; +} + +.tipPoweredBy { + text-align: center; + margin-top: 16px; + font-size: 0.75em; + color: rgba(255, 255, 255, 0.4); +} + +.tipPoweredBy a { + color: rgba(255, 255, 255, 0.6); + text-decoration: none; +} + +.tipPoweredBy a:hover { + color: rgba(255, 255, 255, 0.8); +} + +.tipTotal { + text-align: center; + margin-bottom: 14px; + font-size: 1.1em; + color: rgba(255, 255, 255, 0.9); +} + +.tipTotal span:last-child { + font-weight: 700; + background: linear-gradient(135deg, #f093fb, #f5576c); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.tipModalHeader { + text-align: center; + margin-bottom: 20px; +} + +.tipModalHeader h3 { + margin: 0; + font-size: 1.3em; +} + +/* Tip Button in Video Controls */ +.tipButton { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%) !important; + border: none !important; + position: relative; + overflow: hidden; +} + +.tipButton::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255,255,255,0.3), transparent); + transform: rotate(45deg); + animation: tipButtonShine 3s infinite; +} + +@keyframes tipButtonShine { + 0% { left: -50%; } + 100% { left: 150%; } +} + +/* Tip Chat Message Styling */ +.tip-notification { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + padding: 8px 12px; + background: linear-gradient(135deg, rgba(240, 147, 251, 0.15) 0%, rgba(245, 87, 108, 0.15) 100%); + border-radius: 8px; + border-left: 3px solid #f093fb; +} + +.tip-icon { + font-size: 1.2em; + margin-right: 4px; +} + +.tip-amount { + font-weight: 700; + font-size: 1.15em; + background: linear-gradient(135deg, #f093fb, #f5576c); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.tip-sender { + font-weight: 600; + color: #fff; +} + +.tip-message { + font-style: italic; + color: rgba(255, 255, 255, 0.85); + margin-left: 4px; +} + +/* Chat message container for tips */ +.message.tip-message-container { + background: linear-gradient(135deg, rgba(240, 147, 251, 0.08) 0%, rgba(245, 87, 108, 0.08) 100%); + border-radius: 8px; + margin: 4px 0; + padding: 4px; +} + +/* Loading spinner for tip processing */ +.tipSpinner { + display: inline-block; + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: #fff; + border-radius: 50%; + animation: tipSpin 0.8s linear infinite; + margin-right: 8px; + vertical-align: middle; +} + +@keyframes tipSpin { + to { transform: rotate(360deg); } +} + +/* Mix dropdown styles for director audio source selection */ +.mix-dropdown { + position: absolute; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 4px; + padding: 8px; + z-index: 1000; + min-width: 220px; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + left: 0; + top: 100%; + margin-top: 4px; +} + +.mix-dropdown-header { + color: #aaa; + font-size: 11px; + text-transform: uppercase; + padding: 4px 8px; + border-bottom: 1px solid #444; + margin-bottom: 4px; +} + +.mix-dropdown-section { + margin-bottom: 8px; +} + +.mix-dropdown-section-title { + color: #888; + font-size: 10px; + padding: 4px 8px; + text-transform: uppercase; +} + +.mix-dropdown-item { + display: flex; + align-items: center; + padding: 6px 8px; + cursor: pointer; + border-radius: 3px; + color: #ddd; + font-size: 12px; +} + +.mix-dropdown-item:hover { + background: #3a3a3a; +} + +.mix-dropdown-item input[type="checkbox"] { + margin-right: 8px; + cursor: pointer; +} + +.mix-dropdown-item label { + cursor: pointer; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mix-dropdown-container { + position: relative; +} + +/* Dark theme adjustments */ +.darktheme .mix-dropdown { + background: #1a1a1a; + border-color: #444; +} + +/* Tip Icon Overlay on Videos (two-way opt-in) */ +.tipIconOverlay { + position: absolute; + top: 8px; + left: 8px; + cursor: pointer; + z-index: 10; + display: flex; + align-items: center; + gap: 6px; +} + +.tipIconOverlay:hover { + transform: scale(1.05); +} + +/* Heart icon container - CSS heart shape */ +.tipHeart { + position: relative; + width: 36px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); + transition: opacity 0.8s ease, transform 0.8s ease; + flex-shrink: 0; +} + +/* Heart shape using CSS */ +.tipHeart::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 36px; + height: 32px; + background: rgba(229, 57, 53, 0.95); + clip-path: path('M18 28 C4 18, 0 8, 9 3 C14 0, 18 4, 18 8 C18 4, 22 0, 27 3 C36 8, 32 18, 18 28 Z'); +} + +/* Dollar sign inside heart */ +.tipDollar { + position: relative; + z-index: 1; + color: white; + font-size: 16px; + font-weight: 700; + font-family: Arial, sans-serif; + line-height: 1; + margin-top: -2px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.tipIconOverlay:hover .tipHeart::before { + background: rgba(229, 57, 53, 1); +} + +/* "Send a Tip" tooltip label */ +.tipLabel { + background: rgba(0, 0, 0, 0.85); + color: white; + padding: 5px 10px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; + opacity: 0; + transform: translateX(-5px); + transition: opacity 0.2s ease, transform 0.2s ease; + pointer-events: none; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); +} + +/* Show label state (via JS class) */ +.tipIconOverlay.showLabel .tipLabel { + opacity: 1; + transform: translateX(0); +} + +/* Show tooltip on hover (immediate) */ +.tipIconOverlay:hover .tipLabel { + opacity: 1; + transform: translateX(0); +} + +/* QR code container - positioned below heart for OBS/scene mode */ +.tipQR { + position: absolute; + top: 0; + left: 0; + background: white; + border-radius: 6px; + padding: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + opacity: 0; + transform: scale(0.8); + transition: opacity 0.8s ease, transform 0.8s ease; + pointer-events: none; +} + +/* Show QR code state (hides heart, shows QR) */ +.tipIconOverlay.showQR .tipHeart, +.tipIconOverlay.showQR .tipLabel { + opacity: 0; + transform: scale(0.8); +} + +.tipIconOverlay.showQR .tipQR { + opacity: 1; + transform: scale(1); + pointer-events: auto; +} + +/* Smaller heart when no QR mode */ +.tipIconOverlay.noQR .tipHeart { + width: 30px; + height: 27px; +} + +.tipIconOverlay.noQR .tipHeart::before { + width: 30px; + height: 27px; + clip-path: path('M15 24 C3 15, 0 7, 7.5 2.5 C12 0, 15 3, 15 6.5 C15 3, 18 0, 22.5 2.5 C30 7, 27 15, 15 24 Z'); +} + +.tipIconOverlay.noQR .tipDollar { + font-size: 14px; +} + +/* Hide tip icon in clean mode */ +body.cleanOutput .tipIconOverlay, +.cleanOutput .tipIconOverlay { + display: none !important; +} + +/* Tip Onboarding Modal - matches .promptModal style */ +#tipOnboardingModal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgb(221 221 221); + box-shadow: 0 0 30px 10px #0000005c; + color: black; + font-weight: bold; + border-radius: 10px; + padding: 0; + max-width: 500px; + width: 90%; + z-index: 9999; + overflow: hidden; +} + +.darktheme #tipOnboardingModal { + background-color: var(--discord-grey-5); + color: var(--discord-text); +} + +#tipOnboardingModal .promptModalInner { + padding: 1em; +} + +#tipOnboardingModal h2 { + margin: 0 0 12px 0; + font-size: 1.1em; +} + +#tipOnboardingModal h4 { + margin: 16px 0 6px 0; + font-size: 0.95em; +} + +#tipOnboardingModal p { + margin: 6px 0; + font-weight: normal; + font-size: 0.9em; +} + +#tipOnboardingModal code { + background: rgba(0,0,0,0.1); + padding: 2px 5px; + border-radius: 3px; + font-family: monospace; +} + +.darktheme #tipOnboardingModal code { + background: rgba(255,255,255,0.1); +} + +.tipOnboardingBtn { + display: inline-block; + background: #7289da; + color: white !important; + padding: 8px 16px; + border-radius: 5px; + text-decoration: none; + margin: 6px 0; + font-weight: bold; + font-size: 0.9em; +} + +.tipOnboardingBtn:hover { + background: #5b6eae; + text-decoration: none; +} diff --git a/main.js b/main.js index 207bf88..0120be8 100644 --- a/main.js +++ b/main.js @@ -1,992 +1,1018 @@ -/* - * Copyright (c) 2026 Steve Seguin. All Rights Reserved. - * - * Use of this source code is governed by the APGLv3 open-source license - * that can be found in the LICENSE file in the root of the source - * tree. Alternative licencing options can be made available on request. - * - */ -/*jshint esversion: 6 */ -async function main() { - // main asyncronous thread; mostly initializes the user settings. - - var delayedStartupFuncs = []; - // translation stuff start //// - - var ConfigSettings = getById("main-js"); - let ln_template = null; - let altLabelOverride = null; - - if (urlParams.has("altlabel")) { - try { - altLabelOverride = urlParams.get("altlabel") || ""; - } catch (e) { - altLabelOverride = ""; - } - if (altLabelOverride) { - try { - altLabelOverride = decodeURIComponent(altLabelOverride); - } catch (e) {} - altLabelOverride = altLabelOverride.replace(/_/g, " ").trim(); - if (altLabelOverride.length === 0) { - altLabelOverride = null; - } - } else { - altLabelOverride = null; - } - } - - function applyAltLabelOverride(text) { - if (!text) { - return; - } - if (translation && translation.innerHTML) { - translation.innerHTML["enter-display-name"] = text; - } - miscTranslations["enter-display-name"] = text; - } - - try { - if (ConfigSettings) { - ln_template = ConfigSettings.getAttribute("data-translation"); // Translations - if (typeof ln_template === "undefined") { - ln_template = false; - } else if (ln_template === null) { - ln_template = false; - } - } - - if (urlParams.has("ln") || urlParams.has("language")) { - ln_template = urlParams.get("ln") || urlParams.get("language") || null; - } else if (session.language) { - ln_template = session.language; - } else { - const storedLanguage = localStorage.getItem("vdo_ninja_language"); - if (storedLanguage) { - ln_template = storedLanguage; - session.language = storedLanguage; - } - } - } catch (e) { - errorlog(e); - } - - if (ln_template === null) { - // Only show menu if not in auth mode - if (!urlParams.has("auth") && !urlParams.has("requireauth")) { - getById("mainmenu").style.opacity = 1; - } - } else if (ln_template !== false) { - // checking if manual lanuage override enabled - try { - log("Lang Template: " + ln_template); - await changeLg(ln_template); - if (altLabelOverride) { - applyAltLabelOverride(altLabelOverride); - } - // Only show menu if not in auth mode - if (!urlParams.has("auth") && !urlParams.has("requireauth")) { - //getById("mainmenu").style.opacity = 1; - } - } catch (error) { - errorlog(error); - // Only show menu if not in auth mode - if (!urlParams.has("auth") && !urlParams.has("requireauth")) { - getById("mainmenu").style.opacity = 1; - } - } - } - - // Initialize authentication if enabled - if (window.vdoAuth) { - getById("mainmenu").classList.add("hidden2"); - getById("header").classList.add("hidden2"); - - await window.vdoAuth.init(); - // Menu visibility is now handled by auth completion - if (session.authMode && (session.authToken || session.authSkipped)) { - getById("mainmenu").style.opacity = 1; - } - } - - if (location.hostname !== "vdo.ninja" && location.hostname !== "backup.vdo.ninja" && location.hostname !== "proxy.vdo.ninja" && location.hostname !== "alt.vdo.ninja" && location.hostname !== "obs.ninja") { - errorReport = false; - - if (location.hostname === "rtc.ninja") { - try { - if (session.label === false) { - document.title = "RTC.Ninja"; - } - getById("qos").innerHTML = ""; - getById("logoname").innerHTML = ""; - getById("helpbutton").style.display = "none"; - getById("helpbutton").style.opacity = 0; - getById("reportbutton").style.display = "none"; - getById("reportbutton").style.opacity = 0; - getById("dropButton").classList.add("hidden"); - getById("container-4").classList.add("hidden"); - if (!(urlParams.has("screenshare") || urlParams.has("ss"))) { - getById("container-2").classList.add("hidden"); - } - //getById("mainmenu").style.opacity = 1; - getById("mainmenu").style.margin = "30px 0"; - getById("translateButton").style.display = "none"; - getById("translateButton").style.opacity = 0; - // getById("legal").style.display = "none"; - // getById("legal").style.opacity = 0; - getById("info").style.display = "none"; - getById("info").style.opacity = 0; - getById("chatBody").innerHTML = ""; - } catch (e) {} - } else if (session.label === false) { - document.title = location.hostname; - } - try { - if (ln_template === false) { - if (location.hostname === "china.vdo.ninja") { - changeLg("cn").then(() => { - if (altLabelOverride) { - applyAltLabelOverride(altLabelOverride); - } - }); - } else { - changeLg("blank").then(() => { - if (altLabelOverride) { - applyAltLabelOverride(altLabelOverride); - } - }); - } - } - if (location.hostname === "china.vdo.ninja") { - session.wss = "wss://china.rtc.ninja:8443"; - } - //getById("mainmenu").style.opacity = 1; - - getById("qos").innerHTML = ''; - getById("logoname").innerHTML = getById("qos").outerHTML; - getById("helpbutton").style.display = "none"; - getById("reportbutton").style.display = "none"; - getById("chatBody").innerHTML = ""; - getById("qos").style.color = "#FFF7"; - //getById("qos").style.fontSize = "70%"; - getById("logoname").style.display = "none"; - getById("logoname").style.margin = "0 0 0 5px"; - } catch (error) { - getById("mainmenu").style.opacity = 1; - errorlog(error); - } - } else { - // check if automatic language translation is available - getById("mainmenu").style.opacity = 1; - - if (location.hostname === "alt.vdo.ninja"){ - session.wss = "wss://china.rtc.ninja:8443"; - } - } - - if (altLabelOverride) { - applyAltLabelOverride(altLabelOverride); - } - - //// translation stuff ends //// - - if (urlParams.has("cleanoutput") || urlParams.has("clean") || urlParams.has("cleanish")) { - session.cleanOutput = true; - } - if (urlParams.has("mutestatus") || urlParams.has("showmutestate") || urlParams.has("showmuted")){ - session.showMuteState = true; - } - if (urlParams.has("unmutestatus") || urlParams.has("showunmutestate") || urlParams.has("showunmuted")){ - session.showUnMuteState = true; - } - if (urlParams.has("cleanviewer") || urlParams.has("cv")) { - session.cleanViewer = true; - } - - if (session.cleanOutput || session.cleanViewer) { - session.audioMeterGuest = false; - } - - // Track whether we should swap the default tone for the louder knock sample - if (typeof session.knockToneEnabled === "undefined") { - session.knockToneEnabled = false; - } - - if (urlParams.has("hidehome")) { - session.hidehome = true; - } - hideHomeCheck(); - - if (window.obsstudio || isMELD) { - session.studioSoftware = true; - getById("saveRoom").style.display = "none"; // don't let the user save the room if in OBS - } - - if (urlParams.has("previewmode")) { - session.switchMode = true; - } - try { - if (sessionStorage.getItem("deleteWhipOnLoad")) { - let deleteWhip = sessionStorage.getItem("deleteWhipOnLoad"); - let deleteWhipObj = JSON.parse(deleteWhip); - - if (deleteWhipObj.location) { - try { - let targetUrl = new URL(deleteWhipObj.location); - targetUrl.protocol = window.location.protocol; - - let xhttp = new XMLHttpRequest(); - xhttp.open("DELETE", targetUrl.toString(), true); - if (deleteWhipObj.whipOutputToken) { - xhttp.setRequestHeader("Authorization", "Bearer " + deleteWhipObj.whipOutputToken); - } - xhttp.send(); - } catch(e) { - log(e); - } - } - sessionStorage.removeItem("deleteWhipOnLoad"); - } - } catch (e) { - errorlog(e); - } - - - if (urlParams.has("director") || urlParams.has("dir")) { - session.director = urlParams.get("director") || urlParams.get("dir") || session.roomid || urlParams.get("roomid") || urlParams.get("r") || urlParams.get("room") || filename || true; - session.effect = null; // so the director can see the effects after a page refresh - getById("avatarDiv3").classList.remove("hidden"); // lets the director see the avatar option - } - - if (urlParams.has("feedbackbutton") || urlParams.has("fb")) { - getById("unmuteSelf").classList.remove("hidden"); // lets the director see the avatar option - //session.selfVolume = urlParams.get("fb") || null; - session.selfVolume = urlParams.get("feedbackbutton") || urlParams.get("fb") || null; - if (session.selfVolume){ - getById("unmuteSelf").setAttribute("title", `Hear yourself at ${parseFloat(session.selfVolume)}% volume`); - getById("unmuteSelf").setAttribute("alt", `Hear yourself at ${parseFloat(session.selfVolume)}% volume`); - } - } - - if (urlParams.has("controls") || urlParams.has("videocontrols")) { - session.showControls = true; // show the video control bar - - if (urlParams.get("controls") === "false") { - session.showControls = false; - } else if (urlParams.get("controls") === "0") { - session.showControls = false; - } else if (urlParams.get("controls") === "off") { - session.showControls = false; - } - } - if (urlParams.has("forcecontrols")) { - session.showControls = 2; - function keepControls() { - var tmp = document.activeElement; - document.querySelectorAll("video").forEach(ele => { - ele.focus(); - ele.removeAttribute("controls"); - ele.setAttribute("controls", ""); - }); - tmp.focus(); - } - getById("main").classList.add("forcecontrols"); - setInterval(function () { - keepControls(); - }, 100); - } - if (urlParams.has("nocontrols")) { - session.showControls = false; // show the video control bar - } - - if (!isIFrame && !session.studioSoftware) { - if (ChromiumVersion === 65) { - // pass, since probably manycam and that's bugged - } else if (getStorage("redirect") == "yes") { - setStorage("redirect", "", 0); - session.sticky = true; - } else if (getStorage("settings") != "") { - var URLGOTO = getStorage("settings"); - if (URLGOTO && URLGOTO.startsWith("https://")) { - if (URLGOTO === window.location.href) { - // continue, as its already matched - } else if (!session.cleanOutput) { - window.focus(); - document.body.classList.remove("hidden"); - - session.sticky = await confirmAlt("Would you like to load your previous session?\n\nThis will redirect you to:\n\n" + URLGOTO, true); - if (!session.sticky) { - setStorage("settings", "", 0); - log("deleting cookie as user said no"); - } else { - var cookieSettings = decodeURI(URLGOTO); - setStorage("redirect", "yes", 1); - window.location.replace(cookieSettings); - } - } else { - var cookieSettings = decodeURI(URLGOTO); - setStorage("redirect", "yes", 1); - window.location.replace(cookieSettings); - } - } - } - - if (urlParams.has("sticky")) { - // won't work with iframes. - - //if (getStorage("permission") == "") { - // session.sticky = confirm("Would you allow us to store a cookie to keep your session settings persistent?"); - //} else { - session.sticky = true; - - getById("saveRoom").style.display = "none"; - //} - //if (session.sticky) { - setStorage("permission", "yes", 999); - setStorage("settings", encodeURI(window.location.href), 90); - //} - } - } else if (isIFrame && !window.obsstudio && urlParams.has("sticky")) { - session.sticky = true; - getById("saveRoom").style.display = "none"; - } - - if (urlParams.has("safemode")) { - session.safemode = true; // load defa - } else { - session.store = {}; - try { - loadSettings(); - } catch (e) { - errorlog(e); - } - } - - if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - try { - //getById("electronDragZone").style.cursor="grab"; - //getById("header").style.height = "max(calc(2% + 20px), 40px)"; - - // Override window.prompt to use Electron's dialog via the contextBridge - if (window.electronApi && window.electronApi.prompt) { - window.prompt = function (title, val) { - return window.electronApi.prompt({ title, val }); - }; - } else { - warnlog("electronApi prompt function not available"); - } - - const dragZone = document.getElementById("electronDragZone"); - if (dragZone) { - dragZone.style.display = "block"; - dragZone.style.setProperty("-webkit-app-region", "drag"); - } - - const header = document.getElementById("header"); - if (header) { - const interactiveSelectors = [ - "a", - "button", - "input", - "select", - "textarea", - "[role='button']", - "[onclick]" - ]; - - header.querySelectorAll(interactiveSelectors.join(",")).forEach(node => { - node.style.setProperty("-webkit-app-region", "no-drag"); - if (!node.style.pointerEvents || node.style.pointerEvents === "") { - node.style.pointerEvents = "auto"; - } - }); - } - } catch (e) { - console.error("Error setting up Electron prompt:", e); - } - } - - if (window.electronApi && window.electronApi.exposeDoSomethingInWebApp) { - window.electronApi.exposeDoSomethingInWebApp(function (fauxEventData) { - //alert("WORKS"); - //errorlog("WORKS!"); - session.remoteInterfaceAPI(fauxEventData); - }); - if (window.electronApi.updateVersion){ - window.electronApi.updateVersion(session.version); - } - } - - if (urlParams.has("retrytimeout")) { - session.retryTimeout = parseInt(urlParams.get("retrytimeout")) || 5000; - if (session.retryTimeout < 5000) { - session.retryTimeout = 5000; - } - } - - if (urlParams.has("ptz")) { - session.ptz = true; - } - - if (urlParams.has("notios")) { - iOS = false; - iPad = false; - } - - if (urlParams.has("optimize")) { - session.optimize = parseInt(urlParams.get("optimize")) || 0; - } - - if (urlParams.has("nosettings") || urlParams.has("ns")) { - session.screensharebutton = false; - session.showSettings = false; - } - - if (urlParams.has("nomicbutton") || urlParams.has("nmb")) { - getById("mutebutton").style.setProperty("display", "none", "important"); - } - - if (urlParams.has("novice")) { - // Hiding advanced items - document.querySelectorAll(".advanced").forEach(element => { - element.classList.add("hide"); - }); - } - - if (urlParams.has("userbackgroundimage") || urlParams.has("userbgimage") || urlParams.has("ubgimg")) { - // URL or data:base64 image. Becomes local to this viewer only. - let defaultMedia = urlParams.get("userbackgroundimage") || urlParams.get("userbgimage") || urlParams.get("ubgimg") || "./media/backgrounds/1.png"; - if (defaultMedia) { - try { - defaultMedia = decodeURIComponent(defaultMedia); - } catch (e) {} - session.defaultMedia = defaultMedia; - try { - let fallbackImage = new Image(); - fallbackImage.src = defaultMedia; - } catch (e) {} - } - } - if (urlParams.has("userforegroundimage") || urlParams.has("overlayimage") || urlParams.has("overlayimg")) { - // URL or data:base64 image. Becomes local to this viewer only. - let defaultMedia = urlParams.get("userforegroundimage") || urlParams.get("overlayimage") || urlParams.get("overlayimg") || "./media/avatar1.png"; - if (defaultMedia) { - try { - defaultMedia = decodeURIComponent(defaultMedia); - } catch (e) {} - session.defaultOverlayMedia = defaultMedia; - try { - let fallbackImage = new Image(); - fallbackImage.src = defaultMedia; - } catch (e) {} - } - } - - if (urlParams.has("avatarimg") || urlParams.has("bgimage") || urlParams.has("bgimg")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - let avatarImg = urlParams.get("avatarimg") || urlParams.get("bgimage") || urlParams.get("bgimg") || "./media/avatar1.png"; - if (avatarImg=="0" || avatarImg == "false" || avatarImg == "no"){ - if (session.disableBackground!==false){ - session.disableBackground = true; - } - } else if (avatarImg) { - try { - avatarImg = decodeURIComponent(avatarImg); - } catch (e) {} - try { - let fallbackImage = new Image(); - fallbackImage.src = avatarImg; - session.style = -1; - fallbackImage.onload = function () { - document.documentElement.style.setProperty("--video-background-image", 'url("' + avatarImg + '")'); - if (session.meterStyle !== 5) { - document.documentElement.style.setProperty("--video-background-image-size", "contain"); - } - session.disableBackground = false; - }; - } catch (e) {} - } else { - if (session.disableBackground!==false){ - session.disableBackground = true; - } - } - } - if (urlParams.has("avatarimg2") || urlParams.has("bgimage2") || urlParams.has("bgimg2")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - let avatarImg2 = urlParams.get("avatarimg2") || urlParams.get("bgimage2") || urlParams.get("bgimg2") || "./media/avatar2.png"; - if (avatarImg2) { - try { - avatarImg2 = decodeURIComponent(avatarImg2); - } catch (e) {} - try { - let fallbackImage2 = new Image(); - fallbackImage2.src = avatarImg2; - fallbackImage2.onload = function () { - document.documentElement.style.setProperty("--video-background-image-talking", 'url("' + avatarImg2 + '")'); - if (session.meterStyle !== 5) { - document.documentElement.style.setProperty("--video-background-image-size", "contain"); - } - }; - session.audioEffects = true; - session.meterStyle = 4; - session.style = -1; - if (session.showControls === null) { - session.showControls = false; - } - } catch (e) {} - } - } - - if (urlParams.has("avatarimg3") || urlParams.has("bgimage3") || urlParams.has("bgimg3")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - let avatarImg3 = urlParams.get("avatarimg3") || urlParams.get("bgimage3") || urlParams.get("bgimg3") || "./media/avatar3.png"; - if (avatarImg3) { - try { - avatarImg3 = decodeURIComponent(avatarImg3); - } catch (e) {} - try { - let fallbackImage3 = new Image(); - fallbackImage3.src = avatarImg3; - fallbackImage3.onload = function () { - document.documentElement.style.setProperty("--video-background-image-screaming", 'url("' + avatarImg3 + '")'); - if (session.meterStyle !== 5) { - document.documentElement.style.setProperty("--video-background-image-size", "contain"); - } - }; - session.audioEffects = true; - session.meterStyle = 4; - session.style = -1; - if (session.showControls === null) { - session.showControls = false; - } - } catch (e) {} - } - } - - if (urlParams.has("background") || urlParams.has("appbg")) { - // URL or data:base64 image. Use &chroma if you want to use a color instead of image. - let background = urlParams.get("background") || urlParams.get("appbg") || "./media/logo_cropped.png"; - if (background) { - try { - background = decodeURIComponent(background); - } catch (e) {} - try { - background = 'url("' + background + '")'; - document.documentElement.style.setProperty("--background-main-image", background); - } catch (e) {} - } - } - - if (urlParams.has("poster")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - let posterImage = urlParams.get("poster") || "./media/avatar.webp"; - if (posterImage) { - try { - posterImage = decodeURIComponent(posterImage); - session.posterImage = posterImage; - } catch (e) {} - } - } - - if (urlParams.has("hideplaybutton") || urlParams.has("hpb")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - try { - document.getElementById("bigPlayButton").classList.add("hidden"); - } catch (e) {} - } - - if (urlParams.has("whip") || urlParams.has("whipview")) { - session.whipView = urlParams.get("whip") || urlParams.get("whipview") || false; - if (session.whipView) { - setTimeout(function () { - whipClient(); - }, 1000); - } - } - - if (urlParams.has("noaudiowhipin")){ - session.forceNoAudioWhipIn = true; - } - if (urlParams.has("novideowhipin")){ - session.forceNoVideoWhipIn = true; - } - - if (urlParams.has("autoreload")){ - let refreshInterval = parseInt(urlParams.get("autoreload")) || 60; - if (refreshInterval){ - refreshInterval = refreshInterval*60*1000; // minutes to milliseconds - setInterval(function(){ - session.hangup(true) - }, refreshInterval); - } - } - - if (urlParams.has("autoreload24")) { - let reloadTime = urlParams.get("autoreload24"); - - // Parse the reload time - let [hours, minutes] = reloadTime.split(':').map(Number); - - if (!isNaN(hours) && !isNaN(minutes) && hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) { - let now = new Date(); - let reloadDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes); - - // If the reload time has already passed today, schedule for tomorrow - if (reloadDate <= now) { - reloadDate.setDate(reloadDate.getDate() + 1); - } - - let timeUntilReload = reloadDate.getTime() - now.getTime(); - - setTimeout(function() { - session.hangup(true); - }, timeUntilReload); - } else { - console.error("Invalid reload time format. Please use HH:MM in 24-hour format."); - } - } - - if (urlParams.has("cftoken") || urlParams.has("cft")) { - session.whipOutput = urlParams.get("cftoken") || urlParams.get("cft") || false; - if (session.whipOutput) { - try { - session.whipOutput = decodeURIComponent(session.whipOutput); - } catch (e) {} - if (!session.whipOutputToken) { - session.whipOutputToken = session.whipOutput; - } - session.whipOutput = "https://cloudflare.vdo.ninja/" + session.whipOutput; - session.whipOutputUserSet = true; - } - } - - if (urlParams.has("endpage")) { - session.redirectHangup = urlParams.get("endpage") || false; - session.redirectHangupTimer = 3000; - - if (session.redirectHangup) { - try { - session.redirectHangup = decodeURIComponent(session.redirectHangup); - getById("hangupContainer").innerHTML = "Hang-up complete. Redirecting shortly..."; - } catch (e) {} - } - - if (urlParams.has("endpagetimer")) { - session.redirectHangupTimer = urlParams.get("endpagetimer") || 0; - session.redirectHangupTimer = parseInt(session.redirectHangupTimer) || 0; - } - } - - if (urlParams.has("autoend")) { - session.autoEnd = urlParams.get("autoend"); - if (session.autoEnd) { - session.autoEnd = parseInt(session.autoEnd) || 600000; // default 10 minutes if value provided - } else { - session.autoEnd = 600000; // default 10 minutes if no value - } - } - - - if (urlParams.has("whepwait") || urlParams.has("whepicewait")) { - // I'm going to use this for all whip/whep for the time being. - session.whepWait = urlParams.get("whepwait") || urlParams.get("whepicewait") || 2000; // how long we wait for ice candidates to collect; ms. whep out and whep in - session.whepWait = parseInt(session.whepWait); - if (session.whepWait < 0) { - session.whepWait = 0; - } - } - - if (urlParams.has("whipwait") || urlParams.has("whipicewait")) { - // I'm going to use this for all whip/whep for the time being. - session.whipWait = urlParams.get("whipwait") || urlParams.get("whipicewait") || 2000; // how long we wait for ice candidates to collect; ms. whep out and whep in - session.whipWait = parseInt(session.whipWait); - if (session.whipWait < 0) { - session.whipWait = 0; - } - } else { - session.whipWait = 2000; // REMOVE after october 2024. - } - - if (urlParams.has("whipme")){ // WIP - getById("container-18").classList.remove("hidden"); - } - - if (urlParams.has("whippush") || urlParams.has("whipout") || urlParams.has("pushwhip")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - session.whipOutput = urlParams.get("whippush") || urlParams.get("whipout") || urlParams.get("pushwhip") || null; - if (session.whipOutput) { - try { - if (session.whipOutput == "twitch") { - session.whipOutput = "https://g.webrtc.live-video.net:4443/v2/offer"; - query("#publishOutToken input[type='password']").placeholder = "Twitch stream token here"; - } else { - session.whipOutput = decodeURIComponent(session.whipOutput); - if (!session.whipOutput.startsWith("http://") && !session.whipOutput.startsWith("https://")) { - session.whipOutput = "https://" + session.whipOutput; - } - } - } catch (e) { - errorlog(e); - } - } else { - getById("publishOutURL").classList.remove("hidden"); - } - - if (session.whipOutput) { - session.whipOutputUserSet = true; - } - - if (urlParams.has("whippushtoken") || urlParams.has("whipouttoken") || urlParams.has("pushwhiptoken")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - session.whipOutputToken = urlParams.get("whippushtoken") || urlParams.get("whipouttoken") || urlParams.get("pushwhiptoken") || false; - if (!session.whipOutputToken) { - getById("publishOutToken").classList.remove("hidden"); - } - } else if (session.whipOutput !== false) { - if (!session.whipOutputToken) { - getById("publishOutToken").classList.remove("hidden"); - } - } - - getById("startPublishingButton").classList.remove("hidden"); - } - - if (urlParams.has("whipoutkeyframe") ){ - session.whipOutKeyframe = parseInt(urlParams.get("whipoutkeyframe")) || 0; - } - - if (urlParams.has("whipoutkeyframenewviewer") ){ - session.whipOutKeyframeOnNewViewer = true; - } - - - if (urlParams.has("svc") || urlParams.has("scalabilitymode")) { - // Experiment with this feature here: https://webrtc.github.io/samples/src/content/extensions/svc/ - session.scalabilityMode = urlParams.get("svc") || urlParams.get("scalabilitymode") || "L1T3"; - if (!scalabilityModes.includes(session.scalabilityMode)) { - scalabilityModes.forEach(sca => { - if (sca.toLowerCase() === session.scalabilityMode.toLowerCase()) { - session.scalabilityMode = sca; - log("Corrected the capitalization of the SVC value. just in case thats important"); - } - }); - } - } - - if (urlParams.has("whepplay") || urlParams.has("whep")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - if (urlParams.get("whepplay") || urlParams.has("whep")) { - try { - session.whepInput = decodeURIComponent(urlParams.get("whepplay") || urlParams.get("whep")); - if (session.whepInput) { - setTimeout(function () { - whepIn(); - }, 1000); - } - } catch (e) { - errorlog(e); - } - } - } - if (urlParams.has("whepplaytoken") || urlParams.has("wheptoken")) { - // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case - if (urlParams.get("whepplaytoken") || urlParams.get("wheptoken")) { - try { - session.whepInputToken = urlParams.get("whepplaytoken") || urlParams.get("wheptoken"); - } catch (e) { - errorlog(e); - } - } - } - - if (urlParams.has("nomouseevents") || urlParams.has("nme")) { - session.disableMouseEvents = true; - } - if (urlParams.has("overlaycontrols")) { - session.overlayControls = true; - } - - if (urlParams.has("novideobutton") || urlParams.has("nvb")) { - getById("mutevideobutton").style.setProperty("display", "none", "important"); - } - - if (urlParams.has("nospeakerbutton") || urlParams.has("nsb")) { - getById("mutespeakerbutton").style.setProperty("display", "none", "important"); - } - - if (urlParams.has("noscale") || urlParams.has("noscaling")) { - session.noScaling = true; - } - - if (urlParams.has("pusheffectsdata")) { - session.pushEffectsData = true; - } - - if (urlParams.has("pushloudness") || urlParams.has("getloudness")) { - // this sets the loudness IFRAME API output, if available. - session.pushLoudness = true; - } - - if (urlParams.has("pushfaces") || urlParams.has("getfaces")) { - session.grabFaceData = true; - setTimeout(function () { - // give the app some time to load - getFaces(); - }, 2000); - } - - if (urlParams.has("notmobile")) { - session.mobile = false; - } else if (urlParams.has("mobile")) { - session.mobile = true; - session.audioEffects = false; // disable audio inbound effects also. - session.audioMeterGuest = false; - } else if (iOS || iPad) { - if (SafariVersion && SafariVersion < 17) { - getById("oldiOSWarning").classList.remove("hidden"); // update this to 17 at some point. - } - session.mobile = true; - session.audioEffects = false; // disable audio inbound effects also. - session.audioMeterGuest = false; - window.addEventListener("resize", function () { - // Safari is the new IE. - - if (session.ws) { - var msg = {}; - msg.requestSceneUpdate = true; - session.sendMessage(msg); - } - if (screen && screen.orientation && screen.orientation.type) { - if (screen.orientation.type.includes("portrait")) { - document.getElementsByTagName("html")[0].style.height = "100vh"; - setTimeout(function () { - document.getElementsByTagName("html")[0].style.height = "100%"; - }, 1000); - } else if (screen.orientation.type.includes("landscape")) { - document.getElementsByTagName("html")[0].style.height = "100vh"; - setTimeout(function () { - document.getElementsByTagName("html")[0].style.height = "100%"; - }, 1000); - } - } else if (window.matchMedia("(orientation: portrait)").matches) { - document.getElementsByTagName("html")[0].style.height = "100vh"; - setTimeout(function () { - document.getElementsByTagName("html")[0].style.height = "100%"; - }, 1000); - } else if (window.matchMedia("(orientation: landscape)").matches) { - document.getElementsByTagName("html")[0].style.height = "100vh"; - setTimeout(function () { - document.getElementsByTagName("html")[0].style.height = "100%"; - }, 1000); - } - }); - - if (/CriOS/i.test(navigator.userAgent)) { - // if runngin Chrome on iOS - if (!session.cleanOutput) { - try { - navigator.mediaDevices.getUserMedia; - } catch (e) { - warnUser("Chrome on this device does not support the required technology to use this site.\n\nPlease use Safari instead or update your iOS and browser version."); - } - } - } - } else if (/Android|Pixel|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { - // not sure how accurate this is. - session.mobile = true; - session.audioEffects = false; // disable audio inbound effects also. - session.audioMeterGuest = false; - } else { - log("MAKE DRAGGABLE"); - delayedStartupFuncs.push([makeDraggableElement, getById("subControlButtons")]); - if (SafariVersion && !ChromiumVersion) { - // if desktop Safari, so macOS, give a note saying it sucks - getById("SafariWarning").classList.remove("hidden"); - } - } - - if (urlParams.has("broadcasttransfer") || urlParams.has("bct")) { - log("Broadcast transfer flag set"); - session.broadcastTransfer = urlParams.get("broadcasttransfer") || urlParams.get("bct") || null; - if (session.broadcastTransfer === "false") { - session.broadcastTransfer = false; - } else if (session.broadcastTransfer === "0") { - session.broadcastTransfer = false; - } else if (session.broadcastTransfer === "no") { - session.broadcastTransfer = false; - } else if (session.broadcastTransfer === "off") { - session.broadcastTransfer = false; - } else { - session.broadcastTransfer = true; - } - if (transferSettings) { - transferSettings.broadcast = session.broadcastTransfer; - } - } - - if (urlParams.has("queuetransfer") || urlParams.has("qt")) { - log("Broadcast transfer flag set"); - session.queueTransfer = urlParams.get("queuetransfer") || urlParams.get("qt") || null; - if (session.queueTransfer === "false") { - session.queueTransfer = false; - } else if (session.queueTransfer === "0") { - session.queueTransfer = false; - } else if (session.queueTransfer === "no") { - session.queueTransfer = false; - } else if (session.queueTransfer === "off") { - session.queueTransfer = false; - } else { - session.queueTransfer = true; - } - if (transferSettings) { - transferSettings.queue = session.queueTransfer; - } - } - - if (urlParams.has("broadcast") || urlParams.has("bc")) { - log("Broadcast flag set"); - session.broadcast = urlParams.get("broadcast") || urlParams.get("bc") || null; - - if (session.broadcast === "false") { - session.broadcast = false; - } else if (session.broadcast === "0") { - session.broadcast = false; - } else if (session.broadcast === "no") { - session.broadcast = false; - } else if (session.broadcast === "off") { - session.broadcast = false; - } - - //if ((iOS) || (iPad)) { - // session.nopreview = false; - //} else { - // session.nopreview = true; - //} - session.minipreview = 2; // full screen if nothing else on screen. - session.style = 1; - //getById("header").style.display = "none"; - //getById("header").style.opacity = 0; - session.showList = true; - } - - if (urlParams.has("showlist")) { - session.showList = urlParams.get("showlist"); - if (session.showList === "false") { - session.showList = false; - } else if (session.showList === "0") { - session.showList = false; - } else if (session.showList === "no") { - session.showList = false; - } else if (session.showList === "off") { - session.showList = false; - } else { - session.showList = true; - } - } - //if (session.showList===true){ - // getById("hideusers").classList.add("hidden"); - //} - +/* + * Copyright (c) 2026 Steve Seguin. All Rights Reserved. + * + * Use of this source code is governed by the APGLv3 open-source license + * that can be found in the LICENSE file in the root of the source + * tree. Alternative licencing options can be made available on request. + * + */ +/*jshint esversion: 6 */ +async function main() { + // main asyncronous thread; mostly initializes the user settings. + + var delayedStartupFuncs = []; + // translation stuff start //// + + var ConfigSettings = getById("main-js"); + let ln_template = null; + let altLabelOverride = null; + + if (urlParams.has("altlabel")) { + try { + altLabelOverride = urlParams.get("altlabel") || ""; + } catch (e) { + altLabelOverride = ""; + } + if (altLabelOverride) { + try { + altLabelOverride = decodeURIComponent(altLabelOverride); + } catch (e) {} + altLabelOverride = altLabelOverride.replace(/_/g, " ").trim(); + if (altLabelOverride.length === 0) { + altLabelOverride = null; + } + } else { + altLabelOverride = null; + } + } + + function applyAltLabelOverride(text) { + if (!text) { + return; + } + if (translation && translation.innerHTML) { + translation.innerHTML["enter-display-name"] = text; + } + miscTranslations["enter-display-name"] = text; + } + + try { + if (ConfigSettings) { + ln_template = ConfigSettings.getAttribute("data-translation"); // Translations + if (typeof ln_template === "undefined") { + ln_template = false; + } else if (ln_template === null) { + ln_template = false; + } + } + + if (urlParams.has("ln") || urlParams.has("language")) { + ln_template = urlParams.get("ln") || urlParams.get("language") || null; + } else if (session.language) { + ln_template = session.language; + } else { + const storedLanguage = localStorage.getItem("vdo_ninja_language"); + if (storedLanguage) { + ln_template = storedLanguage; + session.language = storedLanguage; + } + } + } catch (e) { + errorlog(e); + } + + if (ln_template === null) { + // Only show menu if not in auth mode + if (!urlParams.has("auth") && !urlParams.has("requireauth")) { + getById("mainmenu").style.opacity = 1; + } + } else if (ln_template !== false) { + // checking if manual lanuage override enabled + try { + log("Lang Template: " + ln_template); + await changeLg(ln_template); + if (altLabelOverride) { + applyAltLabelOverride(altLabelOverride); + } + // Only show menu if not in auth mode + if (!urlParams.has("auth") && !urlParams.has("requireauth")) { + //getById("mainmenu").style.opacity = 1; + } + } catch (error) { + errorlog(error); + // Only show menu if not in auth mode + if (!urlParams.has("auth") && !urlParams.has("requireauth")) { + getById("mainmenu").style.opacity = 1; + } + } + } + + // Initialize authentication if enabled + if (window.vdoAuth) { + getById("mainmenu").classList.add("hidden2"); + getById("header").classList.add("hidden2"); + + await window.vdoAuth.init(); + // Menu visibility is now handled by auth completion + if (session.authMode && (session.authToken || session.authSkipped)) { + getById("mainmenu").style.opacity = 1; + } + } + + if (location.hostname !== "vdo.ninja" && location.hostname !== "backup.vdo.ninja" && location.hostname !== "proxy.vdo.ninja" && location.hostname !== "alt.vdo.ninja" && location.hostname !== "obs.ninja") { + errorReport = false; + + if (location.hostname === "rtc.ninja") { + try { + if (session.label === false) { + document.title = "RTC.Ninja"; + } + getById("qos").innerHTML = ""; + getById("logoname").innerHTML = ""; + getById("helpbutton").style.display = "none"; + getById("helpbutton").style.opacity = 0; + getById("reportbutton").style.display = "none"; + getById("reportbutton").style.opacity = 0; + getById("dropButton").classList.add("hidden"); + getById("container-4").classList.add("hidden"); + if (!(urlParams.has("screenshare") || urlParams.has("ss"))) { + getById("container-2").classList.add("hidden"); + } + //getById("mainmenu").style.opacity = 1; + getById("mainmenu").style.margin = "30px 0"; + getById("translateButton").style.display = "none"; + getById("translateButton").style.opacity = 0; + // getById("legal").style.display = "none"; + // getById("legal").style.opacity = 0; + getById("info").style.display = "none"; + getById("info").style.opacity = 0; + getById("chatBody").innerHTML = ""; + } catch (e) {} + } else if (session.label === false) { + document.title = location.hostname; + } + try { + if (ln_template === false) { + if (location.hostname === "china.vdo.ninja") { + changeLg("cn").then(() => { + if (altLabelOverride) { + applyAltLabelOverride(altLabelOverride); + } + }); + } else { + changeLg("blank").then(() => { + if (altLabelOverride) { + applyAltLabelOverride(altLabelOverride); + } + }); + } + } + if (location.hostname === "china.vdo.ninja") { + session.wss = "wss://china.rtc.ninja:8443"; + } + //getById("mainmenu").style.opacity = 1; + + getById("qos").innerHTML = ''; + getById("logoname").innerHTML = getById("qos").outerHTML; + getById("helpbutton").style.display = "none"; + getById("reportbutton").style.display = "none"; + getById("chatBody").innerHTML = ""; + getById("qos").style.color = "#FFF7"; + //getById("qos").style.fontSize = "70%"; + getById("logoname").style.display = "none"; + getById("logoname").style.margin = "0 0 0 5px"; + } catch (error) { + getById("mainmenu").style.opacity = 1; + errorlog(error); + } + } else { + // check if automatic language translation is available + getById("mainmenu").style.opacity = 1; + + if (location.hostname === "alt.vdo.ninja"){ + session.wss = "wss://china.rtc.ninja:8443"; + } + } + + if (altLabelOverride) { + applyAltLabelOverride(altLabelOverride); + } + + //// translation stuff ends //// + + if (urlParams.has("cleanoutput") || urlParams.has("clean") || urlParams.has("cleanish")) { + session.cleanOutput = true; + } + if (urlParams.has("mutestatus") || urlParams.has("showmutestate") || urlParams.has("showmuted")){ + session.showMuteState = true; + } + if (urlParams.has("unmutestatus") || urlParams.has("showunmutestate") || urlParams.has("showunmuted")){ + session.showUnMuteState = true; + } + if (urlParams.has("cleanviewer") || urlParams.has("cv")) { + session.cleanViewer = true; + } + + if (session.cleanOutput || session.cleanViewer) { + session.audioMeterGuest = false; + } + + // Track whether we should swap the default tone for the louder knock sample + if (typeof session.knockToneEnabled === "undefined") { + session.knockToneEnabled = false; + } + + if (urlParams.has("hidehome")) { + session.hidehome = true; + } + hideHomeCheck(); + + if (window.obsstudio || isMELD) { + session.studioSoftware = true; + getById("saveRoom").style.display = "none"; // don't let the user save the room if in OBS + } + + if (urlParams.has("previewmode")) { + session.switchMode = true; + } + try { + if (sessionStorage.getItem("deleteWhipOnLoad")) { + let deleteWhip = sessionStorage.getItem("deleteWhipOnLoad"); + let deleteWhipObj = JSON.parse(deleteWhip); + + if (deleteWhipObj.location) { + try { + let targetUrl = new URL(deleteWhipObj.location); + targetUrl.protocol = window.location.protocol; + + let xhttp = new XMLHttpRequest(); + xhttp.open("DELETE", targetUrl.toString(), true); + if (deleteWhipObj.whipOutputToken) { + xhttp.setRequestHeader("Authorization", "Bearer " + deleteWhipObj.whipOutputToken); + } + xhttp.send(); + } catch(e) { + log(e); + } + } + sessionStorage.removeItem("deleteWhipOnLoad"); + } + } catch (e) { + errorlog(e); + } + + + if (urlParams.has("director") || urlParams.has("dir")) { + session.director = urlParams.get("director") || urlParams.get("dir") || session.roomid || urlParams.get("roomid") || urlParams.get("r") || urlParams.get("room") || filename || true; + session.effect = null; // so the director can see the effects after a page refresh + getById("avatarDiv3").classList.remove("hidden"); // lets the director see the avatar option + } + + if (urlParams.has("feedbackbutton") || urlParams.has("fb")) { + getById("unmuteSelf").classList.remove("hidden"); // lets the director see the avatar option + //session.selfVolume = urlParams.get("fb") || null; + session.selfVolume = urlParams.get("feedbackbutton") || urlParams.get("fb") || null; + if (session.selfVolume){ + getById("unmuteSelf").setAttribute("title", `Hear yourself at ${parseFloat(session.selfVolume)}% volume`); + getById("unmuteSelf").setAttribute("alt", `Hear yourself at ${parseFloat(session.selfVolume)}% volume`); + } + } + + if (urlParams.has("controls") || urlParams.has("videocontrols")) { + session.showControls = true; // show the video control bar + + if (urlParams.get("controls") === "false") { + session.showControls = false; + } else if (urlParams.get("controls") === "0") { + session.showControls = false; + } else if (urlParams.get("controls") === "off") { + session.showControls = false; + } + } + if (urlParams.has("forcecontrols")) { + session.showControls = 2; + function keepControls() { + var tmp = document.activeElement; + document.querySelectorAll("video").forEach(ele => { + ele.focus(); + ele.removeAttribute("controls"); + ele.setAttribute("controls", ""); + }); + tmp.focus(); + } + getById("main").classList.add("forcecontrols"); + setInterval(function () { + keepControls(); + }, 100); + } + if (urlParams.has("nocontrols")) { + session.showControls = false; // show the video control bar + } + + if (!isIFrame && !session.studioSoftware) { + if (ChromiumVersion === 65) { + // pass, since probably manycam and that's bugged + } else if (getStorage("redirect") == "yes") { + setStorage("redirect", "", 0); + session.sticky = true; + } else if (getStorage("settings") != "") { + var URLGOTO = getStorage("settings"); + if (URLGOTO && URLGOTO.startsWith("https://")) { + if (URLGOTO === window.location.href) { + // continue, as its already matched + } else if (!session.cleanOutput) { + window.focus(); + document.body.classList.remove("hidden"); + + session.sticky = await confirmAlt("Would you like to load your previous session?\n\nThis will redirect you to:\n\n" + URLGOTO, true); + if (!session.sticky) { + setStorage("settings", "", 0); + log("deleting cookie as user said no"); + } else { + var cookieSettings = decodeURI(URLGOTO); + setStorage("redirect", "yes", 1); + window.location.replace(cookieSettings); + } + } else { + var cookieSettings = decodeURI(URLGOTO); + setStorage("redirect", "yes", 1); + window.location.replace(cookieSettings); + } + } + } + + if (urlParams.has("sticky")) { + // won't work with iframes. + + //if (getStorage("permission") == "") { + // session.sticky = confirm("Would you allow us to store a cookie to keep your session settings persistent?"); + //} else { + session.sticky = true; + + getById("saveRoom").style.display = "none"; + //} + //if (session.sticky) { + setStorage("permission", "yes", 999); + setStorage("settings", encodeURI(window.location.href), 90); + //} + } + } else if (isIFrame && !window.obsstudio && urlParams.has("sticky")) { + session.sticky = true; + getById("saveRoom").style.display = "none"; + } + + if (urlParams.has("safemode")) { + session.safemode = true; // load defa + } else { + session.store = {}; + try { + loadSettings(); + } catch (e) { + errorlog(e); + } + } + + if (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + try { + //getById("electronDragZone").style.cursor="grab"; + //getById("header").style.height = "max(calc(2% + 20px), 40px)"; + + // Override window.prompt to use Electron's dialog via the contextBridge + if (window.electronApi && window.electronApi.prompt) { + window.prompt = function (title, val) { + return window.electronApi.prompt({ title, val }); + }; + } else { + warnlog("electronApi prompt function not available"); + } + + const dragZone = document.getElementById("electronDragZone"); + if (dragZone) { + dragZone.style.display = "block"; + dragZone.style.setProperty("-webkit-app-region", "drag"); + } + + const header = document.getElementById("header"); + if (header) { + const interactiveSelectors = [ + "a", + "button", + "input", + "select", + "textarea", + "[role='button']", + "[onclick]" + ]; + + header.querySelectorAll(interactiveSelectors.join(",")).forEach(node => { + node.style.setProperty("-webkit-app-region", "no-drag"); + if (!node.style.pointerEvents || node.style.pointerEvents === "") { + node.style.pointerEvents = "auto"; + } + }); + } + } catch (e) { + console.error("Error setting up Electron prompt:", e); + } + } + + if (window.electronApi && window.electronApi.exposeDoSomethingInWebApp) { + window.electronApi.exposeDoSomethingInWebApp(function (fauxEventData) { + //alert("WORKS"); + //errorlog("WORKS!"); + session.remoteInterfaceAPI(fauxEventData); + }); + if (window.electronApi.updateVersion){ + window.electronApi.updateVersion(session.version); + } + } + + // Check for ASIO support and log available devices (Windows only via Electron Capture) + // Supports both sync (--node flag) and async (sandbox mode) + try { + if (window.electronApi) { + // Try async first (works in sandbox mode) + if (typeof window.electronApi.isAsioAvailableAsync === "function") { + window.electronApi.isAsioAvailableAsync().then(function(available) { + if (!available) return; + window.electronApi.getAsioDevicesAsync().then(function(asioDevices) { + if (asioDevices && asioDevices.length > 0) { + console.log("ASIO devices available:", asioDevices.map(function(d) { return d.name; })); + } + }).catch(function(e) { console.warn("ASIO devices check failed:", e); }); + }).catch(function(e) { console.warn("ASIO availability check failed:", e); }); + } + // Fallback to sync (works with --node flag) + else if (typeof window.electronApi.isAsioAvailable === "function" && window.electronApi.isAsioAvailable()) { + var asioDevices = window.electronApi.getAsioDevices(); + if (asioDevices && asioDevices.length > 0) { + console.log("ASIO devices available:", asioDevices.map(function(d) { return d.name; })); + } + } + } + } catch (e) { + console.warn("ASIO detection check failed:", e); + } + if (urlParams.has("retrytimeout")) { + session.retryTimeout = parseInt(urlParams.get("retrytimeout")) || 5000; + if (session.retryTimeout < 5000) { + session.retryTimeout = 5000; + } + } + + if (urlParams.has("ptz")) { + session.ptz = true; + } + + if (urlParams.has("notios")) { + iOS = false; + iPad = false; + } + + if (urlParams.has("optimize")) { + session.optimize = parseInt(urlParams.get("optimize")) || 0; + } + + if (urlParams.has("nosettings") || urlParams.has("ns")) { + session.screensharebutton = false; + session.showSettings = false; + } + + if (urlParams.has("nomicbutton") || urlParams.has("nmb")) { + getById("mutebutton").style.setProperty("display", "none", "important"); + } + + if (urlParams.has("novice")) { + // Hiding advanced items + document.querySelectorAll(".advanced").forEach(element => { + element.classList.add("hide"); + }); + } + + if (urlParams.has("userbackgroundimage") || urlParams.has("userbgimage") || urlParams.has("ubgimg")) { + // URL or data:base64 image. Becomes local to this viewer only. + let defaultMedia = urlParams.get("userbackgroundimage") || urlParams.get("userbgimage") || urlParams.get("ubgimg") || "./media/backgrounds/1.png"; + if (defaultMedia) { + try { + defaultMedia = decodeURIComponent(defaultMedia); + } catch (e) {} + session.defaultMedia = defaultMedia; + try { + let fallbackImage = new Image(); + fallbackImage.src = defaultMedia; + } catch (e) {} + } + } + if (urlParams.has("userforegroundimage") || urlParams.has("overlayimage") || urlParams.has("overlayimg")) { + // URL or data:base64 image. Becomes local to this viewer only. + let defaultMedia = urlParams.get("userforegroundimage") || urlParams.get("overlayimage") || urlParams.get("overlayimg") || "./media/avatar1.png"; + if (defaultMedia) { + try { + defaultMedia = decodeURIComponent(defaultMedia); + } catch (e) {} + session.defaultOverlayMedia = defaultMedia; + try { + let fallbackImage = new Image(); + fallbackImage.src = defaultMedia; + } catch (e) {} + } + } + + if (urlParams.has("avatarimg") || urlParams.has("bgimage") || urlParams.has("bgimg")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + let avatarImg = urlParams.get("avatarimg") || urlParams.get("bgimage") || urlParams.get("bgimg") || "./media/avatar1.png"; + if (avatarImg=="0" || avatarImg == "false" || avatarImg == "no"){ + if (session.disableBackground!==false){ + session.disableBackground = true; + } + } else if (avatarImg) { + try { + avatarImg = decodeURIComponent(avatarImg); + } catch (e) {} + try { + let fallbackImage = new Image(); + fallbackImage.src = avatarImg; + session.style = -1; + fallbackImage.onload = function () { + document.documentElement.style.setProperty("--video-background-image", 'url("' + avatarImg + '")'); + if (session.meterStyle !== 5) { + document.documentElement.style.setProperty("--video-background-image-size", "contain"); + } + session.disableBackground = false; + }; + } catch (e) {} + } else { + if (session.disableBackground!==false){ + session.disableBackground = true; + } + } + } + if (urlParams.has("avatarimg2") || urlParams.has("bgimage2") || urlParams.has("bgimg2")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + let avatarImg2 = urlParams.get("avatarimg2") || urlParams.get("bgimage2") || urlParams.get("bgimg2") || "./media/avatar2.png"; + if (avatarImg2) { + try { + avatarImg2 = decodeURIComponent(avatarImg2); + } catch (e) {} + try { + let fallbackImage2 = new Image(); + fallbackImage2.src = avatarImg2; + fallbackImage2.onload = function () { + document.documentElement.style.setProperty("--video-background-image-talking", 'url("' + avatarImg2 + '")'); + if (session.meterStyle !== 5) { + document.documentElement.style.setProperty("--video-background-image-size", "contain"); + } + }; + session.audioEffects = true; + session.meterStyle = 4; + session.style = -1; + if (session.showControls === null) { + session.showControls = false; + } + } catch (e) {} + } + } + + if (urlParams.has("avatarimg3") || urlParams.has("bgimage3") || urlParams.has("bgimg3")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + let avatarImg3 = urlParams.get("avatarimg3") || urlParams.get("bgimage3") || urlParams.get("bgimg3") || "./media/avatar3.png"; + if (avatarImg3) { + try { + avatarImg3 = decodeURIComponent(avatarImg3); + } catch (e) {} + try { + let fallbackImage3 = new Image(); + fallbackImage3.src = avatarImg3; + fallbackImage3.onload = function () { + document.documentElement.style.setProperty("--video-background-image-screaming", 'url("' + avatarImg3 + '")'); + if (session.meterStyle !== 5) { + document.documentElement.style.setProperty("--video-background-image-size", "contain"); + } + }; + session.audioEffects = true; + session.meterStyle = 4; + session.style = -1; + if (session.showControls === null) { + session.showControls = false; + } + } catch (e) {} + } + } + + if (urlParams.has("background") || urlParams.has("appbg")) { + // URL or data:base64 image. Use &chroma if you want to use a color instead of image. + let background = urlParams.get("background") || urlParams.get("appbg") || "./media/logo_cropped.png"; + if (background) { + try { + background = decodeURIComponent(background); + } catch (e) {} + try { + background = 'url("' + background + '")'; + document.documentElement.style.setProperty("--background-main-image", background); + } catch (e) {} + } + } + + if (urlParams.has("poster")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + let posterImage = urlParams.get("poster") || "./media/avatar.webp"; + if (posterImage) { + try { + posterImage = decodeURIComponent(posterImage); + session.posterImage = posterImage; + } catch (e) {} + } + } + + if (urlParams.has("hideplaybutton") || urlParams.has("hpb")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + try { + document.getElementById("bigPlayButton").classList.add("hidden"); + } catch (e) {} + } + + if (urlParams.has("whip") || urlParams.has("whipview")) { + session.whipView = urlParams.get("whip") || urlParams.get("whipview") || false; + if (session.whipView) { + setTimeout(function () { + whipClient(); + }, 1000); + } + } + + if (urlParams.has("noaudiowhipin")){ + session.forceNoAudioWhipIn = true; + } + if (urlParams.has("novideowhipin")){ + session.forceNoVideoWhipIn = true; + } + + if (urlParams.has("autoreload")){ + let refreshInterval = parseInt(urlParams.get("autoreload")) || 60; + if (refreshInterval){ + refreshInterval = refreshInterval*60*1000; // minutes to milliseconds + setInterval(function(){ + session.hangup(true) + }, refreshInterval); + } + } + + if (urlParams.has("autoreload24")) { + let reloadTime = urlParams.get("autoreload24"); + + // Parse the reload time + let [hours, minutes] = reloadTime.split(':').map(Number); + + if (!isNaN(hours) && !isNaN(minutes) && hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) { + let now = new Date(); + let reloadDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes); + + // If the reload time has already passed today, schedule for tomorrow + if (reloadDate <= now) { + reloadDate.setDate(reloadDate.getDate() + 1); + } + + let timeUntilReload = reloadDate.getTime() - now.getTime(); + + setTimeout(function() { + session.hangup(true); + }, timeUntilReload); + } else { + console.error("Invalid reload time format. Please use HH:MM in 24-hour format."); + } + } + + if (urlParams.has("cftoken") || urlParams.has("cft")) { + session.whipOutput = urlParams.get("cftoken") || urlParams.get("cft") || false; + if (session.whipOutput) { + try { + session.whipOutput = decodeURIComponent(session.whipOutput); + } catch (e) {} + if (!session.whipOutputToken) { + session.whipOutputToken = session.whipOutput; + } + session.whipOutput = "https://cloudflare.vdo.ninja/" + session.whipOutput; + session.whipOutputUserSet = true; + } + } + + if (urlParams.has("endpage")) { + session.redirectHangup = urlParams.get("endpage") || false; + session.redirectHangupTimer = 3000; + + if (session.redirectHangup) { + try { + session.redirectHangup = decodeURIComponent(session.redirectHangup); + getById("hangupContainer").innerHTML = "Hang-up complete. Redirecting shortly..."; + } catch (e) {} + } + + if (urlParams.has("endpagetimer")) { + session.redirectHangupTimer = urlParams.get("endpagetimer") || 0; + session.redirectHangupTimer = parseInt(session.redirectHangupTimer) || 0; + } + } + + if (urlParams.has("autoend")) { + session.autoEnd = urlParams.get("autoend"); + if (session.autoEnd) { + session.autoEnd = parseInt(session.autoEnd) || 600000; // default 10 minutes if value provided + } else { + session.autoEnd = 600000; // default 10 minutes if no value + } + } + + + if (urlParams.has("whepwait") || urlParams.has("whepicewait")) { + // I'm going to use this for all whip/whep for the time being. + session.whepWait = urlParams.get("whepwait") || urlParams.get("whepicewait") || 2000; // how long we wait for ice candidates to collect; ms. whep out and whep in + session.whepWait = parseInt(session.whepWait); + if (session.whepWait < 0) { + session.whepWait = 0; + } + } + + if (urlParams.has("whipwait") || urlParams.has("whipicewait")) { + // I'm going to use this for all whip/whep for the time being. + session.whipWait = urlParams.get("whipwait") || urlParams.get("whipicewait") || 2000; // how long we wait for ice candidates to collect; ms. whep out and whep in + session.whipWait = parseInt(session.whipWait); + if (session.whipWait < 0) { + session.whipWait = 0; + } + } else { + session.whipWait = 2000; // REMOVE after october 2024. + } + + if (urlParams.has("whipme")){ // WIP + getById("container-18").classList.remove("hidden"); + } + + if (urlParams.has("whippush") || urlParams.has("whipout") || urlParams.has("pushwhip")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + session.whipOutput = urlParams.get("whippush") || urlParams.get("whipout") || urlParams.get("pushwhip") || null; + if (session.whipOutput) { + try { + if (session.whipOutput == "twitch") { + session.whipOutput = "https://g.webrtc.live-video.net:4443/v2/offer"; + query("#publishOutToken input[type='password']").placeholder = "Twitch stream token here"; + } else { + session.whipOutput = decodeURIComponent(session.whipOutput); + if (!session.whipOutput.startsWith("http://") && !session.whipOutput.startsWith("https://")) { + session.whipOutput = "https://" + session.whipOutput; + } + } + } catch (e) { + errorlog(e); + } + } else { + getById("publishOutURL").classList.remove("hidden"); + } + + if (session.whipOutput) { + session.whipOutputUserSet = true; + } + + if (urlParams.has("whippushtoken") || urlParams.has("whipouttoken") || urlParams.has("pushwhiptoken")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + session.whipOutputToken = urlParams.get("whippushtoken") || urlParams.get("whipouttoken") || urlParams.get("pushwhiptoken") || false; + if (!session.whipOutputToken) { + getById("publishOutToken").classList.remove("hidden"); + } + } else if (session.whipOutput !== false) { + if (!session.whipOutputToken) { + getById("publishOutToken").classList.remove("hidden"); + } + } + + getById("startPublishingButton").classList.remove("hidden"); + } + + if (urlParams.has("whipoutkeyframe") ){ + session.whipOutKeyframe = parseInt(urlParams.get("whipoutkeyframe")) || 0; + } + + if (urlParams.has("whipoutkeyframenewviewer") ){ + session.whipOutKeyframeOnNewViewer = true; + } + + + if (urlParams.has("svc") || urlParams.has("scalabilitymode")) { + // Experiment with this feature here: https://webrtc.github.io/samples/src/content/extensions/svc/ + session.scalabilityMode = urlParams.get("svc") || urlParams.get("scalabilitymode") || "L1T3"; + if (!scalabilityModes.includes(session.scalabilityMode)) { + scalabilityModes.forEach(sca => { + if (sca.toLowerCase() === session.scalabilityMode.toLowerCase()) { + session.scalabilityMode = sca; + log("Corrected the capitalization of the SVC value. just in case thats important"); + } + }); + } + } + + if (urlParams.has("whepplay") || urlParams.has("whep")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + if (urlParams.get("whepplay") || urlParams.has("whep")) { + try { + session.whepInput = decodeURIComponent(urlParams.get("whepplay") || urlParams.get("whep")); + if (session.whepInput) { + setTimeout(function () { + whepIn(); + }, 1000); + } + } catch (e) { + errorlog(e); + } + } + } + if (urlParams.has("whepplaytoken") || urlParams.has("wheptoken")) { + // URL or data:base64 image. Becomes local to this viewer only. This is like &avatar, but slightly different. Just CSS in this case + if (urlParams.get("whepplaytoken") || urlParams.get("wheptoken")) { + try { + session.whepInputToken = urlParams.get("whepplaytoken") || urlParams.get("wheptoken"); + } catch (e) { + errorlog(e); + } + } + } + + if (urlParams.has("nomouseevents") || urlParams.has("nme")) { + session.disableMouseEvents = true; + } + if (urlParams.has("overlaycontrols")) { + session.overlayControls = true; + } + + if (urlParams.has("novideobutton") || urlParams.has("nvb")) { + getById("mutevideobutton").style.setProperty("display", "none", "important"); + } + + if (urlParams.has("nospeakerbutton") || urlParams.has("nsb")) { + getById("mutespeakerbutton").style.setProperty("display", "none", "important"); + } + + if (urlParams.has("noscale") || urlParams.has("noscaling")) { + session.noScaling = true; + } + + if (urlParams.has("pusheffectsdata")) { + session.pushEffectsData = true; + } + + if (urlParams.has("pushloudness") || urlParams.has("getloudness")) { + // this sets the loudness IFRAME API output, if available. + session.pushLoudness = true; + } + + if (urlParams.has("pushfaces") || urlParams.has("getfaces")) { + session.grabFaceData = true; + setTimeout(function () { + // give the app some time to load + getFaces(); + }, 2000); + } + + if (urlParams.has("notmobile")) { + session.mobile = false; + } else if (urlParams.has("mobile")) { + session.mobile = true; + session.audioEffects = false; // disable audio inbound effects also. + session.audioMeterGuest = false; + } else if (iOS || iPad) { + if (SafariVersion && SafariVersion < 17) { + getById("oldiOSWarning").classList.remove("hidden"); // update this to 17 at some point. + } + session.mobile = true; + session.audioEffects = false; // disable audio inbound effects also. + session.audioMeterGuest = false; + window.addEventListener("resize", function () { + // Safari is the new IE. + + if (session.ws) { + var msg = {}; + msg.requestSceneUpdate = true; + session.sendMessage(msg); + } + if (screen && screen.orientation && screen.orientation.type) { + if (screen.orientation.type.includes("portrait")) { + document.getElementsByTagName("html")[0].style.height = "100vh"; + setTimeout(function () { + document.getElementsByTagName("html")[0].style.height = "100%"; + }, 1000); + } else if (screen.orientation.type.includes("landscape")) { + document.getElementsByTagName("html")[0].style.height = "100vh"; + setTimeout(function () { + document.getElementsByTagName("html")[0].style.height = "100%"; + }, 1000); + } + } else if (window.matchMedia("(orientation: portrait)").matches) { + document.getElementsByTagName("html")[0].style.height = "100vh"; + setTimeout(function () { + document.getElementsByTagName("html")[0].style.height = "100%"; + }, 1000); + } else if (window.matchMedia("(orientation: landscape)").matches) { + document.getElementsByTagName("html")[0].style.height = "100vh"; + setTimeout(function () { + document.getElementsByTagName("html")[0].style.height = "100%"; + }, 1000); + } + }); + + if (/CriOS/i.test(navigator.userAgent)) { + // if runngin Chrome on iOS + if (!session.cleanOutput) { + try { + navigator.mediaDevices.getUserMedia; + } catch (e) { + warnUser("Chrome on this device does not support the required technology to use this site.\n\nPlease use Safari instead or update your iOS and browser version."); + } + } + } + } else if (/Android|Pixel|webOS|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { + // not sure how accurate this is. + session.mobile = true; + session.audioEffects = false; // disable audio inbound effects also. + session.audioMeterGuest = false; + } else { + log("MAKE DRAGGABLE"); + delayedStartupFuncs.push([makeDraggableElement, getById("subControlButtons")]); + if (SafariVersion && !ChromiumVersion) { + // if desktop Safari, so macOS, give a note saying it sucks + getById("SafariWarning").classList.remove("hidden"); + } + } + + if (urlParams.has("broadcasttransfer") || urlParams.has("bct")) { + log("Broadcast transfer flag set"); + session.broadcastTransfer = urlParams.get("broadcasttransfer") || urlParams.get("bct") || null; + if (session.broadcastTransfer === "false") { + session.broadcastTransfer = false; + } else if (session.broadcastTransfer === "0") { + session.broadcastTransfer = false; + } else if (session.broadcastTransfer === "no") { + session.broadcastTransfer = false; + } else if (session.broadcastTransfer === "off") { + session.broadcastTransfer = false; + } else { + session.broadcastTransfer = true; + } + if (transferSettings) { + transferSettings.broadcast = session.broadcastTransfer; + } + } + + if (urlParams.has("queuetransfer") || urlParams.has("qt")) { + log("Broadcast transfer flag set"); + session.queueTransfer = urlParams.get("queuetransfer") || urlParams.get("qt") || null; + if (session.queueTransfer === "false") { + session.queueTransfer = false; + } else if (session.queueTransfer === "0") { + session.queueTransfer = false; + } else if (session.queueTransfer === "no") { + session.queueTransfer = false; + } else if (session.queueTransfer === "off") { + session.queueTransfer = false; + } else { + session.queueTransfer = true; + } + if (transferSettings) { + transferSettings.queue = session.queueTransfer; + } + } + + if (urlParams.has("broadcast") || urlParams.has("bc")) { + log("Broadcast flag set"); + session.broadcast = urlParams.get("broadcast") || urlParams.get("bc") || null; + + if (session.broadcast === "false") { + session.broadcast = false; + } else if (session.broadcast === "0") { + session.broadcast = false; + } else if (session.broadcast === "no") { + session.broadcast = false; + } else if (session.broadcast === "off") { + session.broadcast = false; + } + + //if ((iOS) || (iPad)) { + // session.nopreview = false; + //} else { + // session.nopreview = true; + //} + session.minipreview = 2; // full screen if nothing else on screen. + session.style = 1; + //getById("header").style.display = "none"; + //getById("header").style.opacity = 0; + session.showList = true; + } + + if (urlParams.has("showlist")) { + session.showList = urlParams.get("showlist"); + if (session.showList === "false") { + session.showList = false; + } else if (session.showList === "0") { + session.showList = false; + } else if (session.showList === "no") { + session.showList = false; + } else if (session.showList === "off") { + session.showList = false; + } else { + session.showList = true; + } + } + //if (session.showList===true){ + // getById("hideusers").classList.add("hidden"); + //} + if (urlParams.has("meshcast2")) { session.meshcast2 = urlParams.get("meshcast2") || "any"; meshcast2(); @@ -995,2688 +1021,2749 @@ async function main() { meshcast(true); } - - if (urlParams.has("meshcastcode") || urlParams.has("mccode")) { - session.meshcastCode = urlParams.get("meshcastcode") || urlParams.get("mccode") || false; - } - - if (urlParams.has("nomeshcast") || urlParams.has("nowhep")) { - session.noMeshcast = urlParams.get("nomeshcast") || urlParams.has("nowhep") || true; - } - - if (urlParams.has("chunkcast")) { - session.chunkcast = true; - } - - if (urlParams.get("discordwebhook") || urlParams.get("dwh")) { - session.discordHook = decodeURIComponent(urlParams.get("discordwebhook") || urlParams.get("dwh")); - if (!session.discordHook.startsWith("https://discord.com/api/webhooks/")){ - session.discordHook = "https://discord.com/api/webhooks/"+session.discordHook; - } - } else if (urlParams.get("discordwebhook2") || urlParams.get("dwh2")) { - session.discordHook = decodeURIComponent(urlParams.get("discordwebhook2") || urlParams.get("dwh2")); - session.discordHookSensitive = true; - if (!session.discordHook.startsWith("https://discord.com/api/webhooks/")){ - session.discordHook = "https://discord.com/api/webhooks/"+session.discordHook; - } - } - - if (urlParams.has("drawing")) { - session.allowDrawing = urlParams.get("drawing") || true; - } - - if (urlParams.has("nohistory")) { - session.nohistory = true; - } else if (urlParams.has("history")){ - session.nohistory = false; - } else if (isIFrame){ - session.nohistory = true; - } - - if (urlParams.has("pagezoom")) { - enableFullscreenZoom(); - } - //if (urlParams.has('callin')){ - // awaitInboundCall()(); - //} - - //if (urlParams.has("relaywss")) { - // session.relaywss = true; - //} - - if (urlParams.has("fulltalk") && urlParams.get("fulltalk").length == 6) { - listenWebsocket(urlParams.get("fulltalk"), false); // talk and hear all - } else if (urlParams.has("justtalk") && urlParams.get("justtalk").length == 6) { - joinConference(urlParams.get("justtalk")); // just talk - - if (urlParams.has("hearptsn")) { - listenWebsocket(urlParams.get("justtalk")); // hear ptsn only - } - } else if (urlParams.has("hearptsn") && urlParams.get("hearptsn").length == 6) { - listenWebsocket(urlParams.get("hearptsn")); // hear ptsn only - - if (urlParams.has("justtalk")) { - joinConference(urlParams.get("hearptsn")); - } - } - - var filename = false; - try { - if (!session.decrypted) { - filename = window.location.pathname.substring(window.location.pathname.lastIndexOf("/") + 1); - filename = filename.replace("??", "?"); - filename2 = filename.split("?")[0]; - // split at ??? - if (filename.split(".").length == 1) { - if (filename2.length < 2) { - // easy win - filename = false; - } else if (filename.startsWith("&")) { - // easy win - var tmpHref = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/?" + filename.split("&").slice(1).join("&"); - log("TMP " + tmpHref); - updateURL(filename.split("&")[1], true, tmpHref); - filename = false; - } else if (filename2.split("&")[0].includes("=")) { - log("asdf " + filename.split("&")[0]); - if (history.pushState) { - var tmpHref = window.location.href.substring(0, window.location.href.lastIndexOf("/")); - tmpHref = tmpHref + "/?" + filename; - filename = false; - //warnUser("Please ensure your URL is correctly formatted."); - if (!session.nohistory){ - window.history.pushState({ path: tmpHref.toString() }, "", tmpHref.toString()); - } - } - } else { - filename = filename2.split("&")[0]; - if (filename2 != filename) { - warnUser("Warning: Please ensure your URL is correctly formatted."); - } - } - } else { - filename = false; - } - log(filename); - } - } catch (e) { - errorlog(e); - } - - var directorLanding = false; - if (session.director) { - if (session.director === true) { - // room not specified. - directorLanding = true; - } - session.meterStyle = 1; - session.signalMeter = true; - session.batteryMeter = true; - } else if (filename === "director") { - directorLanding = true; - filename = false; - session.meterStyle = 1; - session.signalMeter = true; - session.batteryMeter = true; - } - - - - if (urlParams.has("updateonslotschange") || urlParams.has("uosc")) { - session.updateOnSlotChange = true; - } - - - if (urlParams.has("signalmeter")) { - session.signalMeter = urlParams.get("signalmeter"); - if (session.signalMeter === "false") { - session.signalMeter = false; - } else if (session.signalMeter === "0") { - session.signalMeter = false; - } else if (session.signalMeter === "no") { - session.signalMeter = false; - } else if (session.signalMeter === "off") { - session.signalMeter = false; - } else { - session.signalMeter = true; - } - } - - if (urlParams.has("batterymeter")) { - session.batteryMeter = urlParams.get("batterymeter"); - if (session.batteryMeter === "false") { - session.batteryMeter = false; - } else if (session.batteryMeter === "0") { - session.batteryMeter = false; - } else if (session.batteryMeter === "no") { - session.batteryMeter = false; - } else if (session.batteryMeter === "off") { - session.batteryMeter = false; - } else { - session.batteryMeter = true; - } - } - - if (urlParams.has("rooms")) { - session.rooms = urlParams - .get("rooms") - .split(",") - .map(function (e) { - return sanitizeRoomName(e); - }); - getById("rooms").classList.remove("hidden"); - } - - if (urlParams.has("leaveorientationflag")) { - session.removeOrientationFlag = false; // leave `a=extmap:3 urn:3gpp:video-orientation\r\n` alone - } - - if (urlParams.has("showdirector") || urlParams.has("sd")) { - session.showDirector = parseInt(urlParams.get("showdirector")) || parseInt(urlParams.get("sd")) || true; // if 2, video only allowed. True or 1 will be video + audio allowed. - // fyi, true is the same as 1 when == is used, so assert(1==true) is true. - } - - if (urlParams.has("bitratecutoff") || urlParams.has("bitcut")) { - session.lowBitrateCutoff = parseInt(urlParams.get("bitratecutoff")) || parseInt(urlParams.get("bitcut")) || 300; // low bitrate cut off. - session.hiddenSceneViewBitrate = false; - } - - if (urlParams.has("motionswitch") || urlParams.has("motiondetection")) { - // switch OBS to this scene when there is motion, and "solo view" this video in the VDO.Ninja auto-mixer, if used - session.motionSwitch = parseInt(urlParams.get("motionswitch")) || parseInt(urlParams.get("motiondetection")) || 15; // threshold of motion needed to trigger - session.hiddenSceneViewBitrate = false; - } - - if (urlParams.has("motionrecord") || urlParams.has("recordmotion")) { - // switch OBS to this scene when there is motion, and "solo view" this video in the VDO.Ninja auto-mixer, if used - session.motionRecord = parseInt(urlParams.get("motionrecord")) || parseInt(urlParams.get("recordmotion")) || 15; // threshold of motion needed to trigger - session.hiddenSceneViewBitrate = false; - } - - if (urlParams.has("pausepreview") || urlParams.has("dpp")) { - try { - session.directorViewBitrate = 0; - document.querySelector('#controls_blank button[data-action-type="change-quality1"]').classList.add("pressed"); - document.querySelector('#controls_blank button[data-action-type="change-quality2"]').classList.remove("pressed"); - document.querySelector('#controls_blank button[data-action-type="change-quality3"]').classList.remove("pressed"); - } catch (e) { - errorlog(e); - } - } - if (urlParams.has("locked")) { - session.locked = urlParams.get("locked"); - - if (session.locked == "portrait" || session.locked == "vertical") { - session.locked = 9.0 / 16.0; - } else if (session.locked == "landscape") { - session.locked = 16.0 / 9.0; - } else if (session.locked == "square") { - session.locked = 1.0; - } else { - session.locked = parseFloat(session.locked) || 16 / 9.0; - } - } - - if (urlParams.has("lowbitratescene") || urlParams.has("cutscene")) { - session.lowBitrateSceneChange = urlParams.get("lowbitratescene") || urlParams.get("cutscene") || "cutscene"; // low bitrate cut off. - session.hiddenSceneViewBitrate = false; - if (session.lowBitrateCutoff === false) { - session.lowBitrateCutoff = 300; - } - } - - if (urlParams.has("rotate")) { - session.rotate = urlParams.get("rotate") || 90; - session.rotate = parseInt(session.rotate); - } - - if (urlParams.has("rotatewindow") || urlParams.has("rotatepage")) { - let rotateThis = parseInt(urlParams.get("rotatewindow")) || parseInt(urlParams.get("rotatepage")) || 90; - updateForceRotatedCSS(rotateThis); - } - - if (urlParams.has("facing")) { - session.facingMode = urlParams.get("facing") || false; - } - if (session.facingMode) { - session.facingMode = session.facingMode.toLowerCase(); - if (session.facingMode == "user") { - // - } else if (session.facingMode == "environment") { - // - } else if (session.facingMode == "rear") { - session.facingMode = "environment"; - } else if (session.facingMode == "back") { - session.facingMode = "environment"; - } else if (session.facingMode == "front") { - session.facingMode = "user"; - } else { - session.facingMode = false; - } - } - - // session.facingMode }; // user or environment - - if (urlParams.has("forcelandscape") || urlParams.has("forcedlandscape") || urlParams.has("fl")) { - session.orientation = "landscape"; - if (Firefox) { - session.fullscreen = true; // windowed mode complicates things in this mode - } - } else if (urlParams.has("forceportrait") || urlParams.has("forcedportrait") || urlParams.has("fp")) { - session.orientation = "portrait"; - if (Firefox) { - session.fullscreen = true; // windowed mode complicates things in this mode - } - } - - if (urlParams.has("forceviewerlandscape")) { - session.keepIncomingVideosInLandscape = parseInt(urlParams.get("forceviewerlandscape")) || 270; - } - - if (urlParams.has("forceviewerportrait")) { - session.keepIncomingVideosInPortrait = parseInt(urlParams.get("forceviewerportrait")) || 90; - } - - document.addEventListener("fullscreenchange", event => { - log("full screen change event"); - log(event); - - // Handle myVideo class for self-preview when using fullscreen button - if (session.fullscreenButton) { - var videoSource = document.getElementById("videosource") || document.getElementById("previewWebcam"); - if (videoSource) { - if (document.fullscreenElement) { - // Entering fullscreen - store original class and remove myVideo constraint - if (!videoSource.dataset.originalClass) { - videoSource.dataset.originalClass = videoSource.className; - } - videoSource.classList.remove("myVideo"); - } else { - // Exiting fullscreen - restore original class - if (videoSource.dataset.originalClass) { - videoSource.className = videoSource.dataset.originalClass; - delete videoSource.dataset.originalClass; - } - } - } - } - - if (document.getElementById("previewWebcam")) { - // Update fullscreen icon even in preview mode - if (document.fullscreenElement) { - getById("fullscreenPageToggle").classList.remove("la-expand-arrows-alt"); - getById("fullscreenPageToggle").classList.add("la-compress-arrows-alt"); - } else { - getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); - getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); - } - return; - } - - if (session.orientation && session.mobile) { - if (document.fullscreenElement) { - document.exitFullscreen(); - getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); - getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); - } - return; - } - if (document.fullscreenElement) { - getById("fullscreenPageToggle").classList.remove("la-expand-arrows-alt"); - getById("fullscreenPageToggle").classList.add("la-compress-arrows-alt"); - } else { - getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); - getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); - } - updateMixer(); - }); - - if (urlParams.has("fullscreenbutton") || urlParams.has("fsb")) { - // just an alternative; might be compoundable - if (!(iOS || iPad)) { - session.fullscreenButton = true; - document.documentElement.style.setProperty("--full-screen-button", "none"); - getById("fullscreenPage").classList.remove("hidden"); - } - } else if (urlParams.has("nofullscreenbutton") || urlParams.has("nofsb")) { - // just an alternative; might be compoundable - session.nofullwindowbutton = true; - } - - if (urlParams.has("pip2") || urlParams.has("pipall")) { - // just an alternative; might be compoundable - if (typeof documentPictureInPicture !== "undefined") { - getById("PictureInPicturePage").classList.remove("hidden"); - } - } - - if (urlParams.has("midi") || urlParams.has("hotkeys")) { - session.midiHotkeys = urlParams.get("midi") || urlParams.get("hotkeys") || 1; - session.midiHotkeys = parseInt(session.midiHotkeys); - } - - if (urlParams.has("disablehotkeys")) { - session.disableHotKeys = true; - } - - if (urlParams.has("nohangupbutton") || urlParams.has("nohub")) { - getById("hangupbutton").style.display = "none"; - session.hangupbutton = false; - } - - if (urlParams.has("hangupbutton") || urlParams.has("hub") || urlParams.has("humb64")) { - session.hangupbutton = true; - } - if (urlParams.has("hangupmessage") || urlParams.has("hum") || urlParams.has("humb64")) { - let htmlmessage = urlParams.get("hangupmessage") || urlParams.get("hum") || urlParams.get("humb64"); - - if (urlParams.get("humb64")) { - try { - htmlmessage = atob(htmlmessage); - } catch (e) {} - } - - try { - htmlmessage = htmlmessage.replace(/(\r\n|\n|\r)/gm, ""); - htmlmessage = decodeURIComponent(htmlmessage); - } catch (e) { - console.error(e); - } - getById("hangupContainer").innerHTML = htmlmessage; - } - - if (urlParams.has("socialstream")) { - session.socialstream = urlParams.get("socialstream") || false; - } - - if (urlParams.has("midioffset")) { - session.midiOffset = urlParams.get("midioffset") || 0; - session.midiOffset = parseInt(session.midiOffset); - } - - if (urlParams.has("midiremote") || urlParams.has("remotemidi")) { - if (session.director !== false) { - session.midiRemote = parseInt(urlParams.get("midiremote")) || parseInt(urlParams.get("remotemidi")) || 4; - } else { - session.midiRemote = parseInt(urlParams.get("midiremote")) || parseInt(urlParams.get("remotemidi")) || 1; - } - } - - if (urlParams.has("midipush") || urlParams.has("midiout") || urlParams.has("mo")) { - session.midiOut = parseInt(urlParams.get("midipush")) || parseInt(urlParams.get("midiout")) || parseInt(urlParams.get("mo")) || true; - } - - if (urlParams.has("tc") || urlParams.has("timecode") || urlParams.has("showtimecode")) { - session.midiTimecode = urlParams.get("tc") || urlParams.get("timecode") || urlParams.get("showtimecode") || true; - } - - if (urlParams.has("nochunkediframestats")) { - session.chunkIframe = false; - } - - if (urlParams.has("midiiframe")) { - session.midiIframe = true; - } - - if (urlParams.has("midipull") || urlParams.has("midiin") || urlParams.has("midin") || urlParams.has("mi")) { - session.midiIn = parseInt(urlParams.get("midipull")) || parseInt(urlParams.get("midiin")) || parseInt(urlParams.get("midin")) || parseInt(urlParams.get("mi")) || true; - } - - if (urlParams.has("mididelay")) { - // midi-in delay - session.midiDelay = parseInt(urlParams.get("mididelay")) || 1000; // 1 second playout delay? acts as a buffer as well I guess. - } - - if (urlParams.has("midichannel")) { - session.midiChannel = parseInt(urlParams.get("midichannel")) || false; - } - if (session.midiChannel) { - session.midiChannel = parseInt(session.midiChannel); - if (session.midiChannel > 16) { - session.midiChannel = false; - } - if (session.midiChannel < 1) { - session.midiChannel = false; - } - } - if (urlParams.has("mididevice")) { - session.midiDevice = parseInt(urlParams.get("mididevice")) || false; - } - - if (urlParams.has("ptt")) { - if (urlParams.get("ptt")) { - setHotKeyAuto(urlParams.get("ptt")); - } else { - promptAlt("Select a hotkey", true, false, getById("pptHotKey").value, false, false, true); - } - } - - if (directorLanding) { - getById("container-1").classList.remove("hidden"); - getById("container-1").classList.add("skip-animation"); - getById("container-1").classList.remove("pointer"); - } else if (session.director) { - // if a director, webcam/screenshare/iframe auto-defaults shouldn't work - } else if (urlParams.has("webcam") || urlParams.has("wc") || urlParams.has("miconly")) { - session.webcamonly = true; - session.screensharebutton = false; - if (urlParams.has("miconly")) { - session.videoDevice = 0; - session.miconly = true; - miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); - getById("container-3").title = getById("add_camera").innerText; - - getById("videoMenu").style.display = "none"; - getById("container-3").classList.add("microphoneBackground"); - getById("flipcamerabutton").style.setProperty("display", "none", "important"); - getById("mutevideobutton").style.setProperty("display", "none", "important"); - getById("videoMenu3").style.setProperty("display", "none", "important"); - getById("previewWebcam").classList.add("miconly"); - //if (session.consent){ - // setTimeout(function(){ - // warnUser("⚠ Privacy warning: The director of this room can remotely switch your camera or microphone without permission.", 8000); - // }, 1500); - //} - } - } else if (urlParams.has("screenshare") || urlParams.has("ss")) { - session.screenshare = true; - if (urlParams.get("screenshare") || urlParams.get("ss")) { - session.screenshare = urlParams.get("screenshare") || urlParams.get("ss"); - } - } else if (urlParams.has("fileshare") || urlParams.has("fs")) { - getById("container-5").classList.remove("hidden"); - getById("container-5").classList.add("skip-animation"); - getById("container-5").classList.remove("pointer"); - - //getById("sharefilebutton").style.display = "flex"; // this might be obsolete? - // getById("mediafileshare").classList.remove("hidden"); - - if (SafariVersion) { - getById("safari_warning_fileshare").classList.remove("hidden"); - } else if (!Firefox) { - getById("chrome_warning_fileshare").classList.remove("hidden"); - } - } else if (!session.director && (urlParams.has("website") || urlParams.has("iframe"))) { - getById("container-6").classList.remove("hidden"); - getById("container-6").classList.add("skip-animation"); - getById("container-6").classList.remove("pointer"); - session.website = urlParams.get("website") || urlParams.get("iframe") || false; - if (session.website) { - session.website = decodeURI(session.website); - delayedStartupFuncs.push([session.publishIFrame, session.website]); - } - } else if (!session.director && (urlParams.has("framegrab"))) { - getById("container-6").classList.remove("hidden"); - getById("container-6").classList.add("skip-animation"); - getById("container-6").classList.remove("pointer"); - session.framegrab = urlParams.get("framegrab") || false; - session.framegrabAudio = false; - session.framegrabAudioRequested = false; - const framegrabAudioParamName = urlParams.has("framegrabaudio") - ? "framegrabaudio" - : (urlParams.has("audio") ? "audio" : null); - session.pendingFramegrabAudioSettings = null; - if (framegrabAudioParamName) { - session.framegrabAudioRequested = true; - const rawValue = urlParams.get(framegrabAudioParamName); - if (rawValue === null || rawValue === "") { - session.framegrabAudio = true; - } else { - const normalized = String(rawValue).toLowerCase(); - session.framegrabAudio = !["0", "false", "no", "off"].includes(normalized); - } - if (session.framegrabAudio) { - session.pendingFramegrabAudioSettings = { enable: true }; - } else { - session.pendingFramegrabAudioSettings = { enable: false }; - } - } - delayedStartupFuncs.push([session.publishFrameSource, session.framegrab]); - // session.publishFrameSource - } else if (urlParams.has("webcam2") || urlParams.has("wc2")) { - session.webcamonly = true; - session.screensharebutton = false; - session.introButton = true; - } else if (urlParams.has("screenshare2") || urlParams.has("ss2")) { - session.screenshare = true; - session.introButton = true; - if (urlParams.get("screenshare2") || urlParams.get("ss2")) { - session.screenshare = urlParams.get("screenshare2") || urlParams.get("ss2"); - } - } - - if (session.director && (urlParams.has("website") || urlParams.has("iframe"))) { - getById("container-6").classList.remove("hidden"); - getById("container-6").classList.add("skip-animation"); - getById("container-6").classList.remove("pointer"); - session.website = urlParams.get("website") || urlParams.get("iframe") || false; - if (session.website) { - session.website = decodeURI(session.website); - delayedStartupFuncs.push([shareWebsite, session.website]); - } - } - - if (urlParams.has("sstype") || urlParams.has("screensharetype")) { - // wha type of screen sharing is used; track replace, iframe, or secondary try - session.screenshareType = urlParams.get("sstype") || urlParams.get("screensharetype"); - session.screenshareType = parseInt(session.screenshareType) || false; - } - - if (urlParams.has("ssstyle") || urlParams.has("screensharestyle")) { - // wha type of screen sharing is used; track replace, iframe, or secondary try - session.screenshareStyle = urlParams.get("ssstyle") || urlParams.get("screensharestyle") || 1; - session.screenshareStyle = parseInt(session.screenshareStyle) || false; - } - if (urlParams.has("alignright") || urlParams.has("rightalign")) { - let alignValue = urlParams.get("alignright"); - if (alignValue === null) { - alignValue = urlParams.get("rightalign"); - } - if (alignValue === null || alignValue === "") { - session.alignRight = true; - } else { - const normalizedAlign = String(alignValue).toLowerCase(); - session.alignRight = !["0", "false", "no", "off"].includes(normalizedAlign); - } - } - - if (urlParams.has("suppresslocalaudio")) { - session.suppressLocalAudioPlayback = true; - } - if (urlParams.has("prefercurrenttab")) { - session.preferCurrentTab = true; - } - if (urlParams.has("selfbrowsersurface")) { - // exclude - session.selfBrowserSurface = urlParams.get("selfbrowsersurface") || "exclude"; - } - if (urlParams.has("surfaceswitching")) { - session.surfaceSwitching = urlParams.get("surfaceswitching") || "exclude"; - } - if (urlParams.has("systemaudio")) { - // exclude or exclude - session.systemAudio = urlParams.get("systemaudio") || "exclude"; - } - if (urlParams.has("displaysurface")) { - // browser, window, or monitor (which is default selected) - session.displaySurface = urlParams.get("displaysurface") || "monitor"; - } - - if (urlParams.has("recordwindow") || urlParams.has("rw")) { - // Streamlined scene window recording - captures current tab and records to disk - session.recordWindow = true; - session.cleanOutput = true; - session.preferCurrentTab = true; - session.selfBrowserSurface = "include"; - session.displaySurface = "browser"; - session.suppressLocalAudioPlayback = true; - if (urlParams.get("recordwindow") || urlParams.get("rw")) { - session.recordWindow = parseInt(urlParams.get("recordwindow") || urlParams.get("rw")) || 6000; // bitrate - } - } - - if (urlParams.has("locksize")) { - // browser, window, or monitor (which is default selected) - session.lockWindowSize = urlParams.get("locksize") || true; - } - - if (urlParams.has("intro") || urlParams.has("ib")) { - session.introButton = true; - } - - if (urlParams.has("volumecontrol") || urlParams.has("volumecontrols") || urlParams.has("vc")) { - if (!(iOS || iPad)) { - session.volumeControl = true; - } - } - - if (urlParams.has("autohide")) { - session.autohide = true; - session.dedicatedControlBarSpace = false; - } - - if (urlParams.has("controlbarspace")) { - session.dedicatedControlBarSpace = true; - } else if (urlParams.has("nocontrolbarspace")) { - session.dedicatedControlBarSpace = false; - } - - if (urlParams.has("hidesolo") || urlParams.has("hs")) { - session.hidesololinks = true; - } - - if (urlParams.has("ignorehighlight") || urlParams.has("ih")) { - session.ignoreHighlight = true; - } - - if (urlParams.has("mute") || urlParams.has("muted") || urlParams.has("m")) { - session.muted = true; - } - - if (urlParams.has("hideguest") || urlParams.has("hidden")) { - session.directorVideoMuted = true; - } - - if (urlParams.has("videomute") || urlParams.has("videomuted") || urlParams.has("vm")) { - session.videoMutedFlag = true; - } - - if (urlParams.has("pauseinvisible")) { - session.pauseInvisible = true; - } - - if (urlParams.has("zoomslider")) { - session.zoomSlider = true; - } - if (urlParams.has("ptzslider") || urlParams.has("ptzcontrol") || urlParams.has("ptzcontrols")) { - session.ptzSlider = true; - } - - if (urlParams.get("viewslot")) { - session.viewslot = parseInt(urlParams.get("viewslot")) || false; - session.accept_layouts = true; - session.layout = {}; - session.exclusiveLayoutAudio = true; - session.hiddenSceneViewBitrate = 0; - - } else if (urlParams.has("layout")) { - if (!urlParams.get("layout")) { - session.accept_layouts = true; - session.layout = {}; - } else { - - let decodedParam; - try { - decodedParam = decodeURIComponent(urlParams.get("layout")); - } catch (e) { - decodedParam = urlParams.get("layout"); - } - try { - session.layout = JSON.parse(decodedParam); - } catch (e) { - try { - const base64Decoded = atob(decodedParam); - try { - session.layout = JSON.parse(base64Decoded); - } catch (e) { - session.layout = base64Decoded; - } - } catch (e) { - session.layout = decodedParam; - } - } - - if (typeof session.layout === 'object' && session.layout !== null && Object.keys(session.layout).length > 0) { - session.updateOnSlotChange = true; - } - } - console.warn("Warning: If using &layout with &broadcast, only the director's video will appear in the custom layout, which is likely not intended."); - } - - if (urlParams.get("updateonslotschange") || urlParams.get("uosc")) { - let uosc = urlParams.get("updateonslotschange") || urlParams.get("uosc"); - if (["false","0","off"].includes(uosc)){ - session.updateOnSlotChange = false; - } - } - - if (urlParams.get("exclusivelayoutaudio")) { - session.exclusiveLayoutAudio = true; - } else if (urlParams.get("inclusivelayoutaudio")) { - session.exclusiveLayoutAudio = false; - } - - if (urlParams.has("layouts")) { - // an ordered array of layouts, which can be used to switch between using the API layouts action. - // ie: ?layouts=[[{"x":0,"y":0,"w":100,"h":100,"slot":0}],[{"x":0,"y":0,"w":100,"h":100,"slot":1}],[{"x":0,"y":0,"w":100,"h":100,"slot":2}],[{"x":0,"y":0,"w":100,"h":100,"slot":3}],[{"x":0,"y":0,"w":50,"h":100,"c":false,"slot":0},{"x":50,"y":0,"w":50,"h":100,"c":false,"slot":1}],[{"x":0,"y":0,"w":100,"h":100,"z":0,"c":false,"slot":1},{"x":70,"y":70,"w":30,"h":30,"z":1,"c":true,"slot":0}],[{"x":0,"y":0,"w":50,"h":50,"c":true,"slot":0},{"x":50,"y":0,"w":50,"h":50,"c":true,"slot":1},{"x":0,"y":50,"w":50,"h":50,"c":true,"slot":2},{"x":50,"y":50,"w":50,"h":50,"c":true,"slot":3}],[{"x":0,"y":16.667,"w":66.667,"h":66.667,"c":true,"slot":0},{"x":66.667,"y":0,"w":33.333,"h":33.333,"c":true,"slot":1},{"x":66.667,"y":33.333,"w":33.333,"h":33.333,"c":true,"slot":2},{"x":66.667,"y":66.667,"w":33.333,"h":33.333,"c":true,"slot":3}]] - try { - session.layouts = JSON.parse(decodeURIComponent(urlParams.get("layouts"))) || JSON.parse(urlParams.get("layouts")) || {}; - } catch (e) { - try { - session.layouts = JSON.parse(urlParams.get("layouts")) || false; - } catch (e) { - session.layouts = false; - } - } - } - - /* if (session.layout && session.layouts && (typeof session.layout !== "object") && parseInt(session.layout) && (session.layout == parseInt(session.layout))){ - try { - session.layout = session.layouts[session.layout-1]; - } catch(e){ - session.layout= false; - } - } */ - - if (urlParams.has("deaf") || urlParams.has("deafen")) { - session.directorSpeakerMuted = true; // false == true in this case. - } - - if (urlParams.has("blind")) { - session.directorDisplayMuted = true; // false == true in this case. - } - - if (urlParams.has("blindall")) { - session.directorBlindButton = true; // false == true in this case. - } - if (session.directorBlindButton) { - getById("blindAllGuests").classList.remove("hidden"); - } - - if (urlParams.has("dpi") || urlParams.has("dpr") || urlParams.has("sharper") || urlParams.has("sharpen")) { - session.devicePixelRatio = urlParams.get("dpi") || urlParams.get("dpr") || 2.0; - session.devicePixelRatio = parseFloat(session.devicePixelRatio); - } //else if (window.devicePixelRatio && window.devicePixelRatio!==1){ - // session.devicePixelRatio = window.devicePixelRatio; // this annoys me to no end. - //} - - if (urlParams.has("speakermute") || urlParams.has("speakermuted") || urlParams.has("mutespeaker") || urlParams.has("sm") || urlParams.has("ms")) { - var checkState = urlParams.get("speakermute") || urlParams.get("speakermuted") || urlParams.get("mutespeaker") || urlParams.get("sm") || urlParams.get("ms") || true; - - if (checkState === "false") { - session.speakerMuted = false; - } else if (checkState === "0") { - session.speakerMuted = false; - } else if (checkState === "no") { - session.speakerMuted = false; - } else if (checkState === "off") { - session.speakerMuted = false; - } else { - session.speakerMuted = true; - } - - session.speakerMuted_default = session.speakerMuted; - - if (session.speakerMuted) { - getById("mutespeakertoggle").className = "las la-volume-mute toggleSize"; - //getById("mutespeakerbutton").className="hidden float2 red"; - getById("mutespeakerbutton").classList.add("red"); - getById("mutespeakerbutton").classList.add("float2"); - getById("mutespeakerbutton").classList.remove("float"); - - var sounds = document.getElementsByTagName("video"); - for (var i = 0; i < sounds.length; ++i) { - sounds[i].muted = session.speakerMuted; - } - } - } - - if (urlParams.has("chatbutton") || urlParams.has("chat") || urlParams.has("cb")) { - session.chatbutton = urlParams.get("chatbutton") || urlParams.get("chat") || urlParams.get("cb") || null; - if (session.chatbutton === "false") { - session.chatbutton = false; - } else if (session.chatbutton === "0") { - session.chatbutton = false; - } else if (session.chatbutton === "no") { - session.chatbutton = false; - } else if (session.chatbutton === "off") { - session.chatbutton = false; - } else { - session.chatbutton = true; - getById("chatbutton").classList.remove("hidden"); - } - } - - // Tipping feature - unified &tipsid parameter - // &tipsid=xxx → use this overlay token, enable tips, fetch username from API - // &tipsid (no value) → show onboarding modal for signup - // Legacy: &tip, &tips, &tipid also supported for backwards compatibility - if (urlParams.has("tipsid") || urlParams.has("tip") || urlParams.has("tipid")) { - var tipsIdValue = urlParams.get("tipsid") || urlParams.get("tip") || urlParams.get("tipid"); - session.receiveTips = true; - if (tipsIdValue) { - session.tipsId = tipsIdValue; - } else { - // No ID specified - show onboarding modal after page loads - setTimeout(function() { - if (typeof showTipOnboardingModal === 'function') { - showTipOnboardingModal(); - } - }, 2000); - } - } - // Legacy aliases for showing onboarding - if (urlParams.has("receivetips") || urlParams.has("tipping")) { - session.receiveTips = true; - if (!session.tipsId) { - setTimeout(function() { - if (typeof showTipOnboardingModal === 'function') { - showTipOnboardingModal(); - } - }, 2000); - } - } - if (urlParams.has("tipserver")) { - session.tipServer = urlParams.get("tipserver"); - } - if (urlParams.has("tipamounts")) { - try { - session.tipAmounts = urlParams.get("tipamounts").split(",").map(x => parseInt(x)).filter(x => x > 0); - } catch (e) { - session.tipAmounts = [5, 10, 25, 50, 100]; - } - } - if (urlParams.has("tipcurrency")) { - session.tipCurrency = urlParams.get("tipcurrency").toUpperCase(); - } - // Viewer opt-in to see tip UI (two-way opt-in system) - if (urlParams.has("showtips") || urlParams.has("supporttips")) { - session.showTips = true; - } - // QR code size for tip overlay (default 150px, min 100px for scanability) - if (urlParams.has("tipqrsize")) { - session.tipQRSize = Math.max(parseInt(urlParams.get("tipqrsize")) || 150, 100); - } - // Disable QR code animation - if (urlParams.has("notipqr")) { - session.noTipQR = true; - } - - if (urlParams.has("app")) { - // midi-in delay - session.screenshare = false; - getById("container-2").classList.add("hidden"); - getById("logoname").classList.add("hidden"); - getById("head1a").classList.remove("hidden"); - getById("main").classList.add("appmode"); - getById("jumptoroomButton").innerText = "Join Room"; - - if (getStorage("jumptoURL")) { - getById("joinbyURL").value = getStorage("jumptoURL"); - } - } - - if (session.screenshare !== false) { - if (session.introButton) { - getById("container-3").className = "column columnfade hidden"; // Hide screen share - getById("head1").className = "hidden"; - } else { - getById("container-3").className = "column columnfade hidden"; // Hide webcam - getById("container-2").classList.add("skip-animation"); - getById("container-2").classList.remove("pointer"); - } - } - - if (urlParams.has("hands") || urlParams.has("hand")) { - session.raisehands = urlParams.get("hands") || urlParams.get("hand") || 1; - session.raisehands = parseInt(session.raisehands); - } - - if (urlParams.has("portrait") || urlParams.has("916") || urlParams.has("vertical")) { - // playback aspect ratio - session.aspectRatio = 1; // 9:16 (default of 0 is 16:9) - } else if (urlParams.has("square") || urlParams.has("11")) { - session.aspectRatio = 2; //1:1 ? - } else if (urlParams.has("43")) { - session.aspectRatio = 3; //1:1 ? - } - - if (urlParams.has("structure")) { - session.structure = true; - } - - if (urlParams.has("aspectratio") || urlParams.has("ar")) { - // capture aspect ratio - session.forceAspectRatio = urlParams.get("aspectratio") || urlParams.get("ar") || false; - if (session.forceAspectRatio) { - if (session.forceAspectRatio == "portrait" || session.forceAspectRatio == "vertical") { - session.forceAspectRatio = 9.0 / 16.0; - } else if (session.forceAspectRatio == "landscape") { - session.forceAspectRatio = 16.0 / 9.0; - } else if (session.forceAspectRatio == "square") { - session.forceAspectRatio = 1.0; - } else { - session.forceAspectRatio = parseFloat(session.forceAspectRatio) || false; - } - } - } - if (urlParams.has("screenshareaspectratio") || urlParams.has("ssar")) { - // capture aspect ratio - session.forceScreenShareAspectRatio = urlParams.get("screenshareaspectratio") || urlParams.get("ssar") || 16.0 / 9.0; - if (session.forceScreenShareAspectRatio) { - if (session.forceScreenShareAspectRatio == "portrait" || session.forceScreenShareAspectRatio == "vertical") { - session.forceScreenShareAspectRatio = 9.0 / 16.0; - } else if (session.forceScreenShareAspectRatio == "landscape") { - session.forceScreenShareAspectRatio = 16.0 / 9.0; - } else if (session.forceScreenShareAspectRatio == "square") { - session.forceScreenShareAspectRatio = 1.0; - } else { - session.forceScreenShareAspectRatio = parseFloat(session.forceScreenShareAspectRatio) || false; - } - } - } - - if (urlParams.has("crop")) { - var crop = parseFloat(urlParams.get("crop")) || 0; - if (crop > 0) { - session.forceAspectRatio = 1.7777777778 * (crop / 100); - } else if (crop < 0) { - session.forceAspectRatio = 1.7777777778 / (crop / 100); - } else { - session.forceAspectRatio = 1.3333333333; - } - } - - if (urlParams.has("cover")) { - session.cover = urlParams.get("cover") || true; - document.documentElement.style.setProperty("--fit-style", "cover"); - document.documentElement.style.setProperty("--myvideo-max-width", "100vw"); - document.documentElement.style.setProperty("--myvideo-width", "100vw"); - document.documentElement.style.setProperty("--myvideo-height", "100vh"); - } else if (urlParams.has("fit")) { - // not fully implemented yet. - session.cover = true; - document.documentElement.style.setProperty("--fit-style", "fit"); - document.documentElement.style.setProperty("--myvideo-max-width", "100vw"); - document.documentElement.style.setProperty("--myvideo-width", "100vw"); - document.documentElement.style.setProperty("--myvideo-height", "100vh"); - } - - if (urlParams.has("record")) { - if (!session.cleanOutput) { - if (SafariVersion && !MediaRecorder) { - if (macOS) { - warnUser("Your browser may not support local media recording.\n\nTry Chrome instead if on macOS."); - } else { - warnUser("Your browser or device may not support local media recording.\n\nSafari sometimes allows the feature to be enabled via its experimental settings."); - } - } else if (SafariVersion) { - if (macOS) { - warnUser("It is recommended to use Chrome instead of Safari if doing local media recordings."); - } else if (SafariVersion <= 15) { - warnUser("Please update your device.\n\nOlder versions of Safari may crash after recording for a few minutes."); - } else if (iOS || iPad) { - // iOS-specific warning about split recordings - warnUser("iOS Recording Notice:\n\n• Recordings will be split into 5-minute segments to prevent crashes\n• Files will download as MP4 format\n• Each segment will download separately\n• Use video editing software to join segments if needed\n\nGoogle Drive uploads (if enabled) will work normally."); - } else { - warnUser("Local media recordings are an experimental feature on Apple devices.\n\nPlease at least test it out a few times first."); - } - } - } - session.recordLocal = urlParams.get("record"); - - if (session.recordLocal === "false" || session.recordLocal === "off") { - session.record = false; - session.recordLocal = false; - } else if (session.recordLocal != parseInt(session.recordLocal)) { - session.recordLocal = session.recordDefault; - } else if (session.recordLocal !== null){ - session.recordLocal = parseInt(session.recordLocal); - } else { - session.recordLocal = session.recordDefault; - } - } - - if (session.record === false) { - getById("recordLocalbutton").classList.add("hidden"); - getById("recordLocalScreenbutton").classList.add("hidden"); - try { - document.querySelectorAll('[data-action-type^="record"]').forEach(ele => { - ele.remove(); - delete ele; - }); - document.querySelectorAll('[data-action="Record"]').forEach(ele => { - ele.parentNode.remove(); - delete ele.parentNode; - }); - } catch (e) { - errorlog(e); - } - } - - if (urlParams.has("autorecord")) { - session.autorecord = true; - if (session.recordLocal === false) { - let bitautorec = urlParams.get("autorecord"); - if (bitautorec !== "") { - session.recordLocal = parseInt(bitautorec); - } else { - session.recordLocal = session.recordDefault; - } - } - } - if (urlParams.has("autorecordlocal")) { - session.autorecordlocal = true; - if (session.recordLocal === false) { - session.recordLocal = urlParams.get("autorecordlocal"); - if (session.recordLocal != parseInt(session.recordLocal)) { - session.recordLocal = session.recordDefault; - } else if (session.recordLocal !== ""){ - session.recordLocal = parseInt(session.recordLocal); - } else { - session.recordLocal = session.recordDefault; - } - } - } - if (urlParams.has("autorecordremote")) { - session.autorecordremote = true; - if (session.recordLocal === false) { - session.recordLocal = urlParams.get("autorecordremote"); - if (session.recordLocal != parseInt(session.recordLocal)) { - session.recordLocal = session.recordDefault; - } else if (session.recordLocal !== ""){ - session.recordLocal = parseInt(session.recordLocal); - } else { - session.recordLocal = session.recordDefault; - } - } - } - if (urlParams.has("splitrecording")) { - // minutes - session.recordingInterval = urlParams.get("splitrecording") || 5; // 5 minutes - session.recordingInterval = parseInt(session.recordingInterval) || 1; - // For Mac: https://gist.github.com/steveseguin/8083172a20ad7c9ebcb449e22fc8fe67 - // For Windows: https://gist.github.com/steveseguin/7ca1df1df9ec6042f27ecc8d258e3f30 - } else if ((SafariVersion || iOS || iPad) && (urlParams.has("record") || urlParams.has("autorecord"))) { - // Auto-enable split recording for Safari to prevent memory issues - // iOS/iPad: 5 minutes (tighter memory), desktop Safari: 10 minutes - session.recordingInterval = (iOS || iPad) ? 5 : 10; - if (!session.cleanOutput) { - console.log("Safari detected with recording enabled: Auto-enabling split recording (" + session.recordingInterval + "-minute segments) to prevent memory issues"); - } - } - if (urlParams.has("pcm")) { - session.pcm = true; - } - - if (urlParams.has("recordcodec") || urlParams.has("rc")) { - session.recordingVideoCodec = urlParams.get("recordcodec") || urlParams.get("rc") || false; - } else if (session.recordingVideoCodec===false){ - session.recordingVideoCodec = "vp8"; - } - - if (urlParams.has("recordfolder")) { - session.GDRIVE_FOLDERNAME = urlParams.get("recordfolder") || ""; - } - - if (urlParams.has("menuoffset")) { - getById("subControlButtons").style.bottom = urlParams.get("menuoffset") || "50px"; - getById("controlPositioning").style.bottom = urlParams.get("menuoffset") || "50px"; - getById("subControlButtons").style.setProperty("position", "absolute", "important"); - } - - - if (urlParams.has("bigbutton")) { - session.bigmutebutton = true; - getById("mutebutton").classList.add("bigbutton"); - if (urlParams.get("bigbutton")) { - let bigbuttontext = document.createElement("span"); - bigbuttontext.innerText = urlParams.get("bigbutton"); - bigbuttontext.className = "bigbuttontext"; - getById("mutebutton").appendChild(bigbuttontext); - } - } - - if (urlParams.get("rows")) { - session.rows = urlParams.get("rows"); - session.rows = session.rows.split(","); - } - - if (urlParams.has("nosettings")) { - session.nosettings = true; - getById("settingsbutton").classList.add("hidden"); - } - - if (urlParams.has("publish")) { - session.publish = true; - getById("publishSettings").style.display = "block"; - - if (session.recordLocal !== false){ - getById("startRecordingButton").classList.remove("hidden"); - //session.autorecord = true; - //getById("startPublishingButton").classList.add("hidden"); - } - } - - if (urlParams.has("obscontrols") || urlParams.has("remoteobs") || urlParams.has("obsremote") || urlParams.has("obs") || urlParams.has("controlobs")) { - session.obsControls = urlParams.get("obscontrols") || urlParams.get("remoteobs") || urlParams.get("obsremote") || urlParams.get("obs") || urlParams.get("controlobs"); - if (session.obsControls) { - // whether to show the button or not; that's it. - session.obsControls = session.obsControls.toLowerCase(); - } - if (session.obsControls == "false") { - session.obsControls = false; - } else if (session.obsControls == "0") { - session.obsControls = false; - } else if (session.obsControls == "no") { - session.obsControls = false; - } else if (session.obsControls == "off") { - session.obsControls = false; - } else if (session.obsControls == "full") { - try { - session.dataMode = true; - session.obsControls = true; - //session.doNotSeed = true; - getById("main").classList.add("fullscreenOBSControl"); - toggleOBSControls(); - } catch(e){errorlog(e);} - } else if (session.obsControls) { - session.obsControls = session.obsControls.toLowerCase(); - } else { - session.obsControls = true; - } - } - - if (urlParams.has("nopush") || urlParams.has("noseed") || urlParams.has("viewonly") || urlParams.has("viewmode")) { - // this is like a scene; Seeding is disabled. Can be used with &showall to show all videos on load - session.doNotSeed = true; - - if (session.scene === false) { - session.scene = null; // not a scene, but sorta. false vs null makes a difference here. - } - - session.dataMode = true; // thios will let us connect - // session.showall = true; // this can be used to SHOW the videos. (&showall) - } - - if (urlParams.has("scene") || urlParams.has("scn")) { - session.scene = urlParams.get("scene") || urlParams.get("scn") || 0; - if (typeof session.scene === "string") { - session.scene = session.scene.replace(/[\W]+/g, "_"); - } else { - session.scene = (parseInt(session.scene) || 0) + ""; - } - } - - if (urlParams.has("morescenes")) { - let moreScenes = urlParams.get("morescenes") || 16; - moreScenes = parseInt(moreScenes) || 0; - if (moreScenes < 8) { - moreScenes = 8; - } - session.maxScene = moreScenes; - let sceneButtonMain = document.querySelector("#controls_blank .sceneButtons button"); - if (sceneButtonMain && moreScenes) { - var i = 8; - while (i < moreScenes) { - i++; - let sceneButton = sceneButtonMain.cloneNode(true); - sceneButton.dataset.scene = i; - sceneButton.title = "Add to Scene " + i; - sceneButton.innerHTML = "S" + i + ""; - document.querySelector("#controls_blank .sceneButtons").appendChild(sceneButton); - } - } - } - - // lets a guest join a scene on their own ... but - // doesn't work if the director or scene isn't already loaded. - // requires &openscene on the director to be added - if (urlParams.has("joinscene") || urlParams.has("joinscenes")) { // list of scenes to auto join. - let sceneValues = urlParams.get("joinscene") || urlParams.get("joinscenes"); - if (sceneValues){ - sceneValues = sceneValues.split(","); - session.requestscenes = sceneValues.map(scene => { - scene = scene.trim(); - if (typeof scene === "string") { - return scene.replace(/[\W]+/g, "_"); - } else { - return (parseInt(scene) || 0) + ""; - } - }); - } - } // .. but requires openscene to be set on the target scene. - if (urlParams.has("openscene") || urlParams.has("openscenes")) { - session.openscene = true; - } - - if (urlParams.has("solo")) { - if (session.scene === false) { - session.scene = "0"; - } - session.solo = true; - } - - if (session.scene !== false) { - session.disableWebAudio = true; - if (session.audioEffects === null) { - session.audioEffects = false; - } - session.audioMeterGuest = false; - } - - if (session.recordWindow && session.scene !== false) { - // Add floating record button for scene window recording - var recordBtn = document.createElement("button"); - recordBtn.id = "recordWindowButton"; - recordBtn.innerHTML = "● Start Recording"; - recordBtn.title = "Record this scene to a local video file"; - recordBtn.style.cssText = "position:fixed;top:10px;right:10px;z-index:99999;padding:12px 20px;font-size:16px;line-height:1.2;height:46px;box-sizing:border-box;background:#d00;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:bold;box-shadow:0 2px 10px rgba(0,0,0,0.3);transition:opacity 0.3s;"; - recordBtn.onclick = async function() { - if (this.dataset.recording === "1") { - // Stop recording - if (session.recordWindowElement && session.recordWindowElement.recording) { - recordLocalVideo("stop", false, session.recordWindowElement); - } - this.innerHTML = "● Start Recording"; - this.title = "Record this scene to a local video file"; - this.style.background = "#d00"; - this.style.opacity = "1"; - this.dataset.recording = "0"; - } else { - // Start recording - this.innerHTML = "■ Stop"; - this.title = "Stop recording"; - this.style.background = "#090"; - this.style.opacity = "0.3"; // fade out during recording so it's less visible - this.dataset.recording = "1"; - var bitrate = (typeof session.recordWindow === "number") ? session.recordWindow : 6000; - await recordWindowCapture(bitrate); - } - }; - // Hover to show button clearly during recording - recordBtn.onmouseenter = function() { this.style.opacity = "1"; }; - recordBtn.onmouseleave = function() { if (this.dataset.recording === "1") this.style.opacity = "0.3"; }; - document.body.appendChild(recordBtn); - - // Add Go Live button (experimental - WHIP publish to Twitch) - var liveBtn = document.createElement("button"); - liveBtn.id = "goLiveButton"; - liveBtn.innerHTML = "📡 Go Live"; - liveBtn.title = "Stream to Twitch via WHIP (requires stream key)"; - liveBtn.style.cssText = "position:fixed;top:10px;right:200px;z-index:99999;padding:12px 20px;font-size:16px;line-height:1.2;height:46px;box-sizing:border-box;background:#6441a5;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:bold;box-shadow:0 2px 10px rgba(0,0,0,0.3);"; - liveBtn.onclick = async function() { - if (this.dataset.live === "1") { - // Stop streaming - if (session.goLivePC) { - try { - session.goLivePC.close(); - } catch(e) {} - session.goLivePC = null; - } - this.innerHTML = "📡 Go Live"; - this.title = "Stream to Twitch via WHIP (requires stream key)"; - this.style.background = "#6441a5"; - this.dataset.live = "0"; - return; - } - - // Need a stream to publish - if (!session.recordWindowElement || !session.recordWindowElement.srcObject) { - alert("Please start recording first to capture the scene, then click Go Live."); - return; - } - - var streamKey = prompt("Enter your Twitch Stream Key:"); - if (!streamKey || !streamKey.trim()) { - return; - } - streamKey = streamKey.trim(); - - try { - // Create RTCPeerConnection for WHIP - var config = session.configuration || { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; - var pc = new RTCPeerConnection(config); - session.goLivePC = pc; - - // Add tracks from the captured stream - var stream = session.recordWindowElement.srcObject; - stream.getTracks().forEach(function(track) { - pc.addTransceiver(track, { direction: "sendonly", streams: [stream] }); - }); - - // Create and send offer - var offer = await pc.createOffer(); - await pc.setLocalDescription(offer); - - // Wait for ICE gathering - await new Promise(function(resolve) { - if (pc.iceGatheringState === "complete") { - resolve(); - } else { - pc.onicegatheringstatechange = function() { - if (pc.iceGatheringState === "complete") resolve(); - }; - setTimeout(resolve, 2000); // Fallback timeout - } - }); - - // Send to Twitch WHIP endpoint - var whipUrl = "https://g.webrtc.live-video.net:4443/v2/offer"; - var response = await fetch(whipUrl, { - method: "POST", - headers: { - "Content-Type": "application/sdp", - "Authorization": "Bearer " + streamKey - }, - body: pc.localDescription.sdp - }); - - if (!response.ok) { - throw new Error("WHIP request failed: " + response.status); - } - - var answerSdp = await response.text(); - await pc.setRemoteDescription({ type: "answer", sdp: answerSdp }); - - // Success - update button - this.innerHTML = "🔴 Stop Live"; - this.title = "Stop streaming to Twitch"; - this.style.background = "#d00"; - this.dataset.live = "1"; - - // Handle connection close - pc.onconnectionstatechange = function() { - if (pc.connectionState === "failed" || pc.connectionState === "disconnected") { - liveBtn.innerHTML = "📡 Go Live"; - liveBtn.title = "Stream to Twitch via WHIP (requires stream key)"; - liveBtn.style.background = "#6441a5"; - liveBtn.dataset.live = "0"; - session.goLivePC = null; - } - }; - - } catch(e) { - console.error("Go Live failed:", e); - alert("Failed to go live: " + e.message); - if (session.goLivePC) { - session.goLivePC.close(); - session.goLivePC = null; - } - this.innerHTML = "📡 Go Live"; - this.title = "Stream to Twitch via WHIP (requires stream key)"; - this.style.background = "#6441a5"; - this.dataset.live = "0"; - } - }; - document.body.appendChild(liveBtn); - } - - if (urlParams.has("fakeuser")) { - log("ICE FILTER ENABLED"); - session.fakeUser = true; - session.dataMode = true; - session.autostart = true; - session.novideo = []; - session.noaudio = []; - session.noiframe = []; - session.cleanOutput = true; - } - - if (urlParams.has("retransmit")) { - session.retransmit = true; - session.dataMode = true; - } - - if (urlParams.has("datamode") || urlParams.has("dataonly")) { - // this disables all media in/out. - session.dataMode = true; - } - - if (urlParams.has("pseudoguest") || urlParams.has("pseudoscene")) { - session.videoDevice = 0; - session.audioDevice = 0; - getById("mainmenu").classList.add("hidden"); - getById("header").classList.add("hidden"); - getById("mainmenu").style.display = "none"; - getById("header").style.display = "none"; - session.showList = false; - session.autostart = true; - getById("controlButtons").classList.add("hidden"); - getById("controlButtons").style.display = "none"; - getById("miniTaskBar").classList.add("hidden"); - getById("miniTaskBar").style.display = "none"; - session.dedicatedControlBarSpace = false; - session.pseudoguest = true; - } - - if (session.dataMode) { - if (!(session.meshcast || session.whipOutput !== false || session.screenshare)) { - session.videoDevice = 0; - session.audioDevice = 0; - } - - getById("mainmenu").classList.add("hidden"); - //session.autohide = true; - //session.autostart = true; - //session.novideo = []; - //session.noaudio = []; - //session.noiframe = []; - //session.webcamonly = true; - } - - if (urlParams.has("autoadd")) { - // the streams we want to view; if set, but let blank, we will request no streams to watch. - session.autoadd = urlParams.get("autoadd") || null; // this value can be comma seperated for multiple streams to pull - - if (session.autoadd == null) { - session.autoadd = false; - } - if (session.autoadd) { - session.autoadd = session.autoadd.split(","); - } - } - - if (session.director && urlParams.has("autochannels") || urlParams.has("autochannel")) { - // Director-only: auto-assign guests to audio channels - var val = urlParams.get("autochannels") || urlParams.get("autochannel");; - if (val) { - // Parse comma-separated channel numbers, filter valid 1-8 - session.autochannels = val.split(",") - .map(function(n) { return parseInt(n.trim()); }) - .filter(function(n) { return n >= 1 && n <= 8; }); - if (session.autochannels.length === 0) { - session.autochannels = false; - } - } else { - // &autochannels with no value = default allowed list (skip C4/LFE) - session.autochannels = [1, 2, 3, 5, 6, 7, 8]; - } - } - - if (session.director && urlParams.has("autochannelmode")) { - var mode = urlParams.get("autochannelmode"); - if (mode === "roundrobin" || mode === "rr") { - session.autochannelmode = "roundrobin"; - } else { - session.autochannelmode = "leastused"; - } - } - - if (urlParams.has("preferchannel") || urlParams.has("pc")) { - // Guest's preferred audio channel for auto-assignment - var ch = parseInt(urlParams.get("preferchannel") || urlParams.get("pc")); - if (ch >= 1 && ch <= 8) { - session.preferChannel = ch; - } - } - - //if (session.scene!=="1"){ // scene =0 and 1 should load instantly. - // session.hiddenSceneViewBitrate = 0; // By default this is ~ 400kbps, but if you have 10 scenes, i don't want to kill things. - //} - - if (urlParams.has("hiddenscenebitrate")) { - session.hiddenSceneViewBitrate = parseInt(urlParams.get("hiddenscenebitrate")) || 0; - } else if (urlParams.has("layout") && session.scene !== false && !session.viewslot) { - session.hiddenSceneViewBitrate = false; - } else if (urlParams.has("nohiddensceneoptimization")) { - session.hiddenSceneViewBitrate = false; - } - - if (urlParams.has("preloadbitrate")) { - session.preloadbitrate = parseInt(urlParams.get("preloadbitrate")) || 0; // 1000 - } - - if (urlParams.has("rampuptime")) { - session.rampUpTime = parseInt(urlParams.get("rampuptime")) || 10000; - } - - if (urlParams.has("scenetype") || urlParams.has("type")) { - session.sceneType = parseInt(urlParams.get("scenetype")) || parseInt(urlParams.get("type")) || false; - } - - if (urlParams.has("mediasettings")) { - session.forceMediaSettings = true; - } - - if (urlParams.has("transcript") || urlParams.has("transcribe") || urlParams.has("trans")) { - session.transcript = urlParams.get("transcript") || urlParams.get("transcribe") || urlParams.get("trans") || "en-US"; - } - - if (urlParams.has("cc") || urlParams.has("closedcaptions") || urlParams.has("captions")) { - session.closedCaptions = true; - } - if (urlParams.has("nocclabels") || urlParams.has("nocclabel") || urlParams.has("nocaptionlabels") || urlParams.has("nocaptionlabel")) { - session.nocaptionlabels = true; - } - if (urlParams.has("cccolored") || urlParams.has("cccoloured") || urlParams.has("coloredcc") || urlParams.has("colorcc") || urlParams.has("cccolor")) { - session.ccColored = true; - } - - - if (urlParams.has("base64css") || urlParams.has("b64css") || urlParams.has("cssbase64") || urlParams.has("cssb64")) { - try { - var base64Css = urlParams.get("base64css") || urlParams.get("b64css") || urlParams.get("cssbase64") || urlParams.get("cssb64"); - try { - base64Css = atob(base64Css); // window.btoa(encodeURIComponent("#mainmenu{background-color: pink;}" )); - } catch (e) {} - try { - base64Css = decodeURIComponent(base64Css); // window.btoa(encodeURIComponent("#mainmenu{background-color: pink; ❤" )); - } catch (e) {} - - try { - if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ - session.iFramesAllowed = false; - console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); - } else if ((window !== window.top) || session.studioSoftware) { - // allowed - } else { - console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); - session.iFramesAllowed = false; - } - } catch(e){ - warnlog(e); - } - - var cssStyleSheet = document.createElement("style"); - cssStyleSheet.innerText = base64Css; - document.querySelector("head").appendChild(cssStyleSheet); - } catch (e) { - console.error(e); - } - } - - if (urlParams.has("css")) { - var cssURL = urlParams.get("css"); - try { - cssURL = decodeURI(cssURL); - } catch(e){ - warnlog(e); - } - log(cssURL); - - let validURL = false; - try { - cssUrlObj = new URL(cssURL); - validURL = true; - } catch(e){ - } - try { - if (validURL){ - const cssDomain = cssUrlObj.hostname; - - try { - if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ - session.iFramesAllowed = false; - console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); - } else if ((window.location.hostname === cssDomain) || window.location.hostname.endsWith("."+cssDomain) || (window !== window.top) || session.studioSoftware) { - if (window.location.hostname !== cssDomain){ - console.warn("Third-party CSS has been injected into the site. Security cannot be ensured."); - } - } else { - console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); - session.iFramesAllowed = false; - } - } catch(e){ - warnlog(e); - } - - var cssStylesheet = document.createElement("link"); - cssStylesheet.rel = "stylesheet"; - cssStylesheet.type = "text/css"; - cssStylesheet.media = "screen"; - cssStylesheet.href = cssURL; - document.getElementsByTagName("head")[0].appendChild(cssStylesheet); - - cssStylesheet.onload = function () { - getById("main").classList.remove("hidden"); - log("loaded remote style sheet"); - }; - - cssStylesheet.onerror = function () { - getById("main").classList.remove("hidden"); - errorlog("REMOTE STYLE SHEET HAD ERROR"); - }; - } else { - try { - if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ - console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); - session.iFramesAllowed = false; - } else if ((window !== window.top) || session.studioSoftware) { - // allowed - } else { - console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); - session.iFramesAllowed = false; - } - } catch(e){ - warnlog(e); - } - var cssStylesheet = document.createElement("style"); - cssStylesheet.innerHTML = cssURL; - document.getElementsByTagName("head")[0].appendChild(cssStylesheet); - getById("main").classList.remove("hidden"); - } - - } catch(e){ - warnlog(e); - } - } else { - getById("main").classList.remove("hidden"); - } - - if (urlParams.has("avatar")) { - var avatar = urlParams.get("avatar") || false; - if (avatar && avatar == "default") { - session.avatar = document.getElementById("defaultAvatar2"); - document.body.appendChild(session.avatar); - session.avatar.ready = false; - session.avatar.onload = () => { - session.avatar.ready = true; - getById("noAvatarSelected3").classList.remove("selected"); - getById("noAvatarSelected").classList.remove("selected"); - getById("defaultAvatar1").classList.add("selected"); - getById("defaultAvatar2").classList.add("selected"); - updateRenderOutpipe(); - session.avatar.classList.add("hidden"); - }; - session.avatar.onerror = () => { - session.avatar.classList.add("hidden"); - } - if (session.avatar.complete) { - session.avatar.ready = true; - getById("noAvatarSelected3").classList.remove("selected"); - getById("noAvatarSelected").classList.remove("selected"); - getById("defaultAvatar1").classList.add("selected"); - getById("defaultAvatar2").classList.add("selected"); - } - } else if (avatar) { - try { - avatar = decodeURIComponent(avatar); - } catch (e) {} - - session.avatar = getById("defaultAvatar2"); - document.body.appendChild(session.avatar); - session.avatar.ready = false; - session.avatar.onload = () => { - session.avatar.ready = true; - getById("noAvatarSelected3").classList.remove("selected"); - getById("noAvatarSelected").classList.remove("selected"); - getById("defaultAvatar1").classList.add("selected"); - getById("defaultAvatar2").classList.add("selected"); - updateRenderOutpipe(); - session.avatar.classList.add("hidden"); - }; - session.avatar.onerror = () => { - session.avatar.classList.add("hidden"); - } - getById("defaultAvatar1").src = avatar; - getById("defaultAvatar2").src = avatar; - } else { - getById("avatarDiv3").classList.remove("hidden"); - getById("avatarDiv").classList.remove("hidden"); - } - if (session.disableBackground!==false){ - session.disableBackground = true; - } - } - - if (session.disableBackground){ - document.documentElement.style.setProperty("--video-background-image", "unset"); - } - - if (urlParams.has("prompt") || urlParams.has("validate") || urlParams.has("approve")) { - session.promptAccess = true; - } - - if (urlParams.has("js")) { - // ie: &js=https%3A%2F%2Fvdo.ninja%2Fexamples%2Ftestjs.js - try { - var jsURL = urlParams.get("js"); - try { - jsURL = decodeURI(jsURL); - } catch(e){ - warnlog(e); - } - log(jsURL); - - const jsUrlObj = new URL(jsURL); - const jsDomain = jsUrlObj.hostname; - let allow = false; - try { - if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ - console.error("For security and privacy purposes, Javascript injection using Invite Cam must be consented to."); - if (!session.cleanOutput){ - allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); - } - } else if ((window.location.hostname === jsDomain) || window.location.hostname.endsWith("."+jsDomain) || (window !== window.top) || session.studioSoftware) { - // same domains, iframes, or OBS can run javascript. - allow = true; - if (window.location.hostname !== jsDomain){ - console.warn("Third-party Javascript has been injected into the code. Security cannot be ensured."); - } - } else if (!session.cleanOutput){ - // to allow flexibility, we will allow it if the user consents - allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click cancel.", true); - } - } catch(e){ - allow = true; - warnlog(e); - } - - if (allow){ - // type="text/javascript" crossorigin="anonymous" - let externalJavaascript = document.createElement("script"); - externalJavaascript.type = "text/javascript"; - externalJavaascript.crossorigin = "anonymous"; - externalJavaascript.src = jsURL; - externalJavaascript.onerror = function () { - warnlog("Third-party Javascript failed to load"); - }; - externalJavaascript.onload = function () { - log("Third-party Javascript loaded"); - }; - document.head.appendChild(externalJavaascript); - } else { - console.error("For security/privacy purposes, Javascript injection is now only allowed if used within an IFRAME or if the JS file is hosted on the same domain."); - } - } catch(e){ - errorlog(e); - } - } - - if (urlParams.has("base64js") || urlParams.has("b64js") || urlParams.has("jsbase64") || urlParams.has("jsb64")) { - try { - let allow = false; - try { - if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ - console.error("For security and privacy purposes, Javascript injection using Invite Cam must be consented to."); - if (!session.cleanOutput){ - allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); - } - } else if ((window !== window.top) || session.studioSoftware) { - // iframes or OBS can run javascript. - allow = true; - console.warn("Third-party Javascript has been injected into the code. Security cannot be ensured."); - } else if (!session.cleanOutput){ - // to allow flexibility, we will allow it if the user consents - allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); - } - } catch(e){ - warnlog(e); - allow = true; - } - - if (allow){ - var base64js = urlParams.get("base64js") || urlParams.get("b64js") || urlParams.get("jsbase64") || urlParams.get("jsb64"); - base64js = decodeURIComponent(atob(base64js)); // window.btoa(encodeURIComponent("alert('hi')")); // ?jsb64=YWxlcnQoJ2hpJyk7 - var externalJavaascript = document.createElement("script"); - externalJavaascript.type = "text/javascript"; - externalJavaascript.crossorigin = "anonymous"; - externalJavaascript.innerHTML = base64js; - externalJavaascript.onerror = function () { - errorlog("Third-party Javascript failed to load"); - }; - externalJavaascript.onload = function () { - log("Third-party Javascript loaded"); - }; - document.head.appendChild(externalJavaascript); - } else { - console.error("For security/privacy purposes, Javascript B64 injection is now only allowed if used within an IFRAME."); - } - } catch (e) { - console.error(e); - } - } - - session.sitePassword = session.defaultPassword; - if (urlParams.has("password") || urlParams.has("pass") || urlParams.has("pw") || urlParams.has("p") || (session.password===null)) { - session.password = urlParams.get("password") || urlParams.get("pass") || urlParams.get("pw") || urlParams.get("p") || null; - - if (!session.password) { - window.focus(); - session.password = await promptAlt(getTranslation("enter-password"), true, true); - if (session.password) { - session.password = session.password.trim(); - } - } else if (session.password === "false") { - session.password = false; - session.defaultPassword = false; - } else if (session.password === "0") { - session.password = false; - session.defaultPassword = false; - } else if (session.password === "off") { - session.password = false; - session.defaultPassword = false; - } else { - try { - session.password = decodeURIComponent(session.password); // will be re-encoded in a moment. - } catch (e) { - errorlog(e); - } - } - } else if (urlParams.has("nopassword") || urlParams.has("nopass") || urlParams.has("nopw") || urlParams.has("p0")) { - session.password = false; - session.defaultPassword = false; - } - - if (session.password) { - getById("passwordRoom").value = session.password; - session.password = sanitizePassword(session.password); - session.defaultPassword = false; - getById("addPasswordBasic").style.display = "none"; - } - - if (urlParams.has("salt") && urlParams.get("salt")) { - session.salt = urlParams.get("salt"); - } - - if (urlParams.has("showconnections")) { - session.showConnections = true; // shows remote guest connections as a stat - } - - if (urlParams.has("hash") || urlParams.has("crc") || urlParams.has("check")) { - // could be brute forced in theory, so not as safe as just not using a hash check. - session.taintedSession = null; // waiting to see if valid or not. - var hash_input = urlParams.get("hash") || urlParams.get("crc") || urlParams.get("check"); - if (hash_input){ - hash_input = hash_input.trim(); // yes. really. - } - if (session.password === false) { - window.focus(); - session.password = await promptAlt(getTranslation("enter-password-2"), true, true); - session.password = sanitizePassword(session.password); - getById("passwordRoom").value = session.password; - session.defaultPassword = false; - } - - generateHash(session.password + session.salt, 6) - .then(function (hash) { - // million to one error. I won't - log("hash is " + hash); - if (hash.substring(0, hash_input.length) !== hash_input) { - // this hash crc check is usually just the first 4 characters, but i'll match based on whatever is provided; - // max 6 length for security. 2 could be a good option for better security but more than 6 is too big of a security concern. - generateHash(session.password + "obs.ninja", 6) - .then(function (hash2) { - // million to one error; this is to support a legacy salt used. Depreciated, and will be removed eventually - log("hash2 is " + hash2); - if (hash2.substring(0, 4) !== hash_input) { - // this legacy hash crc checks is always 4 characters - session.taintedSession = true; - if (!session.cleanOutput) { - miniTranslate(getById("request_info_prompt"), "password-incorrect"); - //getById("request_info_prompt").innerHTML = getTranslation("password-incorrect"); - getById("request_info_prompt").style.display = "block"; - getById("mainmenu").style.display = "none"; - getById("head1").style.display = "none"; - session.cleanOutput = true; - } else { - getById("request_info_prompt").innerHTML = ""; - getById("request_info_prompt").style.display = "block"; - getById("mainmenu").style.display = "none"; - getById("head1").style.display = "none"; - } - } else { - session.taintedSession = false; - session.hash = hash; - } - }) - .catch(errorlog); - } else { - session.taintedSession = false; - session.hash = hash; - } - }) - .catch(errorlog); - } - - if (session.defaultPassword !== false) { - session.password = session.defaultPassword; // no user entered password; let's use the default password if its not disabled. - } - - if (urlParams.has("showlabels") || urlParams.has("showlabel") || urlParams.has("sl")) { - session.showlabels = urlParams.get("showlabels") || urlParams.get("showlabel") || urlParams.get("sl") || ""; - session.showlabels = sanitizeLabel(session.showlabels.replace(/[\W]+/g, "_").replace(/_+/g, "_")); - //session.style = 6; - - if (session.showlabels == "") { - session.labelstyle = false; - } else { - session.labelstyle = session.showlabels; - } - - session.showlabels = true; - session.manual = session.manual === null ? false : session.manual; - session.windowed = session.windowed === null ? false : session.windowed; - } - - if (urlParams.has("showmeta")){ - session.showmeta = true; - } - - if (urlParams.has("sizelabel") || urlParams.has("labelsize") || urlParams.has("fontsize")) { - session.labelsize = urlParams.get("sizelabel") || urlParams.get("labelsize") || urlParams.get("fontsize") || 100; - session.labelsize = parseInt(session.labelsize); - } - - if (urlParams.has("label") || urlParams.has("l")) { - session.label = urlParams.get("label") || urlParams.get("l") || null; - var updateURLAsNeed = true; - if (session.label == null || session.label.length == 0) { - window.focus(); - session.label = await promptAlt(getTranslation("enter-display-name"), true); - } else { - var updateURLAsNeed = false; - try { - session.label = decodeURIComponent(session.label); - } catch (e) { - errorlog(e); - } - session.label = session.label.replace(/_/g, " "); - } - if (session.label != null) { - session.label = sanitizeLabel(session.label); // alphanumeric was too strict. - document.title = session.label; // what the result is. - - if (updateURLAsNeed) { - var label = encodeURIComponent(session.label); - if (urlParams.has("l")) { - updateURL("l=" + label, true, false); - } else { - updateURL("label=" + label, true, false); - } - } - } - } else if (urlParams.has("defaultlabel") || urlParams.has("labelsuggestion") || urlParams.has("ls")) { - session.label = urlParams.get("defaultlabel") || urlParams.get("labelsuggestion") || urlParams.get("ls") || null; - var updateURLAsNeed = true; - window.focus(); - var label = await promptAlt(getTranslation("enter-display-name"), true); - if (label) { - session.label = sanitizeLabel(label); // alphanumeric was too strict. - } else { - session.label = sanitizeLabel(session.label); - updateURLAsNeed = false; - } - - document.title = session.label; // what the result is. - - if (updateURLAsNeed) { - var label = encodeURIComponent(session.label); - if (urlParams.has("l")) { - updateURL("l=" + label, true, false); - } else { - updateURL("label=" + label, true, false); - } - } - } - - if (session.label){ - pokeIframeAPI("this-label", session.label); - } - - if (urlParams.has("resources")) { - session.allowResources = urlParams.get("resources") ? urlParams.get("resources").split(",") : true; - } - - if (urlParams.has("meta")) { - session.meta = {}; - const metaFields = urlParams.get("meta").split(","); - - const metaTemplates = { - pronouns: { - id: "1", - label: "Preferred Pronouns", - type: "select", - options: [ - "she/her", - "he/him", - "they/them", - "ze/zir", - "she/they", - "he/they", - "[Custom]" - ], - placeholder: "Select or enter custom pronouns" - }, - title: { - id: "2", - label: "Title/Role", - type: "text", - placeholder: "Indie Developer" - }, - twitter: { - id: "3", - label: "X.com username", - type: "url", - placeholder: "https://x.com/username" - }, - instagram: { - id: "4", - label: "Instagram", - type: "url", - placeholder: "@username" - }, - youtube: { - id: "5", - label: "YouTube Channel", - type: "url", - placeholder: "https://youtube.com/c/channel" - }, - twitch: { - id: "5", - label: "Twitch Channel", - type: "url", - placeholder: "https://www.twitch.tv/username" - }, - bio: { - id: "6", - label: "Short Bio", - type: "textarea", - placeholder: "Tell us about yourself" - }, - location: { - id: "7", - label: "Location", - type: "text", - placeholder: "Toronto, Canada" - }, - avatar: { - id: "8", - label: "Profile Avatar", - type: "file", - accept: "image/jpeg, image/jpg, image/png, image/webp", - placeholder: "Upload an avatar image" - }, - qr: { - id: "9", - label: "QR Code", - type: "file", - accept: "image/jpeg, image/jpg, image/png, image/webp", - placeholder: "Upload a QR Code" - }, - }; - - // Gather meta field data after display name - for (const fieldId of metaFields) { - const [templateName, field] = Object.entries(metaTemplates) - .find(([_, t]) => t.id === fieldId) || []; - - if (field) { - field.templateName = templateName; - const value = await promptAlt(field.label, true, false, false, false, false, false, field); - if (value) { - session.meta[templateName] = { // Use templateName (qr) instead of field.id (9) - type: field.type, - label: field.label, - templateName, - value, - id: field.id - }; - } - } - } - } - - if (urlParams.has("transparent") || urlParams.has("transparency")) { - // sets the window to be transparent - useful for IFRAMES? - session.transparent = true; - } - - if (urlParams.has("slider") || urlParams.has("showslider")) { - session.showSlider = true; - } - - if (session.transparent) { - getById("main").style.backgroundColor = "rgba(0,0,0,0)"; - document.documentElement.style.setProperty("--container-color", "#0000"); - document.documentElement.style.setProperty("--background-color", "#0000"); - document.documentElement.style.setProperty("--regular-margin", "0"); - document.documentElement.style.setProperty("--director-margin", "0 25px 0 0"); - document.documentElement.style.setProperty("--discord-grey-1a", "#0000"); - getById("directorLinksButton").style.color = "black"; - getById("main").style.overflow = "hidden"; - } - - if (urlParams.has("stereo") || urlParams.has("s") || urlParams.has("proaudio")) { - // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono - log("STEREO ENABLED"); - session.stereo = urlParams.get("stereo") || urlParams.get("s") || urlParams.get("proaudio"); - - if (session.stereo) { - session.stereo = session.stereo.toLowerCase(); - } - - //var supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); - //supportedConstraints.channelCount; - - if (session.stereo === "false") { - session.stereo = 0; - session.audioInputChannels = 1; - } else if (session.stereo === "0") { - session.stereo = 0; - session.audioInputChannels = 1; - } else if (session.stereo === "no") { - session.stereo = 0; - session.audioInputChannels = 1; - } else if (session.stereo === "off") { - session.stereo = 0; - session.audioInputChannels = 1; - } else if (session.stereo === "1") { - session.stereo = 1; - } else if (session.stereo === "both") { - session.stereo = 1; - } else if (session.stereo === "3") { - session.stereo = 3; - } else if (session.stereo === "out") { - session.stereo = 3; - } else if (session.stereo === "mono") { - session.stereo = 3; - session.audiobitrate = 128; - } else if (session.stereo === "4") { - session.stereo = 4; - } else if (session.stereo === "multi") { - session.stereo = 4; - } else if (session.stereo === "8") { - session.stereo = 8; - } else if (session.stereo === "surround") { - session.stereo = 8; - } else if (session.stereo === "2") { - session.stereo = 2; - } else if (session.stereo === "6") { - session.stereo = 6; - } else if (session.stereo === "in") { - session.stereo = 2; - } else { - session.stereo = 5; // guests; no stereo in, no high bitrate in, but otherwise like stereo=1 - } - - getById("whipoutstereo").classList.add("hidden"); - } - - if (urlParams.has("screensharestereo") || urlParams.has("sss") || urlParams.has("ssproaudio")) { - // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono - log("screenshare stereo ENABLED"); - session.screenshareStereo = urlParams.get("screensharestereo") || urlParams.get("sss") || urlParams.get("ssproaudio"); - - if (session.screenshareStereo) { - session.screenshareStereo = session.screenshareStereo.toLowerCase(); - } - - if (session.screenshareStereo === "false") { - session.screenshareStereo = 0; - } else if (session.screenshareStereo === "0") { - session.screenshareStereo = 0; - } else if (session.screenshareStereo === "no") { - session.screenshareStereo = 0; - } else if (session.screenshareStereo === "off") { - session.screenshareStereo = 0; - } else if (session.screenshareStereo === "1") { - session.screenshareStereo = 1; - } else if (session.screenshareStereo === "both") { - session.screenshareStereo = 1; - } else if (session.screenshareStereo === "3") { - session.screenshareStereo = 3; - } else if (session.screenshareStereo === "out") { - session.screenshareStereo = 3; - } else if (session.screenshareStereo === "mono") { - session.screenshareStereo = 3; - } else if (session.screenshareStereo === "4") { - session.screenshareStereo = 4; - } else if (session.screenshareStereo === "multi") { - session.screenshareStereo = 4; - } else if (session.screenshareStereo === "2") { - session.screenshareStereo = 2; - } else if (session.screenshareStereo === "in") { - session.screenshareStereo = 2; - } else { - session.screenshareStereo = 5; // guests; no stereo in, no high bitrate in, but otherwise like stereo=1 - } - } - - // Deploy your own handshake server for free; see: https://github.com/steveseguin/websocket_server - if (urlParams.has("pie")) { - // piesocket.com support is to be deprecated after dec/19/21, since piesocket is no longer a free service. - session.customWSS = urlParams.get("pie") || true; // If session.customWSS == true, then there is no need to set parameters via URL - session.wssSetViaUrl = true; - if (session.customWSS && session.customWSS !== true) { - session.wss = "wss://free3.piesocket.com/v3/1?api_key=" + session.customWSS; // if URL param is set, it will use the API key. - } - } - - if ((Firefox && !session.stereo) || session.stereo === 3) { - session.mono = true; // this will set the SDP to mono if firefox - } - - if (urlParams.has("mono")) { - session.mono = true; - if (session.stereo == 1 || session.stereo == 4) { - session.stereo = 3; - session.audiobitrate = 128; - } else if (session.stereo == 5) { - session.stereo = 3; // stereo out only - session.audiobitrate = 128; - } else if (session.stereo == 2) { - session.stereo = 0; - session.audiobitrate = 128; - } - } - - if (session.stereo == 1 || session.stereo == 3 || session.stereo == 4 || session.stereo == 5) { - session.echoCancellation = false; - session.autoGainControl = false; - session.noiseSuppression = false; - } - - if (urlParams.has("channelcount") || urlParams.has("ac") || urlParams.has("inputchannels")) { - // if updates to this, see also function toggleMonoStereoMic() - session.audioInputChannels = urlParams.get("channelcount") || urlParams.get("ac") || urlParams.get("inputchannels") || 0; - session.audioInputChannels = parseInt(session.audioInputChannels); - if (!session.audioInputChannels) { - session.audioInputChannels = false; - } - } else if (urlParams.has("monomic")) { - session.audioInputChannels = 1; - } - - if (session.stereo === 5 && !session.audioInputChannels) { - // allow the guest to set their mic to mono. - document.querySelectorAll(".gear_microphone").forEach(ele => { - ele.classList.remove("hidden"); - }); - } - - if (urlParams.has("echocancellation") || urlParams.has("aec") || urlParams.has("ec")) { - session.echoCancellation = urlParams.get("echocancellation") || urlParams.get("aec") || urlParams.get("ec"); - - if (session.echoCancellation) { - session.echoCancellation = session.echoCancellation.toLowerCase(); - } - if (session.echoCancellation == "false") { - session.echoCancellation = false; - } else if (session.echoCancellation == "0") { - session.echoCancellation = false; - } else if (session.echoCancellation == "no") { - session.echoCancellation = false; - } else if (session.echoCancellation == "off") { - session.echoCancellation = false; - } else { - session.echoCancellation = true; - } - } - - if (urlParams.has("autogain") || urlParams.has("ag") || urlParams.has("agc")) { - session.autoGainControl = urlParams.get("autogain") || urlParams.get("ag") || urlParams.get("agc"); - if (session.autoGainControl) { - session.autoGainControl = session.autoGainControl.toLowerCase(); - } - if (session.autoGainControl == "false") { - session.autoGainControl = false; - } else if (session.autoGainControl == "0") { - session.autoGainControl = false; - } else if (session.autoGainControl == "no") { - session.autoGainControl = false; - } else if (session.autoGainControl == "off") { - session.autoGainControl = false; - } else { - session.autoGainControl = true; - } - } - - if (urlParams.has("denoise") || urlParams.has("dn")) { - session.noiseSuppression = urlParams.get("denoise") || urlParams.get("dn"); - - if (session.noiseSuppression) { - session.noiseSuppression = session.noiseSuppression.toLowerCase(); - } - if (session.noiseSuppression == "false") { - session.noiseSuppression = false; - } else if (session.noiseSuppression == "0") { - session.noiseSuppression = false; - } else if (session.noiseSuppression == "no") { - session.noiseSuppression = false; - } else if (session.noiseSuppression == "off") { - session.noiseSuppression = false; - } else { - session.noiseSuppression = true; - } - } - - if (urlParams.has("isolation") || urlParams.has("voiceisolation") || urlParams.has("vi")) { - session.voiceIsolation = urlParams.get("isolation") || urlParams.get("voiceisolation") || urlParams.get("vi"); - - if (session.voiceIsolation) { - session.voiceIsolation = session.voiceIsolation.toLowerCase(); - } - if (session.voiceIsolation == "false") { - session.voiceIsolation = false; - } else if (session.voiceIsolation == "0") { - session.voiceIsolation = false; - } else if (session.voiceIsolation == "no") { - session.voiceIsolation = false; - } else if (session.voiceIsolation == "off") { - session.voiceIsolation = false; - } else { - session.voiceIsolation = true; - } - } - - if (session.voiceIsolation !== null) { - getById("whipoutvoiceisolation").classList.add("hidden"); - } - if (session.noiseSuppression !== null) { - getById("whipoutdenoise").classList.add("hidden"); - } - if (session.autoGainControl !== null) { - // should be the last - getById("whipoutautogain").classList.add("hidden"); - } - - if (urlParams.has("screenshareaec") || urlParams.has("ssec") || urlParams.has("ssaec")) { - session.screenshareAEC = urlParams.get("screenshareaec") || urlParams.get("ssec") || urlParams.get("ssaec"); - - if (session.screenshareAEC) { - session.screenshareAEC = session.screenshareAEC.toLowerCase(); - } - if (session.screenshareAEC == "false") { - session.screenshareAEC = false; - } else if (session.screenshareAEC == "0") { - session.screenshareAEC = false; - } else if (session.screenshareAEC == "no") { - session.screenshareAEC = false; - } else if (session.screenshareAEC == "off") { - session.screenshareAEC = false; - } else { - session.screenshareAEC = true; - } - } - if (urlParams.has("screenshareautogain") || urlParams.has("ssag") || urlParams.has("ssagc")) { - session.screenshareAutogain = urlParams.get("screenshareautogain") || urlParams.get("ssag") || urlParams.get("ssagc"); - if (session.screenshareAutogain) { - session.screenshareAutogain = session.screenshareAutogain.toLowerCase(); - } - if (session.screenshareAutogain == "false") { - session.screenshareAutogain = false; - } else if (session.screenshareAutogain == "0") { - session.screenshareAutogain = false; - } else if (session.screenshareAutogain == "no") { - session.screenshareAutogain = false; - } else if (session.screenshareAutogain == "off") { - session.screenshareAutogain = false; - } else { - session.screenshareAutogain = true; - } - } - if (urlParams.has("screensharedenoise") || urlParams.has("ssdn")) { - session.screenshareDenoise = urlParams.get("screensharedenoise") || urlParams.get("ssdn"); - - if (session.screenshareDenoise) { - session.screenshareDenoise = session.screenshareDenoise.toLowerCase(); - } - if (session.screenshareDenoise == "false") { - session.screenshareDenoise = false; - } else if (session.screenshareDenoise == "0") { - session.screenshareDenoise = false; - } else if (session.screenshareDenoise == "no") { - session.screenshareDenoise = false; - } else if (session.screenshareDenoise == "off") { - session.screenshareDenoise = false; - } else { - session.screenshareDenoise = true; - } - } - - if (urlParams.has("roombitrate") || urlParams.has("roomvideobitrate") || urlParams.has("rbr")) { - log("Room BITRATE SET"); - session.roombitrate = urlParams.get("roombitrate") || urlParams.get("rbr") || urlParams.get("roomvideobitrate"); - session.roombitrate = parseInt(session.roombitrate); - if (session.roombitrate < 1) { - session.roombitrate = 0; - } - } - - if (urlParams.has("outboundaudiobitrate") || urlParams.has("oab")) { - session.outboundAudioBitrate = parseInt(urlParams.get("outboundaudiobitrate")) || parseInt(urlParams.get("oab")) || false; - } - if (urlParams.has("outboundvideobitrate") || urlParams.has("outboundbitrate") || urlParams.has("ovb")) { - session.outboundVideoBitrate = parseInt(urlParams.get("outboundvideobitrate")) || parseInt(urlParams.get("outboundbitrate")) || parseInt(urlParams.get("ovb")) || false; - session.outboundVideoBitrate_userSet = true; - } else if (session.outboundVideoBitrate!==false){ - session.outboundVideoBitrate_userSet = true; - } - - if (urlParams.has("webp") || urlParams.has("images")) { - // deprecicating this. chunked mode will replace it. - session.webp = urlParams.get("webp") || urlParams.get("images") || "webp"; - } - - if (urlParams.has("webpquality") || urlParams.has("webpq") || urlParams.has("wq")) { - session.webPquality = parseInt(urlParams.get("webpquality")) || parseInt(urlParams.get("webpq")) || parseInt(urlParams.get("wq")) || 4; - } - - if (urlParams.has("audiobitrate") || urlParams.has("ab")) { - // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono - log("AUDIO BITRATE SET"); - session.audiobitrate = urlParams.get("audiobitrate") || urlParams.get("ab"); - session.audiobitrate = parseInt(session.audiobitrate); - if (session.audiobitrate < 1) { - session.audiobitrate = false; - } else if (session.audiobitrate > 510) { - session.audiobitrate = 510; - } // this is to just prevent abuse - } - if (iOS || iPad) { - session.audiobitrate = false; // iOS devices seem to get distortion with custom audio bitrates. Disable for now. - } - - /* if (urlParams.has('whitebalance') || urlParams.has('temp')){ // Need to be applied after the camera is selected. bleh. not enforcible. remove for now. - var temperature = urlParams.get('whitebalance') || urlParams.get('temp'); - try{ - updateCameraConstraints('colorTemperature', parseFloat(temperature)); - } catch (e){errorlog(e);} - } */ - - if (urlParams.has("streamid") || urlParams.has("view") || urlParams.has("v") || urlParams.has("V") ||urlParams.has("pull")) { - // the streams we want to view; if set, but let blank, we will request no streams to watch. - session.view = urlParams.get("streamid") || urlParams.get("view") || urlParams.get("v") || urlParams.get("V") || urlParams.get("pull") || null; // this value can be comma seperated for multiple streams to pull - - getById("headphonesDiv2").classList.remove("hidden"); - getById("headphonesDiv").classList.remove("hidden"); - - getById("addPasswordBasic").style.display = "none"; - - if (session.view == null) { - session.view = ""; - } - - /* if (session.view){ - if (urlParams.has('include') && urlParams.get('include')){ - session.view += ","+urlParams.get('include'); - } - } */ - if (session.scene !== false && session.style === false && session.studioSoftware) { - session.style = 1; - } - } - // https://vdo.ninja/?fakeguests=10&room=faketestroom123&scene&border=10&padding=20&rounded - // https://vdo.ninja/?fakeusers=10&scene&room=test12342345ff - - if (urlParams.has("fakeguests") || urlParams.has("fakefeeds") || urlParams.has("fakeusers")) { - var total = parseInt(urlParams.get("fakeguests")) || parseInt(urlParams.get("fakefeeds")) || parseInt(urlParams.get("fakeusers")) || 4; - session.fakeFeeds = []; - log("Creating " + total + " fake feeds"); - for (var i = 0; i < total; i++) { - let fakeElement = document.createElement("video"); - fakeElement.autoplay = true; - fakeElement.loop = true; - fakeElement.muted = true; - fakeElement.src = "./media/fakesteve.webm"; - fakeElement.labelText = "Fake Steve\\n@steveseguin\\nhe\/him"; - fakeElement.dataset.sid = fakeElement.id = parseInt(Math.random() * 10000000000); - session.fakeFeeds.push(fakeElement); - } - if ((session.view!==false) || session.whipView || session.scene !== false || session.whepInput) { - setTimeout(function () { - updateMixer(); - }, 1000); - } - } - - if (urlParams.has("directoronly") || urlParams.has("directorsonly") || urlParams.has("do")) { - session.viewDirectorOnly = true; - } - - if (session.view !== false) { - session.view_set = session.view.split(","); - } - - if (session.view_set) { - session.allowScreen = []; - session.allowVideos = []; - var i = session.view_set.length; - while (i--) { - var split = session.view_set[i].split(":s"); - if (split.length > 1) { - session.allowScreen.push(split[0]); - session.view_set.splice(i, 1); - if (!(split[0] in session.view_set)) { - session.view_set.push(split[0]); - } - } else if (split[0]) { - session.allowVideos.push(split[0]); - } else { - session.view_set.splice(i, 1); - } - } - } - - if (urlParams.has("include") && urlParams.get("include")) { - urlParams.get("include") - .split(",") - .forEach(sid => { - var sidd = sid.split(":s")[0]; - if (sidd && !session.include.includes(sidd)) { - session.include.push(sidd); - } - }); - } - - if (urlParams.get("waitpage")) { - session.waitPage = urlParams.get("waitpage"); - } - - if (urlParams.has("waitice")) { - session.waitForCandidates = true; - } - - if (urlParams.has("directorview") || urlParams.has("dv")) { - session.directorView = true; - } - if (urlParams.has("graphs")) { - session.allowGraphs = true; - } - - if (urlParams.has("ruler") || urlParams.has("grid") || urlParams.has("thirds")) { - session.fullscreen = true; - if (!session.manual) { - session.manual = session.manual === null ? false : session.manual; - } - session.ruleOfThirds = urlParams.get("ruler") || urlParams.get("grid") || urlParams.get("thirds") || "./media/thirds.svg"; - session.ruleOfThirds = decodeURIComponent(session.ruleOfThirds); - } - - if (urlParams.has("smallshare") || urlParams.has("smallscreen")) { - session.notifyScreenShare = false; - } - - if (urlParams.has("proxy")) { - // routes the wss traffic via an alternative network path. Not - session.proxy = true; // only works if session.wss is set to false - } else if (location.hostname === "proxy.vdo.ninja") { - session.proxy = true; - } - - if (urlParams.has("nopreview") || urlParams.has("np")) { - log("preview OFF"); - session.nopreview = true; - if (iOS || iPad) { - session.nopreview = false; - session.minipreview = 3; // - } - } else if (urlParams.has("minipreview") || urlParams.has("mini")) { - var mini = urlParams.get("minipreview") || urlParams.get("mini"); // 2 is a valid option. (3 is for iPhone with a hidden preview) - - if (mini === "0") { - mini = false; - } else if (mini) { - mini = parseInt(mini); - } else { - mini = 1; - } - log("preview ON"); - session.nopreview = false; - session.minipreview = mini; - if (session.manual === null) { - session.manual = session.manual === null ? false : session.manual; - } - } else if (urlParams.has("largepreview")) { - session.nopreview = false; - session.minipreview = false; - if (session.manual === null) { - session.manual = session.manual === null ? false : session.manual; - } - } else if (urlParams.has("preview") || urlParams.has("showpreview")) { - log("preview ON"); - if (session.manual === null) { - session.manual = session.manual === null ? false : session.manual; - } - session.nopreview = false; - } - - if (urlParams.has("minipreviewoffset") || urlParams.has("mpo")) { - // 40 would be centered - session.leftMiniPreview = urlParams.get("minipreviewoffset") || urlParams.get("mpo") || 0; - session.leftMiniPreview = parseInt(session.leftMiniPreview) || 0; - if (session.leftMiniPreview < -20) { - session.leftMiniPreview = -20; - } else if (session.leftMiniPreview > 120) { - session.leftMiniPreview = 120; - } - } - - if (urlParams.has("obsfix")) { - session.obsfix = urlParams.get("obsfix"); - if (session.obsfix) { - session.obsfix = session.obsfix.toLowerCase(); - } - if (session.obsfix == "false") { - session.obsfix = false; - } else if (session.obsfix == "0") { - session.obsfix = false; - } else if (session.obsfix == "no") { - session.obsfix = false; - } else if (session.obsfix == "off") { - session.obsfix = false; - } else if (parseInt(session.obsfix) > 0) { - session.obsfix = parseInt(session.obsfix); - } else { - session.obsfix = 1; // aggressive. - } - } - - if (urlParams.has("controlroombitrate") || urlParams.has("crb")) { - session.controlRoomBitrate = true; - } - - if (urlParams.has("minroombitrate") || urlParams.has("mrb")) { - session.minimumRoomBitrate = urlParams.get("minroombitrate") || urlParams.get("mrb") || false; - session.minimumRoomBitrate = parseInt(session.minimumRoomBitrate) || false; - } - - if (urlParams.has("remote") || urlParams.has("rem")) { - log("remote ENABLED"); - session.remote = urlParams.get("remote") || urlParams.get("rem") || true; - } - - if (urlParams.has("slideshow")) { - // stream labs mobile fix ? - var ssinterval = parseInt(urlParams.get("slideshow")) || 25; - ssinterval = 1000 / ssinterval; - session.manual = session.manual === null ? true : session.manual; - session.dynamicScale = false; - setInterval(function () { - try { - slideshowHack(); - } catch (e) { - errorlog(e); - } - }, ssinterval); - } - - if (urlParams.has("latency") || urlParams.has("al") || urlParams.has("audiolatency")) { - log("latency ENABLED"); - session.audioLatency = urlParams.get("latency") || urlParams.get("al") || urlParams.get("audiolatency"); - session.audioLatency = parseInt(session.audioLatency) || 0; - session.disableWebAudio = false; - } - - if (urlParams.has("micdelay") || urlParams.has("delay") || urlParams.has("md")) { - log("audio gain ENABLED"); - session.micDelay = urlParams.get("micdelay") || urlParams.get("delay") || urlParams.get("md") || 0; - session.micDelay = parseInt(session.micDelay) || 0; - session.disableWebAudio = false; - } - + + if (urlParams.has("meshcastcode") || urlParams.has("mccode")) { + session.meshcastCode = urlParams.get("meshcastcode") || urlParams.get("mccode") || false; + } + + if (urlParams.has("nomeshcast") || urlParams.has("nowhep")) { + session.noMeshcast = urlParams.get("nomeshcast") || urlParams.has("nowhep") || true; + } + + if (urlParams.has("chunkcast")) { + session.chunkcast = true; + } + + if (urlParams.get("discordwebhook") || urlParams.get("dwh")) { + session.discordHook = decodeURIComponent(urlParams.get("discordwebhook") || urlParams.get("dwh")); + if (!session.discordHook.startsWith("https://discord.com/api/webhooks/")){ + session.discordHook = "https://discord.com/api/webhooks/"+session.discordHook; + } + } else if (urlParams.get("discordwebhook2") || urlParams.get("dwh2")) { + session.discordHook = decodeURIComponent(urlParams.get("discordwebhook2") || urlParams.get("dwh2")); + session.discordHookSensitive = true; + if (!session.discordHook.startsWith("https://discord.com/api/webhooks/")){ + session.discordHook = "https://discord.com/api/webhooks/"+session.discordHook; + } + } + + if (urlParams.has("drawing")) { + session.allowDrawing = urlParams.get("drawing") || true; + } + + if (urlParams.has("nohistory")) { + session.nohistory = true; + } else if (urlParams.has("history")){ + session.nohistory = false; + } else if (isIFrame){ + session.nohistory = true; + } + + if (urlParams.has("pagezoom")) { + enableFullscreenZoom(); + } + //if (urlParams.has('callin')){ + // awaitInboundCall()(); + //} + + //if (urlParams.has("relaywss")) { + // session.relaywss = true; + //} + + if (urlParams.has("fulltalk") && urlParams.get("fulltalk").length == 6) { + listenWebsocket(urlParams.get("fulltalk"), false); // talk and hear all + } else if (urlParams.has("justtalk") && urlParams.get("justtalk").length == 6) { + joinConference(urlParams.get("justtalk")); // just talk + + if (urlParams.has("hearptsn")) { + listenWebsocket(urlParams.get("justtalk")); // hear ptsn only + } + } else if (urlParams.has("hearptsn") && urlParams.get("hearptsn").length == 6) { + listenWebsocket(urlParams.get("hearptsn")); // hear ptsn only + + if (urlParams.has("justtalk")) { + joinConference(urlParams.get("hearptsn")); + } + } + + var filename = false; + try { + if (!session.decrypted) { + filename = window.location.pathname.substring(window.location.pathname.lastIndexOf("/") + 1); + filename = filename.replace("??", "?"); + filename2 = filename.split("?")[0]; + // split at ??? + if (filename.split(".").length == 1) { + if (filename2.length < 2) { + // easy win + filename = false; + } else if (filename.startsWith("&")) { + // easy win + var tmpHref = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/?" + filename.split("&").slice(1).join("&"); + log("TMP " + tmpHref); + updateURL(filename.split("&")[1], true, tmpHref); + filename = false; + } else if (filename2.split("&")[0].includes("=")) { + log("asdf " + filename.split("&")[0]); + if (history.pushState) { + var tmpHref = window.location.href.substring(0, window.location.href.lastIndexOf("/")); + tmpHref = tmpHref + "/?" + filename; + filename = false; + //warnUser("Please ensure your URL is correctly formatted."); + if (!session.nohistory){ + window.history.pushState({ path: tmpHref.toString() }, "", tmpHref.toString()); + } + } + } else { + filename = filename2.split("&")[0]; + if (filename2 != filename) { + warnUser("Warning: Please ensure your URL is correctly formatted."); + } + } + } else { + filename = false; + } + log(filename); + } + } catch (e) { + errorlog(e); + } + + var directorLanding = false; + if (session.director) { + if (session.director === true) { + // room not specified. + directorLanding = true; + } + session.meterStyle = 1; + session.signalMeter = true; + session.batteryMeter = true; + } else if (filename === "director") { + directorLanding = true; + filename = false; + session.meterStyle = 1; + session.signalMeter = true; + session.batteryMeter = true; + } + + + + if (urlParams.has("updateonslotschange") || urlParams.has("uosc")) { + session.updateOnSlotChange = true; + } + + + if (urlParams.has("signalmeter")) { + session.signalMeter = urlParams.get("signalmeter"); + if (session.signalMeter === "false") { + session.signalMeter = false; + } else if (session.signalMeter === "0") { + session.signalMeter = false; + } else if (session.signalMeter === "no") { + session.signalMeter = false; + } else if (session.signalMeter === "off") { + session.signalMeter = false; + } else { + session.signalMeter = true; + } + } + + if (urlParams.has("batterymeter")) { + session.batteryMeter = urlParams.get("batterymeter"); + if (session.batteryMeter === "false") { + session.batteryMeter = false; + } else if (session.batteryMeter === "0") { + session.batteryMeter = false; + } else if (session.batteryMeter === "no") { + session.batteryMeter = false; + } else if (session.batteryMeter === "off") { + session.batteryMeter = false; + } else { + session.batteryMeter = true; + } + } + + if (urlParams.has("rooms")) { + session.rooms = urlParams + .get("rooms") + .split(",") + .map(function (e) { + return sanitizeRoomName(e); + }); + getById("rooms").classList.remove("hidden"); + } + + if (urlParams.has("leaveorientationflag")) { + session.removeOrientationFlag = false; // leave `a=extmap:3 urn:3gpp:video-orientation\r\n` alone + } + + if (urlParams.has("showdirector") || urlParams.has("sd")) { + session.showDirector = parseInt(urlParams.get("showdirector")) || parseInt(urlParams.get("sd")) || true; // if 2, video only allowed. True or 1 will be video + audio allowed. + // fyi, true is the same as 1 when == is used, so assert(1==true) is true. + } + + if (urlParams.has("bitratecutoff") || urlParams.has("bitcut")) { + session.lowBitrateCutoff = parseInt(urlParams.get("bitratecutoff")) || parseInt(urlParams.get("bitcut")) || 300; // low bitrate cut off. + session.hiddenSceneViewBitrate = false; + } + + if (urlParams.has("motionswitch") || urlParams.has("motiondetection")) { + // switch OBS to this scene when there is motion, and "solo view" this video in the VDO.Ninja auto-mixer, if used + session.motionSwitch = parseInt(urlParams.get("motionswitch")) || parseInt(urlParams.get("motiondetection")) || 15; // threshold of motion needed to trigger + session.hiddenSceneViewBitrate = false; + } + + if (urlParams.has("motionrecord") || urlParams.has("recordmotion")) { + // switch OBS to this scene when there is motion, and "solo view" this video in the VDO.Ninja auto-mixer, if used + session.motionRecord = parseInt(urlParams.get("motionrecord")) || parseInt(urlParams.get("recordmotion")) || 15; // threshold of motion needed to trigger + session.hiddenSceneViewBitrate = false; + } + + if (urlParams.has("pausepreview") || urlParams.has("dpp")) { + try { + session.directorViewBitrate = 0; + document.querySelector('#controls_blank button[data-action-type="change-quality1"]').classList.add("pressed"); + document.querySelector('#controls_blank button[data-action-type="change-quality2"]').classList.remove("pressed"); + document.querySelector('#controls_blank button[data-action-type="change-quality3"]').classList.remove("pressed"); + } catch (e) { + errorlog(e); + } + } + if (urlParams.has("locked")) { + session.locked = urlParams.get("locked"); + + if (session.locked == "portrait" || session.locked == "vertical") { + session.locked = 9.0 / 16.0; + } else if (session.locked == "landscape") { + session.locked = 16.0 / 9.0; + } else if (session.locked == "square") { + session.locked = 1.0; + } else { + session.locked = parseFloat(session.locked) || 16 / 9.0; + } + } + + if (urlParams.has("lowbitratescene") || urlParams.has("cutscene")) { + session.lowBitrateSceneChange = urlParams.get("lowbitratescene") || urlParams.get("cutscene") || "cutscene"; // low bitrate cut off. + session.hiddenSceneViewBitrate = false; + if (session.lowBitrateCutoff === false) { + session.lowBitrateCutoff = 300; + } + } + + if (urlParams.has("rotate")) { + session.rotate = urlParams.get("rotate") || 90; + session.rotate = parseInt(session.rotate); + } + + if (urlParams.has("rotatewindow") || urlParams.has("rotatepage")) { + let rotateThis = parseInt(urlParams.get("rotatewindow")) || parseInt(urlParams.get("rotatepage")) || 90; + updateForceRotatedCSS(rotateThis); + } + + if (urlParams.has("facing")) { + session.facingMode = urlParams.get("facing") || false; + } + if (session.facingMode) { + session.facingMode = session.facingMode.toLowerCase(); + if (session.facingMode == "user") { + // + } else if (session.facingMode == "environment") { + // + } else if (session.facingMode == "rear") { + session.facingMode = "environment"; + } else if (session.facingMode == "back") { + session.facingMode = "environment"; + } else if (session.facingMode == "front") { + session.facingMode = "user"; + } else { + session.facingMode = false; + } + } + + // session.facingMode }; // user or environment + + if (urlParams.has("forcelandscape") || urlParams.has("forcedlandscape") || urlParams.has("fl")) { + session.orientation = "landscape"; + if (Firefox) { + session.fullscreen = true; // windowed mode complicates things in this mode + } + } else if (urlParams.has("forceportrait") || urlParams.has("forcedportrait") || urlParams.has("fp")) { + session.orientation = "portrait"; + if (Firefox) { + session.fullscreen = true; // windowed mode complicates things in this mode + } + } + + if (urlParams.has("forceviewerlandscape")) { + session.keepIncomingVideosInLandscape = parseInt(urlParams.get("forceviewerlandscape")) || 270; + } + + if (urlParams.has("forceviewerportrait")) { + session.keepIncomingVideosInPortrait = parseInt(urlParams.get("forceviewerportrait")) || 90; + } + + document.addEventListener("fullscreenchange", event => { + log("full screen change event"); + log(event); + + // Handle myVideo class for self-preview when using fullscreen button + if (session.fullscreenButton) { + var videoSource = document.getElementById("videosource") || document.getElementById("previewWebcam"); + if (videoSource) { + if (document.fullscreenElement) { + // Entering fullscreen - store original class and remove myVideo constraint + if (!videoSource.dataset.originalClass) { + videoSource.dataset.originalClass = videoSource.className; + } + videoSource.classList.remove("myVideo"); + } else { + // Exiting fullscreen - restore original class + if (videoSource.dataset.originalClass) { + videoSource.className = videoSource.dataset.originalClass; + delete videoSource.dataset.originalClass; + } + } + } + } + + if (document.getElementById("previewWebcam")) { + // Update fullscreen icon even in preview mode + if (document.fullscreenElement) { + getById("fullscreenPageToggle").classList.remove("la-expand-arrows-alt"); + getById("fullscreenPageToggle").classList.add("la-compress-arrows-alt"); + } else { + getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); + getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); + } + return; + } + + if (session.orientation && session.mobile) { + if (document.fullscreenElement) { + document.exitFullscreen(); + getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); + getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); + } + return; + } + if (document.fullscreenElement) { + getById("fullscreenPageToggle").classList.remove("la-expand-arrows-alt"); + getById("fullscreenPageToggle").classList.add("la-compress-arrows-alt"); + } else { + getById("fullscreenPageToggle").classList.add("la-expand-arrows-alt"); + getById("fullscreenPageToggle").classList.remove("la-compress-arrows-alt"); + } + updateMixer(); + }); + + if (urlParams.has("fullscreenbutton") || urlParams.has("fsb")) { + // just an alternative; might be compoundable + if (!(iOS || iPad)) { + session.fullscreenButton = true; + document.documentElement.style.setProperty("--full-screen-button", "none"); + getById("fullscreenPage").classList.remove("hidden"); + } + } else if (urlParams.has("nofullscreenbutton") || urlParams.has("nofsb")) { + // just an alternative; might be compoundable + session.nofullwindowbutton = true; + } + + if (urlParams.has("pip2") || urlParams.has("pipall")) { + // just an alternative; might be compoundable + if (typeof documentPictureInPicture !== "undefined") { + getById("PictureInPicturePage").classList.remove("hidden"); + } + } + + if (urlParams.has("midi") || urlParams.has("hotkeys")) { + session.midiHotkeys = urlParams.get("midi") || urlParams.get("hotkeys") || 1; + session.midiHotkeys = parseInt(session.midiHotkeys); + } + + if (urlParams.has("disablehotkeys")) { + session.disableHotKeys = true; + } + + if (urlParams.has("nohangupbutton") || urlParams.has("nohub")) { + getById("hangupbutton").style.display = "none"; + session.hangupbutton = false; + } + + if (urlParams.has("hangupbutton") || urlParams.has("hub") || urlParams.has("humb64")) { + session.hangupbutton = true; + } + if (urlParams.has("hangupmessage") || urlParams.has("hum") || urlParams.has("humb64")) { + let htmlmessage = urlParams.get("hangupmessage") || urlParams.get("hum") || urlParams.get("humb64"); + + if (urlParams.get("humb64")) { + try { + htmlmessage = atob(htmlmessage); + } catch (e) {} + } + + try { + htmlmessage = htmlmessage.replace(/(\r\n|\n|\r)/gm, ""); + htmlmessage = decodeURIComponent(htmlmessage); + } catch (e) { + console.error(e); + } + getById("hangupContainer").innerHTML = htmlmessage; + } + + if (urlParams.has("socialstream")) { + session.socialstream = urlParams.get("socialstream") || false; + } + + if (urlParams.has("midioffset")) { + session.midiOffset = urlParams.get("midioffset") || 0; + session.midiOffset = parseInt(session.midiOffset); + } + + if (urlParams.has("midiremote") || urlParams.has("remotemidi")) { + if (session.director !== false) { + session.midiRemote = parseInt(urlParams.get("midiremote")) || parseInt(urlParams.get("remotemidi")) || 4; + } else { + session.midiRemote = parseInt(urlParams.get("midiremote")) || parseInt(urlParams.get("remotemidi")) || 1; + } + } + + if (urlParams.has("midipush") || urlParams.has("midiout") || urlParams.has("mo")) { + session.midiOut = parseInt(urlParams.get("midipush")) || parseInt(urlParams.get("midiout")) || parseInt(urlParams.get("mo")) || true; + } + + if (urlParams.has("tc") || urlParams.has("timecode") || urlParams.has("showtimecode")) { + session.midiTimecode = urlParams.get("tc") || urlParams.get("timecode") || urlParams.get("showtimecode") || true; + } + + if (urlParams.has("nochunkediframestats")) { + session.chunkIframe = false; + } + + if (urlParams.has("midiiframe")) { + session.midiIframe = true; + } + + if (urlParams.has("midipull") || urlParams.has("midiin") || urlParams.has("midin") || urlParams.has("mi")) { + session.midiIn = parseInt(urlParams.get("midipull")) || parseInt(urlParams.get("midiin")) || parseInt(urlParams.get("midin")) || parseInt(urlParams.get("mi")) || true; + } + + if (urlParams.has("mididelay")) { + // midi-in delay + session.midiDelay = parseInt(urlParams.get("mididelay")) || 1000; // 1 second playout delay? acts as a buffer as well I guess. + } + + if (urlParams.has("midichannel")) { + session.midiChannel = parseInt(urlParams.get("midichannel")) || false; + } + if (session.midiChannel) { + session.midiChannel = parseInt(session.midiChannel); + if (session.midiChannel > 16) { + session.midiChannel = false; + } + if (session.midiChannel < 1) { + session.midiChannel = false; + } + } + if (urlParams.has("mididevice")) { + session.midiDevice = parseInt(urlParams.get("mididevice")) || false; + } + + if (urlParams.has("ptt")) { + if (urlParams.get("ptt")) { + setHotKeyAuto(urlParams.get("ptt")); + } else { + promptAlt("Select a hotkey", true, false, getById("pptHotKey").value, false, false, true); + } + } + + if (directorLanding) { + getById("container-1").classList.remove("hidden"); + getById("container-1").classList.add("skip-animation"); + getById("container-1").classList.remove("pointer"); + } else if (session.director) { + // if a director, webcam/screenshare/iframe auto-defaults shouldn't work + } else if (urlParams.has("webcam") || urlParams.has("wc") || urlParams.has("miconly")) { + session.webcamonly = true; + session.screensharebutton = false; + if (urlParams.has("miconly")) { + session.videoDevice = 0; + session.miconly = true; + miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); + getById("container-3").title = getById("add_camera").innerText; + + getById("videoMenu").style.display = "none"; + getById("container-3").classList.add("microphoneBackground"); + getById("flipcamerabutton").style.setProperty("display", "none", "important"); + getById("mutevideobutton").style.setProperty("display", "none", "important"); + getById("videoMenu3").style.setProperty("display", "none", "important"); + getById("previewWebcam").classList.add("miconly"); + //if (session.consent){ + // setTimeout(function(){ + // warnUser("⚠ Privacy warning: The director of this room can remotely switch your camera or microphone without permission.", 8000); + // }, 1500); + //} + } + } else if (urlParams.has("screenshare") || urlParams.has("ss")) { + session.screenshare = true; + if (urlParams.get("screenshare") || urlParams.get("ss")) { + session.screenshare = urlParams.get("screenshare") || urlParams.get("ss"); + } + } else if (urlParams.has("fileshare") || urlParams.has("fs")) { + getById("container-5").classList.remove("hidden"); + getById("container-5").classList.add("skip-animation"); + getById("container-5").classList.remove("pointer"); + + //getById("sharefilebutton").style.display = "flex"; // this might be obsolete? + // getById("mediafileshare").classList.remove("hidden"); + + if (SafariVersion) { + getById("safari_warning_fileshare").classList.remove("hidden"); + } else if (!Firefox) { + getById("chrome_warning_fileshare").classList.remove("hidden"); + } + } else if (!session.director && (urlParams.has("website") || urlParams.has("iframe"))) { + getById("container-6").classList.remove("hidden"); + getById("container-6").classList.add("skip-animation"); + getById("container-6").classList.remove("pointer"); + session.website = urlParams.get("website") || urlParams.get("iframe") || false; + if (session.website) { + session.website = decodeURI(session.website); + delayedStartupFuncs.push([session.publishIFrame, session.website]); + } + } else if (!session.director && (urlParams.has("framegrab"))) { + getById("container-6").classList.remove("hidden"); + getById("container-6").classList.add("skip-animation"); + getById("container-6").classList.remove("pointer"); + session.framegrab = urlParams.get("framegrab") || false; + session.framegrabAudio = false; + session.framegrabAudioRequested = false; + const framegrabAudioParamName = urlParams.has("framegrabaudio") + ? "framegrabaudio" + : (urlParams.has("audio") ? "audio" : null); + session.pendingFramegrabAudioSettings = null; + if (framegrabAudioParamName) { + session.framegrabAudioRequested = true; + const rawValue = urlParams.get(framegrabAudioParamName); + if (rawValue === null || rawValue === "") { + session.framegrabAudio = true; + } else { + const normalized = String(rawValue).toLowerCase(); + session.framegrabAudio = !["0", "false", "no", "off"].includes(normalized); + } + if (session.framegrabAudio) { + session.pendingFramegrabAudioSettings = { enable: true }; + } else { + session.pendingFramegrabAudioSettings = { enable: false }; + } + } + delayedStartupFuncs.push([session.publishFrameSource, session.framegrab]); + // session.publishFrameSource + } else if (urlParams.has("webcam2") || urlParams.has("wc2")) { + session.webcamonly = true; + session.screensharebutton = false; + session.introButton = true; + } else if (urlParams.has("screenshare2") || urlParams.has("ss2")) { + session.screenshare = true; + session.introButton = true; + if (urlParams.get("screenshare2") || urlParams.get("ss2")) { + session.screenshare = urlParams.get("screenshare2") || urlParams.get("ss2"); + } + } + + if (session.director && (urlParams.has("website") || urlParams.has("iframe"))) { + getById("container-6").classList.remove("hidden"); + getById("container-6").classList.add("skip-animation"); + getById("container-6").classList.remove("pointer"); + session.website = urlParams.get("website") || urlParams.get("iframe") || false; + if (session.website) { + session.website = decodeURI(session.website); + delayedStartupFuncs.push([shareWebsite, session.website]); + } + } + + if (urlParams.has("sstype") || urlParams.has("screensharetype")) { + // wha type of screen sharing is used; track replace, iframe, or secondary try + session.screenshareType = urlParams.get("sstype") || urlParams.get("screensharetype"); + session.screenshareType = parseInt(session.screenshareType) || false; + } + + if (urlParams.has("ssstyle") || urlParams.has("screensharestyle")) { + // wha type of screen sharing is used; track replace, iframe, or secondary try + session.screenshareStyle = urlParams.get("ssstyle") || urlParams.get("screensharestyle") || 1; + session.screenshareStyle = parseInt(session.screenshareStyle) || false; + } + if (urlParams.has("alignright") || urlParams.has("rightalign")) { + let alignValue = urlParams.get("alignright"); + if (alignValue === null) { + alignValue = urlParams.get("rightalign"); + } + if (alignValue === null || alignValue === "") { + session.alignRight = true; + } else { + const normalizedAlign = String(alignValue).toLowerCase(); + session.alignRight = !["0", "false", "no", "off"].includes(normalizedAlign); + } + } + + if (urlParams.has("suppresslocalaudio")) { + session.suppressLocalAudioPlayback = true; + } + if (urlParams.has("prefercurrenttab")) { + session.preferCurrentTab = true; + } + if (urlParams.has("selfbrowsersurface")) { + // exclude + session.selfBrowserSurface = urlParams.get("selfbrowsersurface") || "exclude"; + } + if (urlParams.has("surfaceswitching")) { + session.surfaceSwitching = urlParams.get("surfaceswitching") || "exclude"; + } + if (urlParams.has("systemaudio")) { + // exclude or exclude + session.systemAudio = urlParams.get("systemaudio") || "exclude"; + } + if (urlParams.has("displaysurface")) { + // browser, window, or monitor (which is default selected) + session.displaySurface = urlParams.get("displaysurface") || "monitor"; + } + + if (urlParams.has("recordwindow") || urlParams.has("rw")) { + // Streamlined scene window recording - captures current tab and records to disk + session.recordWindow = true; + session.cleanOutput = true; + session.preferCurrentTab = true; + session.selfBrowserSurface = "include"; + session.displaySurface = "browser"; + session.suppressLocalAudioPlayback = true; + if (urlParams.get("recordwindow") || urlParams.get("rw")) { + session.recordWindow = parseInt(urlParams.get("recordwindow") || urlParams.get("rw")) || 6000; // bitrate + } + } + + if (urlParams.has("locksize")) { + // browser, window, or monitor (which is default selected) + session.lockWindowSize = urlParams.get("locksize") || true; + } + + if (urlParams.has("intro") || urlParams.has("ib")) { + session.introButton = true; + } + + if (urlParams.has("volumecontrol") || urlParams.has("volumecontrols") || urlParams.has("vc")) { + if (!(iOS || iPad)) { + session.volumeControl = true; + } + } + + if (urlParams.has("autohide")) { + session.autohide = true; + session.dedicatedControlBarSpace = false; + } + + if (urlParams.has("controlbarspace")) { + session.dedicatedControlBarSpace = true; + } else if (urlParams.has("nocontrolbarspace")) { + session.dedicatedControlBarSpace = false; + } + + if (urlParams.has("hidesolo") || urlParams.has("hs")) { + session.hidesololinks = true; + } + + if (urlParams.has("ignorehighlight") || urlParams.has("ih")) { + session.ignoreHighlight = true; + } + + if (urlParams.has("mute") || urlParams.has("muted") || urlParams.has("m")) { + session.muted = true; + } + + if (urlParams.has("hideguest") || urlParams.has("hidden")) { + session.directorVideoMuted = true; + } + + if (urlParams.has("videomute") || urlParams.has("videomuted") || urlParams.has("vm")) { + session.videoMutedFlag = true; + } + + if (urlParams.has("pauseinvisible")) { + session.pauseInvisible = true; + } + + if (urlParams.has("zoomslider")) { + session.zoomSlider = true; + } + if (urlParams.has("ptzslider") || urlParams.has("ptzcontrol") || urlParams.has("ptzcontrols")) { + session.ptzSlider = true; + } + + if (urlParams.get("viewslot")) { + session.viewslot = parseInt(urlParams.get("viewslot")) || false; + session.accept_layouts = true; + session.layout = {}; + session.exclusiveLayoutAudio = true; + session.hiddenSceneViewBitrate = 0; + + } else if (urlParams.has("layout")) { + if (!urlParams.get("layout")) { + session.accept_layouts = true; + session.layout = {}; + } else { + + let decodedParam; + try { + decodedParam = decodeURIComponent(urlParams.get("layout")); + } catch (e) { + decodedParam = urlParams.get("layout"); + } + try { + session.layout = JSON.parse(decodedParam); + } catch (e) { + try { + const base64Decoded = atob(decodedParam); + try { + session.layout = JSON.parse(base64Decoded); + } catch (e) { + session.layout = base64Decoded; + } + } catch (e) { + session.layout = decodedParam; + } + } + + if (typeof session.layout === 'object' && session.layout !== null && Object.keys(session.layout).length > 0) { + session.updateOnSlotChange = true; + } + } + console.warn("Warning: If using &layout with &broadcast, only the director's video will appear in the custom layout, which is likely not intended."); + } + + if (urlParams.get("updateonslotschange") || urlParams.get("uosc")) { + let uosc = urlParams.get("updateonslotschange") || urlParams.get("uosc"); + if (["false","0","off"].includes(uosc)){ + session.updateOnSlotChange = false; + } + } + + if (urlParams.get("exclusivelayoutaudio")) { + session.exclusiveLayoutAudio = true; + } else if (urlParams.get("inclusivelayoutaudio")) { + session.exclusiveLayoutAudio = false; + } + + if (urlParams.has("layouts")) { + // an ordered array of layouts, which can be used to switch between using the API layouts action. + // ie: ?layouts=[[{"x":0,"y":0,"w":100,"h":100,"slot":0}],[{"x":0,"y":0,"w":100,"h":100,"slot":1}],[{"x":0,"y":0,"w":100,"h":100,"slot":2}],[{"x":0,"y":0,"w":100,"h":100,"slot":3}],[{"x":0,"y":0,"w":50,"h":100,"c":false,"slot":0},{"x":50,"y":0,"w":50,"h":100,"c":false,"slot":1}],[{"x":0,"y":0,"w":100,"h":100,"z":0,"c":false,"slot":1},{"x":70,"y":70,"w":30,"h":30,"z":1,"c":true,"slot":0}],[{"x":0,"y":0,"w":50,"h":50,"c":true,"slot":0},{"x":50,"y":0,"w":50,"h":50,"c":true,"slot":1},{"x":0,"y":50,"w":50,"h":50,"c":true,"slot":2},{"x":50,"y":50,"w":50,"h":50,"c":true,"slot":3}],[{"x":0,"y":16.667,"w":66.667,"h":66.667,"c":true,"slot":0},{"x":66.667,"y":0,"w":33.333,"h":33.333,"c":true,"slot":1},{"x":66.667,"y":33.333,"w":33.333,"h":33.333,"c":true,"slot":2},{"x":66.667,"y":66.667,"w":33.333,"h":33.333,"c":true,"slot":3}]] + try { + session.layouts = JSON.parse(decodeURIComponent(urlParams.get("layouts"))) || JSON.parse(urlParams.get("layouts")) || {}; + } catch (e) { + try { + session.layouts = JSON.parse(urlParams.get("layouts")) || false; + } catch (e) { + session.layouts = false; + } + } + } + + /* if (session.layout && session.layouts && (typeof session.layout !== "object") && parseInt(session.layout) && (session.layout == parseInt(session.layout))){ + try { + session.layout = session.layouts[session.layout-1]; + } catch(e){ + session.layout= false; + } + } */ + + if (urlParams.has("deaf") || urlParams.has("deafen")) { + session.directorSpeakerMuted = true; // false == true in this case. + } + + if (urlParams.has("blind")) { + session.directorDisplayMuted = true; // false == true in this case. + } + + if (urlParams.has("blindall")) { + session.directorBlindButton = true; // false == true in this case. + } + if (session.directorBlindButton) { + getById("blindAllGuests").classList.remove("hidden"); + } + + if (urlParams.has("dpi") || urlParams.has("dpr") || urlParams.has("sharper") || urlParams.has("sharpen")) { + session.devicePixelRatio = urlParams.get("dpi") || urlParams.get("dpr") || 2.0; + session.devicePixelRatio = parseFloat(session.devicePixelRatio); + } //else if (window.devicePixelRatio && window.devicePixelRatio!==1){ + // session.devicePixelRatio = window.devicePixelRatio; // this annoys me to no end. + //} + + if (urlParams.has("speakermute") || urlParams.has("speakermuted") || urlParams.has("mutespeaker") || urlParams.has("sm") || urlParams.has("ms")) { + var checkState = urlParams.get("speakermute") || urlParams.get("speakermuted") || urlParams.get("mutespeaker") || urlParams.get("sm") || urlParams.get("ms") || true; + + if (checkState === "false") { + session.speakerMuted = false; + } else if (checkState === "0") { + session.speakerMuted = false; + } else if (checkState === "no") { + session.speakerMuted = false; + } else if (checkState === "off") { + session.speakerMuted = false; + } else { + session.speakerMuted = true; + } + + session.speakerMuted_default = session.speakerMuted; + + if (session.speakerMuted) { + getById("mutespeakertoggle").className = "las la-volume-mute toggleSize"; + //getById("mutespeakerbutton").className="hidden float2 red"; + getById("mutespeakerbutton").classList.add("red"); + getById("mutespeakerbutton").classList.add("float2"); + getById("mutespeakerbutton").classList.remove("float"); + + var sounds = document.getElementsByTagName("video"); + for (var i = 0; i < sounds.length; ++i) { + sounds[i].muted = session.speakerMuted; + } + } + } + + if (urlParams.has("chatbutton") || urlParams.has("chat") || urlParams.has("cb")) { + session.chatbutton = urlParams.get("chatbutton") || urlParams.get("chat") || urlParams.get("cb") || null; + if (session.chatbutton === "false") { + session.chatbutton = false; + } else if (session.chatbutton === "0") { + session.chatbutton = false; + } else if (session.chatbutton === "no") { + session.chatbutton = false; + } else if (session.chatbutton === "off") { + session.chatbutton = false; + } else { + session.chatbutton = true; + getById("chatbutton").classList.remove("hidden"); + } + } + + // Tipping feature - unified &tipsid parameter + // &tipsid=xxx → use this overlay token, enable tips, fetch username from API + // &tipsid (no value) → show onboarding modal for signup + // Legacy: &tip, &tips, &tipid also supported for backwards compatibility + if (urlParams.has("tipsid") || urlParams.has("tip") || urlParams.has("tipid")) { + var tipsIdValue = urlParams.get("tipsid") || urlParams.get("tip") || urlParams.get("tipid"); + session.receiveTips = true; + if (tipsIdValue) { + session.tipsId = tipsIdValue; + } else { + // No ID specified - show onboarding modal after page loads + setTimeout(function() { + if (typeof showTipOnboardingModal === 'function') { + showTipOnboardingModal(); + } + }, 2000); + } + } + // Legacy aliases for showing onboarding + if (urlParams.has("receivetips") || urlParams.has("tipping")) { + session.receiveTips = true; + if (!session.tipsId) { + setTimeout(function() { + if (typeof showTipOnboardingModal === 'function') { + showTipOnboardingModal(); + } + }, 2000); + } + } + if (urlParams.has("tipserver")) { + session.tipServer = urlParams.get("tipserver"); + } + if (urlParams.has("tipamounts")) { + try { + session.tipAmounts = urlParams.get("tipamounts").split(",").map(x => parseInt(x)).filter(x => x > 0); + } catch (e) { + session.tipAmounts = [5, 10, 25, 50, 100]; + } + } + if (urlParams.has("tipcurrency")) { + session.tipCurrency = urlParams.get("tipcurrency").toUpperCase(); + } + // Viewer opt-in to see tip UI (two-way opt-in system) + if (urlParams.has("showtips") || urlParams.has("supporttips")) { + session.showTips = true; + } + // QR code size for tip overlay (default 150px, min 100px for scanability) + if (urlParams.has("tipqrsize")) { + session.tipQRSize = Math.max(parseInt(urlParams.get("tipqrsize")) || 150, 100); + } + // Disable QR code animation + if (urlParams.has("notipqr")) { + session.noTipQR = true; + } + + if (urlParams.has("app")) { + // midi-in delay + session.screenshare = false; + getById("container-2").classList.add("hidden"); + getById("logoname").classList.add("hidden"); + getById("head1a").classList.remove("hidden"); + getById("main").classList.add("appmode"); + getById("jumptoroomButton").innerText = "Join Room"; + + if (getStorage("jumptoURL")) { + getById("joinbyURL").value = getStorage("jumptoURL"); + } + } + + if (session.screenshare !== false) { + if (session.introButton) { + getById("container-3").className = "column columnfade hidden"; // Hide screen share + getById("head1").className = "hidden"; + } else { + getById("container-3").className = "column columnfade hidden"; // Hide webcam + getById("container-2").classList.add("skip-animation"); + getById("container-2").classList.remove("pointer"); + } + } + + if (urlParams.has("hands") || urlParams.has("hand")) { + session.raisehands = urlParams.get("hands") || urlParams.get("hand") || 1; + session.raisehands = parseInt(session.raisehands); + } + + if (urlParams.has("portrait") || urlParams.has("916") || urlParams.has("vertical")) { + // playback aspect ratio + session.aspectRatio = 1; // 9:16 (default of 0 is 16:9) + } else if (urlParams.has("square") || urlParams.has("11")) { + session.aspectRatio = 2; //1:1 ? + } else if (urlParams.has("43")) { + session.aspectRatio = 3; //1:1 ? + } + + if (urlParams.has("structure")) { + session.structure = true; + } + + if (urlParams.has("aspectratio") || urlParams.has("ar")) { + // capture aspect ratio + session.forceAspectRatio = urlParams.get("aspectratio") || urlParams.get("ar") || false; + if (session.forceAspectRatio) { + if (session.forceAspectRatio == "portrait" || session.forceAspectRatio == "vertical") { + session.forceAspectRatio = 9.0 / 16.0; + } else if (session.forceAspectRatio == "landscape") { + session.forceAspectRatio = 16.0 / 9.0; + } else if (session.forceAspectRatio == "square") { + session.forceAspectRatio = 1.0; + } else { + session.forceAspectRatio = parseFloat(session.forceAspectRatio) || false; + } + } + } + if (urlParams.has("screenshareaspectratio") || urlParams.has("ssar")) { + // capture aspect ratio + session.forceScreenShareAspectRatio = urlParams.get("screenshareaspectratio") || urlParams.get("ssar") || 16.0 / 9.0; + if (session.forceScreenShareAspectRatio) { + if (session.forceScreenShareAspectRatio == "portrait" || session.forceScreenShareAspectRatio == "vertical") { + session.forceScreenShareAspectRatio = 9.0 / 16.0; + } else if (session.forceScreenShareAspectRatio == "landscape") { + session.forceScreenShareAspectRatio = 16.0 / 9.0; + } else if (session.forceScreenShareAspectRatio == "square") { + session.forceScreenShareAspectRatio = 1.0; + } else { + session.forceScreenShareAspectRatio = parseFloat(session.forceScreenShareAspectRatio) || false; + } + } + } + + if (urlParams.has("crop")) { + var crop = parseFloat(urlParams.get("crop")) || 0; + if (crop > 0) { + session.forceAspectRatio = 1.7777777778 * (crop / 100); + } else if (crop < 0) { + session.forceAspectRatio = 1.7777777778 / (crop / 100); + } else { + session.forceAspectRatio = 1.3333333333; + } + } + + if (urlParams.has("cover")) { + session.cover = urlParams.get("cover") || true; + document.documentElement.style.setProperty("--fit-style", "cover"); + document.documentElement.style.setProperty("--myvideo-max-width", "100vw"); + document.documentElement.style.setProperty("--myvideo-width", "100vw"); + document.documentElement.style.setProperty("--myvideo-height", "100vh"); + } else if (urlParams.has("fit")) { + // not fully implemented yet. + session.cover = true; + document.documentElement.style.setProperty("--fit-style", "fit"); + document.documentElement.style.setProperty("--myvideo-max-width", "100vw"); + document.documentElement.style.setProperty("--myvideo-width", "100vw"); + document.documentElement.style.setProperty("--myvideo-height", "100vh"); + } + + if (urlParams.has("record")) { + if (!session.cleanOutput) { + if (SafariVersion && !MediaRecorder) { + if (macOS) { + warnUser("Your browser may not support local media recording.\n\nTry Chrome instead if on macOS."); + } else { + warnUser("Your browser or device may not support local media recording.\n\nSafari sometimes allows the feature to be enabled via its experimental settings."); + } + } else if (SafariVersion) { + if (macOS) { + warnUser("It is recommended to use Chrome instead of Safari if doing local media recordings."); + } else if (SafariVersion <= 15) { + warnUser("Please update your device.\n\nOlder versions of Safari may crash after recording for a few minutes."); + } else if (iOS || iPad) { + // iOS-specific warning about split recordings + warnUser("iOS Recording Notice:\n\n• Recordings will be split into 5-minute segments to prevent crashes\n• Files will download as MP4 format\n• Each segment will download separately\n• Use video editing software to join segments if needed\n\nGoogle Drive uploads (if enabled) will work normally."); + } else { + warnUser("Local media recordings are an experimental feature on Apple devices.\n\nPlease at least test it out a few times first."); + } + } + } + session.recordLocal = urlParams.get("record"); + + if (session.recordLocal === "false" || session.recordLocal === "off") { + session.record = false; + session.recordLocal = false; + } else if (session.recordLocal != parseInt(session.recordLocal)) { + session.recordLocal = session.recordDefault; + } else if (session.recordLocal !== null){ + session.recordLocal = parseInt(session.recordLocal); + } else { + session.recordLocal = session.recordDefault; + } + } + + if (session.record === false) { + getById("recordLocalbutton").classList.add("hidden"); + getById("recordLocalScreenbutton").classList.add("hidden"); + try { + document.querySelectorAll('[data-action-type^="record"]').forEach(ele => { + ele.remove(); + delete ele; + }); + document.querySelectorAll('[data-action="Record"]').forEach(ele => { + ele.parentNode.remove(); + delete ele.parentNode; + }); + } catch (e) { + errorlog(e); + } + } + + if (urlParams.has("autorecord")) { + session.autorecord = true; + if (session.recordLocal === false) { + let bitautorec = urlParams.get("autorecord"); + if (bitautorec !== "") { + session.recordLocal = parseInt(bitautorec); + } else { + session.recordLocal = session.recordDefault; + } + } + } + if (urlParams.has("autorecordlocal")) { + session.autorecordlocal = true; + if (session.recordLocal === false) { + session.recordLocal = urlParams.get("autorecordlocal"); + if (session.recordLocal != parseInt(session.recordLocal)) { + session.recordLocal = session.recordDefault; + } else if (session.recordLocal !== ""){ + session.recordLocal = parseInt(session.recordLocal); + } else { + session.recordLocal = session.recordDefault; + } + } + } + if (urlParams.has("autorecordremote")) { + session.autorecordremote = true; + if (session.recordLocal === false) { + session.recordLocal = urlParams.get("autorecordremote"); + if (session.recordLocal != parseInt(session.recordLocal)) { + session.recordLocal = session.recordDefault; + } else if (session.recordLocal !== ""){ + session.recordLocal = parseInt(session.recordLocal); + } else { + session.recordLocal = session.recordDefault; + } + } + } + if (urlParams.has("splitrecording")) { + // minutes + session.recordingInterval = urlParams.get("splitrecording") || 5; // 5 minutes + session.recordingInterval = parseInt(session.recordingInterval) || 1; + // For Mac: https://gist.github.com/steveseguin/8083172a20ad7c9ebcb449e22fc8fe67 + // For Windows: https://gist.github.com/steveseguin/7ca1df1df9ec6042f27ecc8d258e3f30 + } else if ((SafariVersion || iOS || iPad) && (urlParams.has("record") || urlParams.has("autorecord"))) { + // Auto-enable split recording for Safari to prevent memory issues + // iOS/iPad: 5 minutes (tighter memory), desktop Safari: 10 minutes + session.recordingInterval = (iOS || iPad) ? 5 : 10; + if (!session.cleanOutput) { + console.log("Safari detected with recording enabled: Auto-enabling split recording (" + session.recordingInterval + "-minute segments) to prevent memory issues"); + } + } + if (urlParams.has("pcm")) { + session.pcm = true; + } + + if (urlParams.has("recordcodec") || urlParams.has("rc")) { + session.recordingVideoCodec = urlParams.get("recordcodec") || urlParams.get("rc") || false; + } else if (session.recordingVideoCodec===false){ + session.recordingVideoCodec = "vp8"; + } + + if (urlParams.has("recordfolder")) { + session.GDRIVE_FOLDERNAME = urlParams.get("recordfolder") || ""; + } + + if (urlParams.has("menuoffset")) { + getById("subControlButtons").style.bottom = urlParams.get("menuoffset") || "50px"; + getById("controlPositioning").style.bottom = urlParams.get("menuoffset") || "50px"; + getById("subControlButtons").style.setProperty("position", "absolute", "important"); + } + + + if (urlParams.has("bigbutton")) { + session.bigmutebutton = true; + getById("mutebutton").classList.add("bigbutton"); + if (urlParams.get("bigbutton")) { + let bigbuttontext = document.createElement("span"); + bigbuttontext.innerText = urlParams.get("bigbutton"); + bigbuttontext.className = "bigbuttontext"; + getById("mutebutton").appendChild(bigbuttontext); + } + } + + if (urlParams.get("rows")) { + session.rows = urlParams.get("rows"); + session.rows = session.rows.split(","); + } + + if (urlParams.has("nosettings")) { + session.nosettings = true; + getById("settingsbutton").classList.add("hidden"); + } + + if (urlParams.has("publish")) { + session.publish = true; + getById("publishSettings").style.display = "block"; + + if (session.recordLocal !== false){ + getById("startRecordingButton").classList.remove("hidden"); + //session.autorecord = true; + //getById("startPublishingButton").classList.add("hidden"); + } + } + + if (urlParams.has("obscontrols") || urlParams.has("remoteobs") || urlParams.has("obsremote") || urlParams.has("obs") || urlParams.has("controlobs")) { + session.obsControls = urlParams.get("obscontrols") || urlParams.get("remoteobs") || urlParams.get("obsremote") || urlParams.get("obs") || urlParams.get("controlobs"); + if (session.obsControls) { + // whether to show the button or not; that's it. + session.obsControls = session.obsControls.toLowerCase(); + } + if (session.obsControls == "false") { + session.obsControls = false; + } else if (session.obsControls == "0") { + session.obsControls = false; + } else if (session.obsControls == "no") { + session.obsControls = false; + } else if (session.obsControls == "off") { + session.obsControls = false; + } else if (session.obsControls == "full") { + try { + session.dataMode = true; + session.obsControls = true; + //session.doNotSeed = true; + getById("main").classList.add("fullscreenOBSControl"); + toggleOBSControls(); + } catch(e){errorlog(e);} + } else if (session.obsControls) { + session.obsControls = session.obsControls.toLowerCase(); + } else { + session.obsControls = true; + } + } + + if (urlParams.has("nopush") || urlParams.has("noseed") || urlParams.has("viewonly") || urlParams.has("viewmode")) { + // this is like a scene; Seeding is disabled. Can be used with &showall to show all videos on load + session.doNotSeed = true; + + if (session.scene === false) { + session.scene = null; // not a scene, but sorta. false vs null makes a difference here. + } + + session.dataMode = true; // thios will let us connect + // session.showall = true; // this can be used to SHOW the videos. (&showall) + } + + if (urlParams.has("scene") || urlParams.has("scn")) { + session.scene = urlParams.get("scene") || urlParams.get("scn") || 0; + if (typeof session.scene === "string") { + session.scene = session.scene.replace(/[\W]+/g, "_"); + } else { + session.scene = (parseInt(session.scene) || 0) + ""; + } + } + + if (urlParams.has("morescenes")) { + let moreScenes = urlParams.get("morescenes") || 16; + moreScenes = parseInt(moreScenes) || 0; + if (moreScenes < 8) { + moreScenes = 8; + } + session.maxScene = moreScenes; + let sceneButtonMain = document.querySelector("#controls_blank .sceneButtons button"); + if (sceneButtonMain && moreScenes) { + var i = 8; + while (i < moreScenes) { + i++; + let sceneButton = sceneButtonMain.cloneNode(true); + sceneButton.dataset.scene = i; + sceneButton.title = "Add to Scene " + i; + sceneButton.innerHTML = "S" + i + ""; + document.querySelector("#controls_blank .sceneButtons").appendChild(sceneButton); + } + } + } + + // lets a guest join a scene on their own ... but + // doesn't work if the director or scene isn't already loaded. + // requires &openscene on the director to be added + if (urlParams.has("joinscene") || urlParams.has("joinscenes")) { // list of scenes to auto join. + let sceneValues = urlParams.get("joinscene") || urlParams.get("joinscenes"); + if (sceneValues){ + sceneValues = sceneValues.split(","); + session.requestscenes = sceneValues.map(scene => { + scene = scene.trim(); + if (typeof scene === "string") { + return scene.replace(/[\W]+/g, "_"); + } else { + return (parseInt(scene) || 0) + ""; + } + }); + } + } // .. but requires openscene to be set on the target scene. + if (urlParams.has("openscene") || urlParams.has("openscenes")) { + session.openscene = true; + } + + if (urlParams.has("solo")) { + if (session.scene === false) { + session.scene = "0"; + } + session.solo = true; + } + + if (session.scene !== false) { + session.disableWebAudio = true; + if (session.audioEffects === null) { + session.audioEffects = false; + } + session.audioMeterGuest = false; + } + + if (session.recordWindow && session.scene !== false) { + // Add floating record button for scene window recording + var recordBtn = document.createElement("button"); + recordBtn.id = "recordWindowButton"; + recordBtn.innerHTML = "● Start Recording"; + recordBtn.title = "Record this scene to a local video file"; + recordBtn.style.cssText = "position:fixed;top:10px;right:10px;z-index:99999;padding:12px 20px;font-size:16px;line-height:1.2;height:46px;box-sizing:border-box;background:#d00;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:bold;box-shadow:0 2px 10px rgba(0,0,0,0.3);transition:opacity 0.3s;"; + recordBtn.onclick = async function() { + if (this.dataset.recording === "1") { + // Stop recording + if (session.recordWindowElement && session.recordWindowElement.recording) { + recordLocalVideo("stop", false, session.recordWindowElement); + } + this.innerHTML = "● Start Recording"; + this.title = "Record this scene to a local video file"; + this.style.background = "#d00"; + this.style.opacity = "1"; + this.dataset.recording = "0"; + } else { + // Start recording + this.innerHTML = "■ Stop"; + this.title = "Stop recording"; + this.style.background = "#090"; + this.style.opacity = "0.3"; // fade out during recording so it's less visible + this.dataset.recording = "1"; + var bitrate = (typeof session.recordWindow === "number") ? session.recordWindow : 6000; + await recordWindowCapture(bitrate); + } + }; + // Hover to show button clearly during recording + recordBtn.onmouseenter = function() { this.style.opacity = "1"; }; + recordBtn.onmouseleave = function() { if (this.dataset.recording === "1") this.style.opacity = "0.3"; }; + document.body.appendChild(recordBtn); + + // Add Go Live button (experimental - WHIP publish to Twitch) + var liveBtn = document.createElement("button"); + liveBtn.id = "goLiveButton"; + liveBtn.innerHTML = "📡 Go Live"; + liveBtn.title = "Stream to Twitch via WHIP (requires stream key)"; + liveBtn.style.cssText = "position:fixed;top:10px;right:200px;z-index:99999;padding:12px 20px;font-size:16px;line-height:1.2;height:46px;box-sizing:border-box;background:#6441a5;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:bold;box-shadow:0 2px 10px rgba(0,0,0,0.3);"; + liveBtn.onclick = async function() { + if (this.dataset.live === "1") { + // Stop streaming + if (session.goLivePC) { + try { + session.goLivePC.close(); + } catch(e) {} + session.goLivePC = null; + } + this.innerHTML = "📡 Go Live"; + this.title = "Stream to Twitch via WHIP (requires stream key)"; + this.style.background = "#6441a5"; + this.dataset.live = "0"; + return; + } + + // Need a stream to publish + if (!session.recordWindowElement || !session.recordWindowElement.srcObject) { + alert("Please start recording first to capture the scene, then click Go Live."); + return; + } + + var streamKey = prompt("Enter your Twitch Stream Key:"); + if (!streamKey || !streamKey.trim()) { + return; + } + streamKey = streamKey.trim(); + + try { + // Create RTCPeerConnection for WHIP + var config = session.configuration || { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; + var pc = new RTCPeerConnection(config); + session.goLivePC = pc; + + // Add tracks from the captured stream + var stream = session.recordWindowElement.srcObject; + stream.getTracks().forEach(function(track) { + pc.addTransceiver(track, { direction: "sendonly", streams: [stream] }); + }); + + // Create and send offer + var offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + + // Wait for ICE gathering + await new Promise(function(resolve) { + if (pc.iceGatheringState === "complete") { + resolve(); + } else { + pc.onicegatheringstatechange = function() { + if (pc.iceGatheringState === "complete") resolve(); + }; + setTimeout(resolve, 2000); // Fallback timeout + } + }); + + // Send to Twitch WHIP endpoint + var whipUrl = "https://g.webrtc.live-video.net:4443/v2/offer"; + var response = await fetch(whipUrl, { + method: "POST", + headers: { + "Content-Type": "application/sdp", + "Authorization": "Bearer " + streamKey + }, + body: pc.localDescription.sdp + }); + + if (!response.ok) { + throw new Error("WHIP request failed: " + response.status); + } + + var answerSdp = await response.text(); + await pc.setRemoteDescription({ type: "answer", sdp: answerSdp }); + + // Success - update button + this.innerHTML = "🔴 Stop Live"; + this.title = "Stop streaming to Twitch"; + this.style.background = "#d00"; + this.dataset.live = "1"; + + // Handle connection close + pc.onconnectionstatechange = function() { + if (pc.connectionState === "failed" || pc.connectionState === "disconnected") { + liveBtn.innerHTML = "📡 Go Live"; + liveBtn.title = "Stream to Twitch via WHIP (requires stream key)"; + liveBtn.style.background = "#6441a5"; + liveBtn.dataset.live = "0"; + session.goLivePC = null; + } + }; + + } catch(e) { + console.error("Go Live failed:", e); + alert("Failed to go live: " + e.message); + if (session.goLivePC) { + session.goLivePC.close(); + session.goLivePC = null; + } + this.innerHTML = "📡 Go Live"; + this.title = "Stream to Twitch via WHIP (requires stream key)"; + this.style.background = "#6441a5"; + this.dataset.live = "0"; + } + }; + document.body.appendChild(liveBtn); + } + + if (urlParams.has("fakeuser")) { + log("ICE FILTER ENABLED"); + session.fakeUser = true; + session.dataMode = true; + session.autostart = true; + session.novideo = []; + session.noaudio = []; + session.noiframe = []; + session.cleanOutput = true; + } + + if (urlParams.has("retransmit")) { + session.retransmit = true; + session.dataMode = true; + } + + if (urlParams.has("datamode") || urlParams.has("dataonly")) { + // this disables all media in/out. + session.dataMode = true; + } + + if (urlParams.has("pseudoguest") || urlParams.has("pseudoscene")) { + session.videoDevice = 0; + session.audioDevice = 0; + getById("mainmenu").classList.add("hidden"); + getById("header").classList.add("hidden"); + getById("mainmenu").style.display = "none"; + getById("header").style.display = "none"; + session.showList = false; + session.autostart = true; + getById("controlButtons").classList.add("hidden"); + getById("controlButtons").style.display = "none"; + getById("miniTaskBar").classList.add("hidden"); + getById("miniTaskBar").style.display = "none"; + session.dedicatedControlBarSpace = false; + session.pseudoguest = true; + } + + if (session.dataMode) { + if (!(session.meshcast || session.whipOutput !== false || session.screenshare)) { + session.videoDevice = 0; + session.audioDevice = 0; + } + + getById("mainmenu").classList.add("hidden"); + //session.autohide = true; + //session.autostart = true; + //session.novideo = []; + //session.noaudio = []; + //session.noiframe = []; + //session.webcamonly = true; + } + + if (urlParams.has("autoadd")) { + // the streams we want to view; if set, but let blank, we will request no streams to watch. + session.autoadd = urlParams.get("autoadd") || null; // this value can be comma seperated for multiple streams to pull + + if (session.autoadd == null) { + session.autoadd = false; + } + if (session.autoadd) { + session.autoadd = session.autoadd.split(","); + } + } + + if (session.director && urlParams.has("autochannels") || urlParams.has("autochannel")) { + // Director-only: auto-assign guests to audio channels + var val = urlParams.get("autochannels") || urlParams.get("autochannel");; + if (val) { + // Parse comma-separated channel numbers, filter valid 1-8 + session.autochannels = val.split(",") + .map(function(n) { return parseInt(n.trim()); }) + .filter(function(n) { return n >= 1 && n <= 8; }); + if (session.autochannels.length === 0) { + session.autochannels = false; + } + } else { + // &autochannels with no value = default allowed list (skip C4/LFE) + session.autochannels = [1, 2, 3, 5, 6, 7, 8]; + } + } + + if (session.director && urlParams.has("autochannelmode")) { + var mode = urlParams.get("autochannelmode"); + if (mode === "roundrobin" || mode === "rr") { + session.autochannelmode = "roundrobin"; + } else { + session.autochannelmode = "leastused"; + } + } + + if (urlParams.has("preferchannel") || urlParams.has("pc")) { + // Guest's preferred audio channel for auto-assignment + var ch = parseInt(urlParams.get("preferchannel") || urlParams.get("pc")); + if (ch >= 1 && ch <= 8) { + session.preferChannel = ch; + } + } + + //if (session.scene!=="1"){ // scene =0 and 1 should load instantly. + // session.hiddenSceneViewBitrate = 0; // By default this is ~ 400kbps, but if you have 10 scenes, i don't want to kill things. + //} + + if (urlParams.has("hiddenscenebitrate")) { + session.hiddenSceneViewBitrate = parseInt(urlParams.get("hiddenscenebitrate")) || 0; + } else if (urlParams.has("layout") && session.scene !== false && !session.viewslot) { + session.hiddenSceneViewBitrate = false; + } else if (urlParams.has("nohiddensceneoptimization")) { + session.hiddenSceneViewBitrate = false; + } + + if (urlParams.has("preloadbitrate")) { + session.preloadbitrate = parseInt(urlParams.get("preloadbitrate")) || 0; // 1000 + } + + if (urlParams.has("rampuptime")) { + session.rampUpTime = parseInt(urlParams.get("rampuptime")) || 10000; + } + + if (urlParams.has("scenetype") || urlParams.has("type")) { + session.sceneType = parseInt(urlParams.get("scenetype")) || parseInt(urlParams.get("type")) || false; + } + + if (urlParams.has("mediasettings")) { + session.forceMediaSettings = true; + } + + if (urlParams.has("transcript") || urlParams.has("transcribe") || urlParams.has("trans")) { + session.transcript = urlParams.get("transcript") || urlParams.get("transcribe") || urlParams.get("trans") || "en-US"; + } + + if (urlParams.has("cc") || urlParams.has("closedcaptions") || urlParams.has("captions")) { + session.closedCaptions = true; + } + if (urlParams.has("nocclabels") || urlParams.has("nocclabel") || urlParams.has("nocaptionlabels") || urlParams.has("nocaptionlabel")) { + session.nocaptionlabels = true; + } + if (urlParams.has("cccolored") || urlParams.has("cccoloured") || urlParams.has("coloredcc") || urlParams.has("colorcc") || urlParams.has("cccolor")) { + session.ccColored = true; + } + + + if (urlParams.has("base64css") || urlParams.has("b64css") || urlParams.has("cssbase64") || urlParams.has("cssb64")) { + try { + var base64Css = urlParams.get("base64css") || urlParams.get("b64css") || urlParams.get("cssbase64") || urlParams.get("cssb64"); + try { + base64Css = atob(base64Css); // window.btoa(encodeURIComponent("#mainmenu{background-color: pink;}" )); + } catch (e) {} + try { + base64Css = decodeURIComponent(base64Css); // window.btoa(encodeURIComponent("#mainmenu{background-color: pink; ❤" )); + } catch (e) {} + + try { + if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ + session.iFramesAllowed = false; + console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); + } else if ((window !== window.top) || session.studioSoftware) { + // allowed + } else { + console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); + session.iFramesAllowed = false; + } + } catch(e){ + warnlog(e); + } + + var cssStyleSheet = document.createElement("style"); + cssStyleSheet.innerText = base64Css; + document.querySelector("head").appendChild(cssStyleSheet); + } catch (e) { + console.error(e); + } + } + + if (urlParams.has("css")) { + var cssURL = urlParams.get("css"); + try { + cssURL = decodeURI(cssURL); + } catch(e){ + warnlog(e); + } + log(cssURL); + + let validURL = false; + try { + cssUrlObj = new URL(cssURL); + validURL = true; + } catch(e){ + } + try { + if (validURL){ + const cssDomain = cssUrlObj.hostname; + + try { + if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ + session.iFramesAllowed = false; + console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); + } else if ((window.location.hostname === cssDomain) || window.location.hostname.endsWith("."+cssDomain) || (window !== window.top) || session.studioSoftware) { + if (window.location.hostname !== cssDomain){ + console.warn("Third-party CSS has been injected into the site. Security cannot be ensured."); + } + } else { + console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); + session.iFramesAllowed = false; + } + } catch(e){ + warnlog(e); + } + + var cssStylesheet = document.createElement("link"); + cssStylesheet.rel = "stylesheet"; + cssStylesheet.type = "text/css"; + cssStylesheet.media = "screen"; + cssStylesheet.href = cssURL; + document.getElementsByTagName("head")[0].appendChild(cssStylesheet); + + cssStylesheet.onload = function () { + getById("main").classList.remove("hidden"); + log("loaded remote style sheet"); + }; + + cssStylesheet.onerror = function () { + getById("main").classList.remove("hidden"); + errorlog("REMOTE STYLE SHEET HAD ERROR"); + }; + } else { + try { + if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ + console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); + session.iFramesAllowed = false; + } else if ((window !== window.top) || session.studioSoftware) { + // allowed + } else { + console.warn("For security and privacy purposes, please note that you will not be allowed to use CSS injection together with IFRAMES."); + session.iFramesAllowed = false; + } + } catch(e){ + warnlog(e); + } + var cssStylesheet = document.createElement("style"); + cssStylesheet.innerHTML = cssURL; + document.getElementsByTagName("head")[0].appendChild(cssStylesheet); + getById("main").classList.remove("hidden"); + } + + } catch(e){ + warnlog(e); + } + } else { + getById("main").classList.remove("hidden"); + } + + if (urlParams.has("avatar")) { + var avatar = urlParams.get("avatar") || false; + if (avatar && avatar == "default") { + session.avatar = document.getElementById("defaultAvatar2"); + document.body.appendChild(session.avatar); + session.avatar.ready = false; + session.avatar.onload = () => { + session.avatar.ready = true; + getById("noAvatarSelected3").classList.remove("selected"); + getById("noAvatarSelected").classList.remove("selected"); + getById("defaultAvatar1").classList.add("selected"); + getById("defaultAvatar2").classList.add("selected"); + updateRenderOutpipe(); + session.avatar.classList.add("hidden"); + }; + session.avatar.onerror = () => { + session.avatar.classList.add("hidden"); + } + if (session.avatar.complete) { + session.avatar.ready = true; + getById("noAvatarSelected3").classList.remove("selected"); + getById("noAvatarSelected").classList.remove("selected"); + getById("defaultAvatar1").classList.add("selected"); + getById("defaultAvatar2").classList.add("selected"); + } + } else if (avatar) { + try { + avatar = decodeURIComponent(avatar); + } catch (e) {} + + session.avatar = getById("defaultAvatar2"); + document.body.appendChild(session.avatar); + session.avatar.ready = false; + session.avatar.onload = () => { + session.avatar.ready = true; + getById("noAvatarSelected3").classList.remove("selected"); + getById("noAvatarSelected").classList.remove("selected"); + getById("defaultAvatar1").classList.add("selected"); + getById("defaultAvatar2").classList.add("selected"); + updateRenderOutpipe(); + session.avatar.classList.add("hidden"); + }; + session.avatar.onerror = () => { + session.avatar.classList.add("hidden"); + } + getById("defaultAvatar1").src = avatar; + getById("defaultAvatar2").src = avatar; + } else { + getById("avatarDiv3").classList.remove("hidden"); + getById("avatarDiv").classList.remove("hidden"); + } + if (session.disableBackground!==false){ + session.disableBackground = true; + } + } + + if (session.disableBackground){ + document.documentElement.style.setProperty("--video-background-image", "unset"); + } + + if (urlParams.has("prompt") || urlParams.has("validate") || urlParams.has("approve")) { + session.promptAccess = true; + } + + if (urlParams.has("js")) { + // ie: &js=https%3A%2F%2Fvdo.ninja%2Fexamples%2Ftestjs.js + try { + var jsURL = urlParams.get("js"); + try { + jsURL = decodeURI(jsURL); + } catch(e){ + warnlog(e); + } + log(jsURL); + + const jsUrlObj = new URL(jsURL); + const jsDomain = jsUrlObj.hostname; + let allow = false; + try { + if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ + console.error("For security and privacy purposes, Javascript injection using Invite Cam must be consented to."); + if (!session.cleanOutput){ + allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); + } + } else if ((window.location.hostname === jsDomain) || window.location.hostname.endsWith("."+jsDomain) || (window !== window.top) || session.studioSoftware) { + // same domains, iframes, or OBS can run javascript. + allow = true; + if (window.location.hostname !== jsDomain){ + console.warn("Third-party Javascript has been injected into the code. Security cannot be ensured."); + } + } else if (!session.cleanOutput){ + // to allow flexibility, we will allow it if the user consents + allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click cancel.", true); + } + } catch(e){ + allow = true; + warnlog(e); + } + + if (allow){ + // type="text/javascript" crossorigin="anonymous" + let externalJavaascript = document.createElement("script"); + externalJavaascript.type = "text/javascript"; + externalJavaascript.crossorigin = "anonymous"; + externalJavaascript.src = jsURL; + externalJavaascript.onerror = function () { + warnlog("Third-party Javascript failed to load"); + }; + externalJavaascript.onload = function () { + log("Third-party Javascript loaded"); + }; + document.head.appendChild(externalJavaascript); + } else { + console.error("For security/privacy purposes, Javascript injection is now only allowed if used within an IFRAME or if the JS file is hosted on the same domain."); + } + } catch(e){ + errorlog(e); + } + } + + if (urlParams.has("base64js") || urlParams.has("b64js") || urlParams.has("jsbase64") || urlParams.has("jsb64")) { + try { + let allow = false; + try { + if (["invite.cam","invitecamera.com"].includes(getParentHostname())){ + console.error("For security and privacy purposes, Javascript injection using Invite Cam must be consented to."); + if (!session.cleanOutput){ + allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); + } + } else if ((window !== window.top) || session.studioSoftware) { + // iframes or OBS can run javascript. + allow = true; + console.warn("Third-party Javascript has been injected into the code. Security cannot be ensured."); + } else if (!session.cleanOutput){ + // to allow flexibility, we will allow it if the user consents + allow = await confirmAlt("This link wishes to inject third-party Javascript ⚠️\n\nIf you trust the link, click OK. Otherwise, click Cancel.", true); + } + } catch(e){ + warnlog(e); + allow = true; + } + + if (allow){ + var base64js = urlParams.get("base64js") || urlParams.get("b64js") || urlParams.get("jsbase64") || urlParams.get("jsb64"); + base64js = decodeURIComponent(atob(base64js)); // window.btoa(encodeURIComponent("alert('hi')")); // ?jsb64=YWxlcnQoJ2hpJyk7 + var externalJavaascript = document.createElement("script"); + externalJavaascript.type = "text/javascript"; + externalJavaascript.crossorigin = "anonymous"; + externalJavaascript.innerHTML = base64js; + externalJavaascript.onerror = function () { + errorlog("Third-party Javascript failed to load"); + }; + externalJavaascript.onload = function () { + log("Third-party Javascript loaded"); + }; + document.head.appendChild(externalJavaascript); + } else { + console.error("For security/privacy purposes, Javascript B64 injection is now only allowed if used within an IFRAME."); + } + } catch (e) { + console.error(e); + } + } + + session.sitePassword = session.defaultPassword; + if (urlParams.has("password") || urlParams.has("pass") || urlParams.has("pw") || urlParams.has("p") || (session.password===null)) { + session.password = urlParams.get("password") || urlParams.get("pass") || urlParams.get("pw") || urlParams.get("p") || null; + + if (!session.password) { + window.focus(); + session.password = await promptAlt(getTranslation("enter-password"), true, true); + if (session.password) { + session.password = session.password.trim(); + } + } else if (session.password === "false") { + session.password = false; + session.defaultPassword = false; + } else if (session.password === "0") { + session.password = false; + session.defaultPassword = false; + } else if (session.password === "off") { + session.password = false; + session.defaultPassword = false; + } else { + try { + session.password = decodeURIComponent(session.password); // will be re-encoded in a moment. + } catch (e) { + errorlog(e); + } + } + } else if (urlParams.has("nopassword") || urlParams.has("nopass") || urlParams.has("nopw") || urlParams.has("p0")) { + session.password = false; + session.defaultPassword = false; + } + + if (session.password) { + getById("passwordRoom").value = session.password; + session.password = sanitizePassword(session.password); + session.defaultPassword = false; + getById("addPasswordBasic").style.display = "none"; + } + + if (urlParams.has("salt") && urlParams.get("salt")) { + session.salt = urlParams.get("salt"); + } + + if (urlParams.has("showconnections")) { + session.showConnections = true; // shows remote guest connections as a stat + } + + if (urlParams.has("hash") || urlParams.has("crc") || urlParams.has("check")) { + // could be brute forced in theory, so not as safe as just not using a hash check. + session.taintedSession = null; // waiting to see if valid or not. + var hash_input = urlParams.get("hash") || urlParams.get("crc") || urlParams.get("check"); + if (hash_input){ + hash_input = hash_input.trim(); // yes. really. + } + if (session.password === false) { + window.focus(); + session.password = await promptAlt(getTranslation("enter-password-2"), true, true); + session.password = sanitizePassword(session.password); + getById("passwordRoom").value = session.password; + session.defaultPassword = false; + } + + generateHash(session.password + session.salt, 6) + .then(function (hash) { + // million to one error. I won't + log("hash is " + hash); + if (hash.substring(0, hash_input.length) !== hash_input) { + // this hash crc check is usually just the first 4 characters, but i'll match based on whatever is provided; + // max 6 length for security. 2 could be a good option for better security but more than 6 is too big of a security concern. + generateHash(session.password + "obs.ninja", 6) + .then(function (hash2) { + // million to one error; this is to support a legacy salt used. Depreciated, and will be removed eventually + log("hash2 is " + hash2); + if (hash2.substring(0, 4) !== hash_input) { + // this legacy hash crc checks is always 4 characters + session.taintedSession = true; + if (!session.cleanOutput) { + miniTranslate(getById("request_info_prompt"), "password-incorrect"); + //getById("request_info_prompt").innerHTML = getTranslation("password-incorrect"); + getById("request_info_prompt").style.display = "block"; + getById("mainmenu").style.display = "none"; + getById("head1").style.display = "none"; + session.cleanOutput = true; + } else { + getById("request_info_prompt").innerHTML = ""; + getById("request_info_prompt").style.display = "block"; + getById("mainmenu").style.display = "none"; + getById("head1").style.display = "none"; + } + } else { + session.taintedSession = false; + session.hash = hash; + } + }) + .catch(errorlog); + } else { + session.taintedSession = false; + session.hash = hash; + } + }) + .catch(errorlog); + } + + if (session.defaultPassword !== false) { + session.password = session.defaultPassword; // no user entered password; let's use the default password if its not disabled. + } + + if (urlParams.has("showlabels") || urlParams.has("showlabel") || urlParams.has("sl")) { + session.showlabels = urlParams.get("showlabels") || urlParams.get("showlabel") || urlParams.get("sl") || ""; + session.showlabels = sanitizeLabel(session.showlabels.replace(/[\W]+/g, "_").replace(/_+/g, "_")); + //session.style = 6; + + if (session.showlabels == "") { + session.labelstyle = false; + } else { + session.labelstyle = session.showlabels; + } + + session.showlabels = true; + session.manual = session.manual === null ? false : session.manual; + session.windowed = session.windowed === null ? false : session.windowed; + } + + if (urlParams.has("showmeta")){ + session.showmeta = true; + } + + if (urlParams.has("sizelabel") || urlParams.has("labelsize") || urlParams.has("fontsize")) { + session.labelsize = urlParams.get("sizelabel") || urlParams.get("labelsize") || urlParams.get("fontsize") || 100; + session.labelsize = parseInt(session.labelsize); + } + + if (urlParams.has("label") || urlParams.has("l")) { + session.label = urlParams.get("label") || urlParams.get("l") || null; + var updateURLAsNeed = true; + if (session.label == null || session.label.length == 0) { + window.focus(); + session.label = await promptAlt(getTranslation("enter-display-name"), true); + } else { + var updateURLAsNeed = false; + try { + session.label = decodeURIComponent(session.label); + } catch (e) { + errorlog(e); + } + session.label = session.label.replace(/_/g, " "); + } + if (session.label != null) { + session.label = sanitizeLabel(session.label); // alphanumeric was too strict. + document.title = session.label; // what the result is. + + if (updateURLAsNeed) { + var label = encodeURIComponent(session.label); + if (urlParams.has("l")) { + updateURL("l=" + label, true, false); + } else { + updateURL("label=" + label, true, false); + } + } + } + } else if (urlParams.has("defaultlabel") || urlParams.has("labelsuggestion") || urlParams.has("ls")) { + session.label = urlParams.get("defaultlabel") || urlParams.get("labelsuggestion") || urlParams.get("ls") || null; + var updateURLAsNeed = true; + window.focus(); + var label = await promptAlt(getTranslation("enter-display-name"), true); + if (label) { + session.label = sanitizeLabel(label); // alphanumeric was too strict. + } else { + session.label = sanitizeLabel(session.label); + updateURLAsNeed = false; + } + + document.title = session.label; // what the result is. + + if (updateURLAsNeed) { + var label = encodeURIComponent(session.label); + if (urlParams.has("l")) { + updateURL("l=" + label, true, false); + } else { + updateURL("label=" + label, true, false); + } + } + } + + if (session.label){ + pokeIframeAPI("this-label", session.label); + } + + if (urlParams.has("resources")) { + session.allowResources = urlParams.get("resources") ? urlParams.get("resources").split(",") : true; + } + + if (urlParams.has("meta")) { + session.meta = {}; + const metaFields = urlParams.get("meta").split(","); + + const metaTemplates = { + pronouns: { + id: "1", + label: "Preferred Pronouns", + type: "select", + options: [ + "she/her", + "he/him", + "they/them", + "ze/zir", + "she/they", + "he/they", + "[Custom]" + ], + placeholder: "Select or enter custom pronouns" + }, + title: { + id: "2", + label: "Title/Role", + type: "text", + placeholder: "Indie Developer" + }, + twitter: { + id: "3", + label: "X.com username", + type: "url", + placeholder: "https://x.com/username" + }, + instagram: { + id: "4", + label: "Instagram", + type: "url", + placeholder: "@username" + }, + youtube: { + id: "5", + label: "YouTube Channel", + type: "url", + placeholder: "https://youtube.com/c/channel" + }, + twitch: { + id: "5", + label: "Twitch Channel", + type: "url", + placeholder: "https://www.twitch.tv/username" + }, + bio: { + id: "6", + label: "Short Bio", + type: "textarea", + placeholder: "Tell us about yourself" + }, + location: { + id: "7", + label: "Location", + type: "text", + placeholder: "Toronto, Canada" + }, + avatar: { + id: "8", + label: "Profile Avatar", + type: "file", + accept: "image/jpeg, image/jpg, image/png, image/webp", + placeholder: "Upload an avatar image" + }, + qr: { + id: "9", + label: "QR Code", + type: "file", + accept: "image/jpeg, image/jpg, image/png, image/webp", + placeholder: "Upload a QR Code" + }, + }; + + // Gather meta field data after display name + for (const fieldId of metaFields) { + const [templateName, field] = Object.entries(metaTemplates) + .find(([_, t]) => t.id === fieldId) || []; + + if (field) { + field.templateName = templateName; + const value = await promptAlt(field.label, true, false, false, false, false, false, field); + if (value) { + session.meta[templateName] = { // Use templateName (qr) instead of field.id (9) + type: field.type, + label: field.label, + templateName, + value, + id: field.id + }; + } + } + } + } + + if (urlParams.has("transparent") || urlParams.has("transparency")) { + // sets the window to be transparent - useful for IFRAMES? + session.transparent = true; + } + + if (urlParams.has("slider") || urlParams.has("showslider")) { + session.showSlider = true; + } + + if (session.transparent) { + getById("main").style.backgroundColor = "rgba(0,0,0,0)"; + document.documentElement.style.setProperty("--container-color", "#0000"); + document.documentElement.style.setProperty("--background-color", "#0000"); + document.documentElement.style.setProperty("--regular-margin", "0"); + document.documentElement.style.setProperty("--director-margin", "0 25px 0 0"); + document.documentElement.style.setProperty("--discord-grey-1a", "#0000"); + getById("directorLinksButton").style.color = "black"; + getById("main").style.overflow = "hidden"; + } + + // Low-latency mode: optimizes all settings for minimum latency + // These are defaults that can be overridden by other URL parameters + if (urlParams.has("lowlatency") || urlParams.has("ll") || urlParams.has("ultralow")) { + log("LOW LATENCY MODE ENABLED"); + + // Audio processing off (adds ~30-100ms latency) + session.echoCancellation = false; + session.autoGainControl = false; + session.noiseSuppression = false; + + // Minimum packet time (10ms vs default 20ms) + if (session.ptime === false) { + session.ptime = 10; + } + if (session.minptime === false) { + session.minptime = 10; + } + if (session.maxptime === false) { + session.maxptime = 20; // don't let it grow too large + } + + // Disable FEC (adds bandwidth overhead and ~latency for recovery) + if (!urlParams.has("fec")) { + session.noFEC = true; + } + + // CBR for predictable timing (better for real-time than VBR) + if (!urlParams.has("vbr") && session.cbr !== 0) { + session.cbr = 1; + } + + // Minimum jitter buffer on receiver + if (session.buffer === false && !urlParams.has("buffer")) { + session.buffer = 0; + } + if (session.audioBuffer === false && !urlParams.has("audiobuffer")) { + session.audioBuffer = 0; + } + + // Disable A/V sync compensation (adds delay) + if (session.sync === false && !urlParams.has("sync")) { + session.sync = 0; + } + + // Disable WebAudio processing pipeline (reduces CPU and latency) + if (!urlParams.has("ap")) { + session.disableWebAudio = true; + session.disableViewerWebAudioPipeline = true; + session.audioEffects = false; + session.audioMeterGuest = false; + if (session.noisegate === null) { + session.noisegate = false; + } + } + + // Lower audio latency hint for AudioContext + if (session.audioLatency === false && !urlParams.has("latency")) { + session.audioLatency = 10; // 10ms latency hint + } + } + + if (urlParams.has("stereo") || urlParams.has("s") || urlParams.has("proaudio")) { + // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono + log("STEREO ENABLED"); + session.stereo = urlParams.get("stereo") || urlParams.get("s") || urlParams.get("proaudio"); + + if (session.stereo) { + session.stereo = session.stereo.toLowerCase(); + } + + //var supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + //supportedConstraints.channelCount; + + if (session.stereo === "false") { + session.stereo = 0; + session.audioInputChannels = 1; + } else if (session.stereo === "0") { + session.stereo = 0; + session.audioInputChannels = 1; + } else if (session.stereo === "no") { + session.stereo = 0; + session.audioInputChannels = 1; + } else if (session.stereo === "off") { + session.stereo = 0; + session.audioInputChannels = 1; + } else if (session.stereo === "1") { + session.stereo = 1; + } else if (session.stereo === "both") { + session.stereo = 1; + } else if (session.stereo === "3") { + session.stereo = 3; + } else if (session.stereo === "out") { + session.stereo = 3; + } else if (session.stereo === "mono") { + session.stereo = 3; + session.audiobitrate = 128; + } else if (session.stereo === "4") { + session.stereo = 4; + } else if (session.stereo === "multi") { + session.stereo = 4; + } else if (session.stereo === "8") { + session.stereo = 8; + } else if (session.stereo === "surround") { + session.stereo = 8; + } else if (session.stereo === "2") { + session.stereo = 2; + } else if (session.stereo === "6") { + session.stereo = 6; + } else if (session.stereo === "in") { + session.stereo = 2; + } else { + session.stereo = 5; // guests; no stereo in, no high bitrate in, but otherwise like stereo=1 + } + + getById("whipoutstereo").classList.add("hidden"); + } + + if (urlParams.has("screensharestereo") || urlParams.has("sss") || urlParams.has("ssproaudio")) { + // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono + log("screenshare stereo ENABLED"); + session.screenshareStereo = urlParams.get("screensharestereo") || urlParams.get("sss") || urlParams.get("ssproaudio"); + + if (session.screenshareStereo) { + session.screenshareStereo = session.screenshareStereo.toLowerCase(); + } + + if (session.screenshareStereo === "false") { + session.screenshareStereo = 0; + } else if (session.screenshareStereo === "0") { + session.screenshareStereo = 0; + } else if (session.screenshareStereo === "no") { + session.screenshareStereo = 0; + } else if (session.screenshareStereo === "off") { + session.screenshareStereo = 0; + } else if (session.screenshareStereo === "1") { + session.screenshareStereo = 1; + } else if (session.screenshareStereo === "both") { + session.screenshareStereo = 1; + } else if (session.screenshareStereo === "3") { + session.screenshareStereo = 3; + } else if (session.screenshareStereo === "out") { + session.screenshareStereo = 3; + } else if (session.screenshareStereo === "mono") { + session.screenshareStereo = 3; + } else if (session.screenshareStereo === "4") { + session.screenshareStereo = 4; + } else if (session.screenshareStereo === "multi") { + session.screenshareStereo = 4; + } else if (session.screenshareStereo === "2") { + session.screenshareStereo = 2; + } else if (session.screenshareStereo === "in") { + session.screenshareStereo = 2; + } else { + session.screenshareStereo = 5; // guests; no stereo in, no high bitrate in, but otherwise like stereo=1 + } + } + + // Deploy your own handshake server for free; see: https://github.com/steveseguin/websocket_server + if (urlParams.has("pie")) { + // piesocket.com support is to be deprecated after dec/19/21, since piesocket is no longer a free service. + session.customWSS = urlParams.get("pie") || true; // If session.customWSS == true, then there is no need to set parameters via URL + session.wssSetViaUrl = true; + if (session.customWSS && session.customWSS !== true) { + session.wss = "wss://free3.piesocket.com/v3/1?api_key=" + session.customWSS; // if URL param is set, it will use the API key. + } + } + + if ((Firefox && !session.stereo) || session.stereo === 3) { + session.mono = true; // this will set the SDP to mono if firefox + } + + if (urlParams.has("mono")) { + session.mono = true; + if (session.stereo == 1 || session.stereo == 4) { + session.stereo = 3; + session.audiobitrate = 128; + } else if (session.stereo == 5) { + session.stereo = 3; // stereo out only + session.audiobitrate = 128; + } else if (session.stereo == 2) { + session.stereo = 0; + session.audiobitrate = 128; + } + } + + if (session.stereo == 1 || session.stereo == 3 || session.stereo == 4 || session.stereo == 5) { + session.echoCancellation = false; + session.autoGainControl = false; + session.noiseSuppression = false; + } + + if (urlParams.has("channelcount") || urlParams.has("ac") || urlParams.has("inputchannels")) { + // if updates to this, see also function toggleMonoStereoMic() + session.audioInputChannels = urlParams.get("channelcount") || urlParams.get("ac") || urlParams.get("inputchannels") || 0; + session.audioInputChannels = parseInt(session.audioInputChannels); + if (!session.audioInputChannels) { + session.audioInputChannels = false; + } + } else if (urlParams.has("monomic")) { + session.audioInputChannels = 1; + } + + if (session.stereo === 5 && !session.audioInputChannels) { + // allow the guest to set their mic to mono. + document.querySelectorAll(".gear_microphone").forEach(ele => { + ele.classList.remove("hidden"); + }); + } + + if (urlParams.has("echocancellation") || urlParams.has("aec") || urlParams.has("ec")) { + session.echoCancellation = urlParams.get("echocancellation") || urlParams.get("aec") || urlParams.get("ec"); + + if (session.echoCancellation) { + session.echoCancellation = session.echoCancellation.toLowerCase(); + } + if (session.echoCancellation == "false") { + session.echoCancellation = false; + } else if (session.echoCancellation == "0") { + session.echoCancellation = false; + } else if (session.echoCancellation == "no") { + session.echoCancellation = false; + } else if (session.echoCancellation == "off") { + session.echoCancellation = false; + } else { + session.echoCancellation = true; + } + } + + if (urlParams.has("autogain") || urlParams.has("ag") || urlParams.has("agc")) { + session.autoGainControl = urlParams.get("autogain") || urlParams.get("ag") || urlParams.get("agc"); + if (session.autoGainControl) { + session.autoGainControl = session.autoGainControl.toLowerCase(); + } + if (session.autoGainControl == "false") { + session.autoGainControl = false; + } else if (session.autoGainControl == "0") { + session.autoGainControl = false; + } else if (session.autoGainControl == "no") { + session.autoGainControl = false; + } else if (session.autoGainControl == "off") { + session.autoGainControl = false; + } else { + session.autoGainControl = true; + } + } + + if (urlParams.has("denoise") || urlParams.has("dn")) { + session.noiseSuppression = urlParams.get("denoise") || urlParams.get("dn"); + + if (session.noiseSuppression) { + session.noiseSuppression = session.noiseSuppression.toLowerCase(); + } + if (session.noiseSuppression == "false") { + session.noiseSuppression = false; + } else if (session.noiseSuppression == "0") { + session.noiseSuppression = false; + } else if (session.noiseSuppression == "no") { + session.noiseSuppression = false; + } else if (session.noiseSuppression == "off") { + session.noiseSuppression = false; + } else { + session.noiseSuppression = true; + } + } + + if (urlParams.has("isolation") || urlParams.has("voiceisolation") || urlParams.has("vi")) { + session.voiceIsolation = urlParams.get("isolation") || urlParams.get("voiceisolation") || urlParams.get("vi"); + + if (session.voiceIsolation) { + session.voiceIsolation = session.voiceIsolation.toLowerCase(); + } + if (session.voiceIsolation == "false") { + session.voiceIsolation = false; + } else if (session.voiceIsolation == "0") { + session.voiceIsolation = false; + } else if (session.voiceIsolation == "no") { + session.voiceIsolation = false; + } else if (session.voiceIsolation == "off") { + session.voiceIsolation = false; + } else { + session.voiceIsolation = true; + } + } + + if (session.voiceIsolation !== null) { + getById("whipoutvoiceisolation").classList.add("hidden"); + } + if (session.noiseSuppression !== null) { + getById("whipoutdenoise").classList.add("hidden"); + } + if (session.autoGainControl !== null) { + // should be the last + getById("whipoutautogain").classList.add("hidden"); + } + + if (urlParams.has("screenshareaec") || urlParams.has("ssec") || urlParams.has("ssaec")) { + session.screenshareAEC = urlParams.get("screenshareaec") || urlParams.get("ssec") || urlParams.get("ssaec"); + + if (session.screenshareAEC) { + session.screenshareAEC = session.screenshareAEC.toLowerCase(); + } + if (session.screenshareAEC == "false") { + session.screenshareAEC = false; + } else if (session.screenshareAEC == "0") { + session.screenshareAEC = false; + } else if (session.screenshareAEC == "no") { + session.screenshareAEC = false; + } else if (session.screenshareAEC == "off") { + session.screenshareAEC = false; + } else { + session.screenshareAEC = true; + } + } + if (urlParams.has("screenshareautogain") || urlParams.has("ssag") || urlParams.has("ssagc")) { + session.screenshareAutogain = urlParams.get("screenshareautogain") || urlParams.get("ssag") || urlParams.get("ssagc"); + if (session.screenshareAutogain) { + session.screenshareAutogain = session.screenshareAutogain.toLowerCase(); + } + if (session.screenshareAutogain == "false") { + session.screenshareAutogain = false; + } else if (session.screenshareAutogain == "0") { + session.screenshareAutogain = false; + } else if (session.screenshareAutogain == "no") { + session.screenshareAutogain = false; + } else if (session.screenshareAutogain == "off") { + session.screenshareAutogain = false; + } else { + session.screenshareAutogain = true; + } + } + if (urlParams.has("screensharedenoise") || urlParams.has("ssdn")) { + session.screenshareDenoise = urlParams.get("screensharedenoise") || urlParams.get("ssdn"); + + if (session.screenshareDenoise) { + session.screenshareDenoise = session.screenshareDenoise.toLowerCase(); + } + if (session.screenshareDenoise == "false") { + session.screenshareDenoise = false; + } else if (session.screenshareDenoise == "0") { + session.screenshareDenoise = false; + } else if (session.screenshareDenoise == "no") { + session.screenshareDenoise = false; + } else if (session.screenshareDenoise == "off") { + session.screenshareDenoise = false; + } else { + session.screenshareDenoise = true; + } + } + + if (urlParams.has("roombitrate") || urlParams.has("roomvideobitrate") || urlParams.has("rbr")) { + log("Room BITRATE SET"); + session.roombitrate = urlParams.get("roombitrate") || urlParams.get("rbr") || urlParams.get("roomvideobitrate"); + session.roombitrate = parseInt(session.roombitrate); + if (session.roombitrate < 1) { + session.roombitrate = 0; + } + } + + if (urlParams.has("outboundaudiobitrate") || urlParams.has("oab")) { + session.outboundAudioBitrate = parseInt(urlParams.get("outboundaudiobitrate")) || parseInt(urlParams.get("oab")) || false; + } + if (urlParams.has("outboundvideobitrate") || urlParams.has("outboundbitrate") || urlParams.has("ovb")) { + session.outboundVideoBitrate = parseInt(urlParams.get("outboundvideobitrate")) || parseInt(urlParams.get("outboundbitrate")) || parseInt(urlParams.get("ovb")) || false; + session.outboundVideoBitrate_userSet = true; + } else if (session.outboundVideoBitrate!==false){ + session.outboundVideoBitrate_userSet = true; + } + + if (urlParams.has("webp") || urlParams.has("images")) { + // deprecicating this. chunked mode will replace it. + session.webp = urlParams.get("webp") || urlParams.get("images") || "webp"; + } + + if (urlParams.has("webpquality") || urlParams.has("webpq") || urlParams.has("wq")) { + session.webPquality = parseInt(urlParams.get("webpquality")) || parseInt(urlParams.get("webpq")) || parseInt(urlParams.get("wq")) || 4; + } + + if (urlParams.has("audiobitrate") || urlParams.has("ab")) { + // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono + log("AUDIO BITRATE SET"); + session.audiobitrate = urlParams.get("audiobitrate") || urlParams.get("ab"); + session.audiobitrate = parseInt(session.audiobitrate); + if (session.audiobitrate < 1) { + session.audiobitrate = false; + } else if (session.audiobitrate > 510) { + session.audiobitrate = 510; + } // this is to just prevent abuse + } + if (iOS || iPad) { + session.audiobitrate = false; // iOS devices seem to get distortion with custom audio bitrates. Disable for now. + } + + /* if (urlParams.has('whitebalance') || urlParams.has('temp')){ // Need to be applied after the camera is selected. bleh. not enforcible. remove for now. + var temperature = urlParams.get('whitebalance') || urlParams.get('temp'); + try{ + updateCameraConstraints('colorTemperature', parseFloat(temperature)); + } catch (e){errorlog(e);} + } */ + + if (urlParams.has("streamid") || urlParams.has("view") || urlParams.has("v") || urlParams.has("V") ||urlParams.has("pull")) { + // the streams we want to view; if set, but let blank, we will request no streams to watch. + session.view = urlParams.get("streamid") || urlParams.get("view") || urlParams.get("v") || urlParams.get("V") || urlParams.get("pull") || null; // this value can be comma seperated for multiple streams to pull + + getById("headphonesDiv2").classList.remove("hidden"); + getById("headphonesDiv").classList.remove("hidden"); + + getById("addPasswordBasic").style.display = "none"; + + if (session.view == null) { + session.view = ""; + } + + /* if (session.view){ + if (urlParams.has('include') && urlParams.get('include')){ + session.view += ","+urlParams.get('include'); + } + } */ + if (session.scene !== false && session.style === false && session.studioSoftware) { + session.style = 1; + } + } + // https://vdo.ninja/?fakeguests=10&room=faketestroom123&scene&border=10&padding=20&rounded + // https://vdo.ninja/?fakeusers=10&scene&room=test12342345ff + + if (urlParams.has("fakeguests") || urlParams.has("fakefeeds") || urlParams.has("fakeusers")) { + var total = parseInt(urlParams.get("fakeguests")) || parseInt(urlParams.get("fakefeeds")) || parseInt(urlParams.get("fakeusers")) || 4; + session.fakeFeeds = []; + log("Creating " + total + " fake feeds"); + for (var i = 0; i < total; i++) { + let fakeElement = document.createElement("video"); + fakeElement.autoplay = true; + fakeElement.loop = true; + fakeElement.muted = true; + fakeElement.src = "./media/fakesteve.webm"; + fakeElement.labelText = "Fake Steve\\n@steveseguin\\nhe\/him"; + fakeElement.dataset.sid = fakeElement.id = parseInt(Math.random() * 10000000000); + session.fakeFeeds.push(fakeElement); + } + if ((session.view!==false) || session.whipView || session.scene !== false || session.whepInput) { + setTimeout(function () { + updateMixer(); + }, 1000); + } + } + + if (urlParams.has("directoronly") || urlParams.has("directorsonly") || urlParams.has("do")) { + session.viewDirectorOnly = true; + } + + if (session.view !== false) { + session.view_set = session.view.split(","); + } + + if (session.view_set) { + session.allowScreen = []; + session.allowVideos = []; + var i = session.view_set.length; + while (i--) { + var split = session.view_set[i].split(":s"); + if (split.length > 1) { + session.allowScreen.push(split[0]); + session.view_set.splice(i, 1); + if (!(split[0] in session.view_set)) { + session.view_set.push(split[0]); + } + } else if (split[0]) { + session.allowVideos.push(split[0]); + } else { + session.view_set.splice(i, 1); + } + } + } + + if (urlParams.has("include") && urlParams.get("include")) { + urlParams.get("include") + .split(",") + .forEach(sid => { + var sidd = sid.split(":s")[0]; + if (sidd && !session.include.includes(sidd)) { + session.include.push(sidd); + } + }); + } + + if (urlParams.get("waitpage")) { + session.waitPage = urlParams.get("waitpage"); + } + + if (urlParams.has("waitice")) { + session.waitForCandidates = true; + } + + if (urlParams.has("directorview") || urlParams.has("dv")) { + session.directorView = true; + } + if (urlParams.has("graphs")) { + session.allowGraphs = true; + } + + if (urlParams.has("ruler") || urlParams.has("grid") || urlParams.has("thirds")) { + session.fullscreen = true; + if (!session.manual) { + session.manual = session.manual === null ? false : session.manual; + } + session.ruleOfThirds = urlParams.get("ruler") || urlParams.get("grid") || urlParams.get("thirds") || "./media/thirds.svg"; + session.ruleOfThirds = decodeURIComponent(session.ruleOfThirds); + } + + if (urlParams.has("smallshare") || urlParams.has("smallscreen")) { + session.notifyScreenShare = false; + } + + if (urlParams.has("proxy")) { + // routes the wss traffic via an alternative network path. Not + session.proxy = true; // only works if session.wss is set to false + } else if (location.hostname === "proxy.vdo.ninja") { + session.proxy = true; + } + + if (urlParams.has("nopreview") || urlParams.has("np")) { + log("preview OFF"); + session.nopreview = true; + if (iOS || iPad) { + session.nopreview = false; + session.minipreview = 3; // + } + } else if (urlParams.has("minipreview") || urlParams.has("mini")) { + var mini = urlParams.get("minipreview") || urlParams.get("mini"); // 2 is a valid option. (3 is for iPhone with a hidden preview) + + if (mini === "0") { + mini = false; + } else if (mini) { + mini = parseInt(mini); + } else { + mini = 1; + } + log("preview ON"); + session.nopreview = false; + session.minipreview = mini; + if (session.manual === null) { + session.manual = session.manual === null ? false : session.manual; + } + } else if (urlParams.has("largepreview")) { + session.nopreview = false; + session.minipreview = false; + if (session.manual === null) { + session.manual = session.manual === null ? false : session.manual; + } + } else if (urlParams.has("preview") || urlParams.has("showpreview")) { + log("preview ON"); + if (session.manual === null) { + session.manual = session.manual === null ? false : session.manual; + } + session.nopreview = false; + } + + if (urlParams.has("minipreviewoffset") || urlParams.has("mpo")) { + // 40 would be centered + session.leftMiniPreview = urlParams.get("minipreviewoffset") || urlParams.get("mpo") || 0; + session.leftMiniPreview = parseInt(session.leftMiniPreview) || 0; + if (session.leftMiniPreview < -20) { + session.leftMiniPreview = -20; + } else if (session.leftMiniPreview > 120) { + session.leftMiniPreview = 120; + } + } + + if (urlParams.has("obsfix")) { + session.obsfix = urlParams.get("obsfix"); + if (session.obsfix) { + session.obsfix = session.obsfix.toLowerCase(); + } + if (session.obsfix == "false") { + session.obsfix = false; + } else if (session.obsfix == "0") { + session.obsfix = false; + } else if (session.obsfix == "no") { + session.obsfix = false; + } else if (session.obsfix == "off") { + session.obsfix = false; + } else if (parseInt(session.obsfix) > 0) { + session.obsfix = parseInt(session.obsfix); + } else { + session.obsfix = 1; // aggressive. + } + } + + if (urlParams.has("controlroombitrate") || urlParams.has("crb")) { + session.controlRoomBitrate = true; + } + + if (urlParams.has("minroombitrate") || urlParams.has("mrb")) { + session.minimumRoomBitrate = urlParams.get("minroombitrate") || urlParams.get("mrb") || false; + session.minimumRoomBitrate = parseInt(session.minimumRoomBitrate) || false; + } + + if (urlParams.has("remote") || urlParams.has("rem")) { + log("remote ENABLED"); + session.remote = urlParams.get("remote") || urlParams.get("rem") || true; + } + + if (urlParams.has("slideshow")) { + // stream labs mobile fix ? + var ssinterval = parseInt(urlParams.get("slideshow")) || 25; + ssinterval = 1000 / ssinterval; + session.manual = session.manual === null ? true : session.manual; + session.dynamicScale = false; + setInterval(function () { + try { + slideshowHack(); + } catch (e) { + errorlog(e); + } + }, ssinterval); + } + + if (urlParams.has("latency") || urlParams.has("al") || urlParams.has("audiolatency")) { + log("latency ENABLED"); + session.audioLatency = urlParams.get("latency") || urlParams.get("al") || urlParams.get("audiolatency"); + session.audioLatency = parseInt(session.audioLatency) || 0; + session.disableWebAudio = false; + } + + if (urlParams.has("micdelay") || urlParams.has("delay") || urlParams.has("md")) { + log("audio gain ENABLED"); + session.micDelay = urlParams.get("micdelay") || urlParams.get("delay") || urlParams.get("md") || 0; + session.micDelay = parseInt(session.micDelay) || 0; + session.disableWebAudio = false; + } + if (urlParams.has("tips")) { const guestTips = getById("guestTips"); if (guestTips) { @@ -3702,5702 +3789,5702 @@ async function main() { guestTips.style.display = "flex"; } } - - if (urlParams.has("audiogain") || urlParams.has("gain") || urlParams.has("g") || urlParams.has("muteguest")) { - log("audio gain ENABLED"); - session.audioGain = urlParams.get("audiogain") || urlParams.get("gain") || urlParams.get("g") || 0; - session.audioGain = parseInt(session.audioGain) || 0; - session.disableWebAudio = false; - } - if (urlParams.has("volume") || urlParams.has("vol")) { - // This sets the default volume for all new video playback elements; 0 to 100. - log("setting default volume for playback"); - session.volume = urlParams.get("volume") || urlParams.get("vol") || 100; - session.volume = parseInt(session.volume) || 0; - session.volume = session.volume / 100; // 0 to 1.0 - } - if (urlParams.has("compressor") || urlParams.has("comp")) { - log("audio gain ENABLED"); - session.compressor = 1; - session.disableWebAudio = false; - } else if (urlParams.has("limiter")) { - log("audio gain ENABLED"); - session.compressor = 2; - session.disableWebAudio = false; - } - if (urlParams.has("equalizer") || urlParams.has("eq")) { - session.equalizer = true; - session.disableWebAudio = false; - } - // Mic panning (publisher-side): downmix to mono, then pan to stereo output. - if (urlParams.has("micpanning") || urlParams.has("mpan")) { - let mp = urlParams.get("micpanning") || urlParams.get("mpan"); - if (mp === null || mp === "" || mp === "true") { - session.micPanning = 90; // center by default, but enable control - } else { - mp = parseInt(mp); - if (isNaN(mp)) { mp = 90; } - if (mp < 0) { mp = 0; } - if (mp > 180) { mp = 180; } - session.micPanning = mp; - } - // Ensure WebAudio outbound pipeline is enabled (unless &noap set later) - session.disableWebAudio = false; - } - if (urlParams.has("lowcut") || urlParams.has("lc") || urlParams.has("higpass")) { - session.lowcut = urlParams.get("lowcut") || urlParams.get("lc") || urlParams.get("higpass") || 100; - session.lowcut = parseInt(session.lowcut); - session.disableWebAudio = false; - } - - if (urlParams.has("pip")) { - session.pip = true; // togglePip - //session.manual=true; - //innerHTML = - } - if (urlParams.has("pip3") || urlParams.has("mypip") || urlParams.has("pipme")) { - session.pip3 = true; - } - - if (urlParams.has("manual")) { - session.manual = true; - } - - if (urlParams.has("keyframeinterval") || urlParams.has("keyframerate") || urlParams.has("keyframe") || urlParams.has("fki")) { - log("keyframeRate ENABLED"); - session.keyframeRate = parseInt(urlParams.get("keyframeinterval") || urlParams.get("keyframerate") || urlParams.get("keyframe") || urlParams.get("fki")) || 0; - } - - if (urlParams.has("obsoff") || urlParams.has("oo") || urlParams.has("disableobs")) { - log("OBS feedback disabled"); - session.disableOBS = true; - getById("obsState").style.setProperty("display", "none", "important"); - } - - if (urlParams.has("hidecodirectors") || urlParams.has("hidecodirector") || urlParams.has("hidedirector") || urlParams.has("hidedirectors") || urlParams.has("hd")) { - document.querySelector(":root").style.setProperty("--show-codirectors", "none", "important"); - session.hideDirector = true; - } - - if (urlParams.has("pptcontrols") || urlParams.has("slides") || urlParams.has("ppt") || urlParams.has("powerpoint")) { - session.pptControls = true; // shows powerpoint controls to remotely control a powerpoint slide. Requires additional remote setup. - } - - if (urlParams.has("allowedscenes")) { - session.filterOBSscenes = urlParams.get("allowedscenes"); - if (session.filterOBSscenes) { - session.filterOBSscenes = session.filterOBSscenes.split(","); - } else { - session.filterOBSscenes = true; - } - } - - if (urlParams.has("tallyoff") || urlParams.has("notally") || urlParams.has("disabletally") || urlParams.has("to")) { - log("Tally Light off"); - getById("obsState").style.setProperty("display", "none", "important"); - } else if (urlParams.has("tally")) { - session.tallyStyle = 1; - session.tallyStyleDefault = 1; - getById("obsState").classList.add("larger"); - } - - if (urlParams.has("automute") || urlParams.has("am")) { - session.automute = urlParams.get("automute") || true; - session.micIsolatedAutoMute = []; // default auto mutes - } - - if (urlParams.has("noobsstream")){ - session.obsState.streaming = false; - } - if (urlParams.has("noobsvirtual")){ - session.obsState.virtualcam = false; - } - if (urlParams.has("noobsrecord")){ - session.obsState.recording = false; - } - if (urlParams.has("noobssourceactive")){ - session.obsState.sourceActive = false; - } - if (urlParams.has("noobsvisibility")){ - session.obsState.visibility = false; - } - - if (window.obsstudio) { - session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? - session.audioMeterGuest = false; - - getById("miniTaskBar").classList.add("hidden"); - - if (session.audioEffects === null) { - session.audioEffects = false; - } - if (window.obsstudio.pluginVersion) { - if (macOS) { - // if mac, no fix - //session.obsfix = false; - } else if (window.obsstudio.pluginVersion == "2.17.4") { - // if obs v27.2 beta, no fix - //session.obsfix = false; - } else { - var ver = window.obsstudio.pluginVersion.split("."); - if (ver.length >= 2) { - if (parseInt(ver[0]) <= 2) { - if (parseInt(ver[0]) == 2) { - if (parseInt(ver[1]) <= 16) { - session.obsfix = 15; - } - } else { - session.obsfix = 15; - } - } - } - } - } - try { - log("OBS VERSION:" + window.obsstudio.pluginVersion); - log("macOS: " + macOS); - log(window.obsstudio); - - if (!urlParams.has("streamlabs")) { - var ver1 = window.obsstudio.pluginVersion.split("."); - - if (ver1.length == 3) { - // Should be 3, but disabled3 - if (ver1.length == 3 && parseInt(ver1[0]) == 2 && ChromiumVersion < 76 && macOS) { - updateURL("streamlabs"); - getById("main").innerHTML = - "

    Update OBS Studio to v26.1.2 or newer; older versions and StreamLabs OBS are not supported on macOS.\ -
    download here:
    https://github.com/obsproject/obs-studio/releases\ -



    \ -

    Please use the Electron Capture app if there are further problems or if you wish to use StreamLabs on macOS still.

    \ -
    You can bypass this error message by refreshing, Clicking Here, or by adding &streamlabs to the URL, but it may still not actually work.\ - \ -
    Please report this problem to steve@seguin.email if you feel it is an error.\ -
    "; - } - } - } - - //if (navigator.userAgent.indexOf('Mac OS X') != -1) { - // session.codec = "h264"; // default the codec to h264 if OBS is on macOS (that's all it supports with hardware) // oct 2021, OBS now supports vp8 and actually breaks with h264 android devices. - //} - - if (session.disableOBS === false) { - - - getOBSDetails(); - - window.addEventListener("obsSceneChanged", obsSceneChanged); - if (session.obsState.visibility!==false){ - window.addEventListener("obsSourceVisibleChanged", obsSourceVisibleChanged); - if (typeof document.visibilityState !== "undefined") { - session.obsState.visibility = document.visibilityState === "visible"; - } - } - if (session.obsState.sourceActive!==false){ - window.addEventListener("obsSourceActiveChanged", obsSourceActiveChanged); - } - if (session.obsState.streaming!==false){ - window.addEventListener("obsStreamingStarted", obsStreamingStarted); - window.addEventListener("obsStreamingStopped", obsStreamingStopped); - } - if (!session.obsState.recording!==false){ - window.addEventListener("obsRecordingStarted", obsRecordingStarted); - window.addEventListener("obsRecordingStopped", obsRecordingStopped); - } - if (session.obsState.virtualcam!==false){ - window.addEventListener("obsVirtualcamStarted", obsVirtualcamStarted); - window.addEventListener("obsVirtualcamStopped", obsVirtualcamStopped); - } - } - } catch (e) { - errorlog(e); - } - } else if (session.studioSoftware){ - session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? - session.audioMeterGuest = false; - - getById("miniTaskBar").classList.add("hidden"); - - if (session.audioEffects === null) { - session.audioEffects = false; - } - } - - if (urlParams.has("chroma")) { - log("Chroma ENABLED"); - getById("main").style.backgroundColor = "#" + (urlParams.get("chroma") || "0F0"); - } - - if (urlParams.has("margin")) { - try { - session.videoMargin = urlParams.get("margin") || 10; - session.videoMargin = parseInt(session.videoMargin); - //document.querySelector(':root').style.setProperty('--video-margin', session.videoMargin+"px"); - } catch (e) { - errorlog("variable css failed"); - } - } - - if (urlParams.has("rounded") || urlParams.has("round")) { - try { - session.borderRadius = urlParams.get("rounded") || urlParams.get("round") || 50; - session.borderRadius = parseInt(session.borderRadius); - document.querySelector(":root").style.setProperty("--video-rounded", session.borderRadius + "px"); - session.manual = session.manual === null ? false : session.manual; - session.windowed = session.windowed === null ? false : session.windowed; - } catch (e) { - errorlog("variable css failed"); - } - } - - if (urlParams.has("border")) { - try { - var videoBorder = urlParams.get("border") || 10; - videoBorder = parseInt(videoBorder); - session.border = videoBorder; - videoBorder += "px"; - document.querySelector(":root").style.setProperty("--video-border-color", "#000"); - document.querySelector(":root").style.setProperty("--video-border", videoBorder); - - session.manual = session.manual === null ? false : session.manual; - session.windowed = session.windowed === null ? false : session.windowed; - } catch (e) { - errorlog("variable css failed"); - } - } - - if (urlParams.has("bordercolor")) { - try { - session.borderColor = urlParams.get("bordercolor") || "#000"; - document.querySelector(":root").style.setProperty("--video-border-color", session.borderColor); - } catch (e) { - errorlog("variable css failed"); - } - } - - if (urlParams.has("holdercolor")) { - try { - session.holderColor = urlParams.get("holdercolor") || session.borderColor || "#000"; - if (parseInt(session.holderColor) == session.holderColor){ - session.holderColor = "#"+session.holderColor; - } - document.querySelector(":root").style.setProperty("--video-holder-color", session.holderColor); - } catch (e) { - errorlog("variable css failed"); - } - } - - if (urlParams.has("color")) { - session.colorVideosBackground = urlParams.get("color") || session.borderColor || "#000"; - } - - if (urlParams.has("retry")) { - session.forceRetry = parseInt(urlParams.get("retry")) || 30; - } - - if (session.forceRetry) { - clearInterval(session.forceRetryTimeout); - session.forceRetryTimeout = setTimeout(function () { - try { - session.retryWatchInterval(); - } catch (e) { - log(e); - clearTimeout(this); - } - }, session.forceRetry * 1000); - } - - if (urlParams.get("dropbox")) { - setupDropbox(urlParams.get("dropbox")).then(() => { - log("Loaded dropbox SDK"); - }).catch(e => errorlog(e)); - } - if (urlParams.has("gdrive")) { - session.gdrive = {}; - } - - try { - if (urlParams.has("darkmode") || urlParams.has("nightmode") || urlParams.has("darktheme")) { - session.darkmode = urlParams.get("darkmode") || urlParams.get("nightmode") || urlParams.get("darktheme") || null; - if (session.darkmode === null || session.darkmode === "") { - session.darkmode = true; - } else if (darkmode == "false" || darkmode == "0" || darkmode == 0 || darkmode == "off") { - session.darkmode = false; - } - } else if (urlParams.has("lightmode") || urlParams.has("lighttheme")) { - session.darkmode = false; - } else if (urlParams.has("whitemode") || urlParams.has("whitetheme")) { - document.body.classList.remove('darktheme'); - document.body.classList.add('whitetheme'); - session.darkmode = false; - } else if (session.studioSoftware) { - session.darkmode = false; // prevent OBS from defaulting to dark mode, avoiding possible overlooked bugs. - } else if (session.darkmode === null) { - session.darkmode = getComputedStyle(document.querySelector(":root")).getPropertyValue("--color-mode").trim(); - if (session.darkmode == "dark") { - session.darkmode = true; - } else { - session.darkmode = false; - } - } - - if (session.darkmode) { - document.body.classList.add("darktheme"); - //document.querySelector(':root').style.setProperty('--background-color',"#02050c" ); - } else { - document.body.classList.remove("darktheme"); - //document.querySelector(':root').style.setProperty('--background-color',"#141926" ); // already set as default. - } - } catch (e) { - errorlog(e); - console.warn("⚠️ If you are seeing this error, it's likely a third-party browser extension is breaking the site\n\nTry a different browser, incognito mode, or disable the problematic extension."); - } - - if (urlParams.has("videodevice") || urlParams.has("vdevice") || urlParams.has("vd") || urlParams.has("device") || urlParams.has("d") || urlParams.has("vdo")) { - session.videoDevice = urlParams.get("videodevice") || urlParams.get("vdevice") || urlParams.get("vd") || urlParams.get("device") || urlParams.get("d") || urlParams.get("vdo"); - - if (session.videoDevice === null) { - session.videoDevice = "1"; - } else if (session.videoDevice) { - session.videoDevice = normalizeDeviceLabel(session.videoDevice); - } - - if (session.videoDevice == "false") { - session.videoDevice = 0; - } else if (session.videoDevice == "0") { - session.videoDevice = 0; - } else if (session.videoDevice == "no") { - session.videoDevice = 0; - } else if (session.videoDevice == "off") { - session.videoDevice = 0; - } else if (session.videoDevice == "snapcam") { - session.videoDevice = "snap_camera"; - } else if (session.videoDevice == "canon") { - session.videoDevice = "eos"; - } else if (session.videoDevice == "camlink") { - session.videoDevice = "cam_link"; - } else if (session.videoDevice == "ndi") { - session.videoDevice = "newtek_ndi_video"; - } else if (session.videoDevice == "") { - session.videoDevice = 1; - } else if (session.videoDevice == "1") { - session.videoDevice = 1; - } else if (session.videoDevice == "default") { - session.videoDevice = 1; - } - - if (!urlParams.has("vdo")) { - getById("videoMenu").style.display = "none"; - getById("videoMenu").classList.add("hidden"); - // getById("videoMenu2").style.display = "none"; - // getById("videoMenu2").classList.add("hidden"); - // getById("videoMenu3").style.display = "none"; - // getById("videoMenu3").classList.add("hidden"); - } - log("session.videoDevice:" + session.videoDevice); - } - - // audioDevice - if (urlParams.has("audiodevice") || urlParams.has("adevice") || urlParams.has("ad") || urlParams.has("device") || urlParams.has("d") || urlParams.has("ado")) { - session.audioDevice = urlParams.get("audiodevice") || urlParams.get("adevice") || urlParams.get("ad") || urlParams.get("device") || urlParams.get("d") || urlParams.get("ado"); - - if (session.audioDevice === null) { - session.audioDevice = "1"; - } else if (session.audioDevice) { - session.audioDevice = normalizeDeviceLabel(session.audioDevice); - } - - if (session.audioDevice == "false") { - session.audioDevice = 0; - } else if (session.audioDevice == "0") { - session.audioDevice = 0; - } else if (session.audioDevice == "no") { - session.audioDevice = 0; - } else if (session.audioDevice == "off") { - session.audioDevice = 0; - } else if (session.audioDevice == "") { - session.audioDevice = 1; - } else if (session.audioDevice == "1") { - session.audioDevice = 1; - } else if (session.audioDevice == "default") { - session.audioDevice = 1; - } else if (session.audioDevice == "ndi") { - session.audioDevice = ["line_newtek_ndi_audio"]; - } else { - session.audioDevice = session.audioDevice.split(","); - } - - getById("headphonesDiv").classList.add("hidden"); - getById("headphonesDiv2").classList.add("hidden"); - - if (typeof session.audioDevice !== "object" && !urlParams.has("ado")) { - getById("audioMenu").style.display = "none"; - getById("audioScreenShare1").style.display = "none"; - getById("audioMenu").classList.add("hidden"); - getById("audioScreenShare1").classList.add("hidden"); - } - - if (session.audioDevice) { - // 0 or false, do not triger - log("requestAudioStream..()"); - try { - await requestAudioStream(); - } catch (e) { - errorlog(e); - } - } - - } - - if (session.videoDevice === 0) { - getById("previewWebcam").classList.add("miconly"); - if (session.audioDevice === 0) { - miniTranslate(getById("add_camera"), "click-start-to-join", "Click Start to Join"); - getById("container-2").className = "column columnfade hidden"; - getById("container-3").classList.add("skip-animation"); - getById("container-3").classList.remove("pointer"); - //delayedStartupFuncs.push([previewWebcam]); - session.webcamonly = true; - } else { - miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); - getById("container-3").classList.add("microphoneBackground"); - } - getById("container-3").title = getById("add_camera").innerText; - } - - if (session.mobile) { - getById("shareScreenGear").style.display = "none"; - getById("dropButton").style.display = "none"; - //getById("container-2").className = 'column columnfade hidden'; // Hide screen share on mobile - session.screensharebutton = false; - screensharesupport = false; - - if (session.audioDevice !== 0) { - getById("flipcamerabutton").classList.remove("hidden"); - } - } - - if (urlParams.has("androidfix")) { - session.AndroidFix = true; - } - - if (urlParams.has("consent")) { - session.consent = true; - getById("consentWarning").classList.remove("hidden"); - getById("consentWarning2").classList.remove("hidden"); - } - - if (urlParams.has("autojoin") || urlParams.has("autostart") || urlParams.has("aj") || urlParams.has("as")) { - session.autostart = true; - } - - if (urlParams.has("blackout") || urlParams.has("blackoutmode") || urlParams.has("bo") || urlParams.has("bom")) { - getById("blackoutmode").classList.remove("hidden"); - if (urlParams.get("blackout") || urlParams.get("blackoutmode") || urlParams.get("bo") || urlParams.get("bom")) { - blackoutMode(); - } - } - - if (session.dataMode) { - delayedStartupFuncs.push([joinDataMode]); - } else if (session.autostart) { - if (session.screenshare !== false) { - delayedStartupFuncs.push([publishScreen]); - } - if (session.consent) { - setTimeout(function () { - warnUser("⚠ Privacy warning: The director of this room can remotely switch your camera or microphone without permission.", 8000); - }, 1500); - } - } - - if (urlParams.has("noiframe") || urlParams.has("noiframes") || urlParams.has("nif") || urlParams.has("nowebsite")) { - session.noiframe = urlParams.get("noiframe") || urlParams.get("noiframes") || urlParams.get("nif") || urlParams.get("nowebsite"); - - if (!session.noiframe) { - session.noiframe = []; - } else { - session.noiframe = session.noiframe.split(","); - } - log("disable iframe playback"); - log(session.noiframe); - } - - if (urlParams.has("exclude") || urlParams.has("ex")) { - session.exclude = urlParams.get("exclude") || urlParams.get("ex"); - - if (!session.exclude) { - session.exclude = false; - } else { - session.exclude = session.exclude.split(","); - } - log("exclude audio/video playback"); - log(session.exclude); - } - - if (urlParams.has("noscreenshare") || urlParams.has("noscreenshares") || urlParams.has("noscreen") || urlParams.has("noscreens")) { - session.noScreenShare = true; - log("disable screen share playback"); - } - if (urlParams.has("screenp2p") || urlParams.has("noscreenwhep")) { - session.screenWhepPreference = "p2p"; - } - if (urlParams.has("screenwheponly")) { - session.screenWhepPreference = "whep"; - } - if (urlParams.has("screenwhep")) { - const pref = parseScreenToggleParam(urlParams.get("screenwhep")); - if (pref === false) { - session.screenWhepPreference = "p2p"; - } else if (pref === true) { - session.screenWhepPreference = "whep"; - } - } - function parseScreenToggleParam(value) { - if (value === null || value === undefined) { - return true; - } - const normalized = ("" + value).trim().toLowerCase(); - if (!normalized.length) { - return true; - } - if (["0", "false", "no", "off", "disable", "disabled", "none"].includes(normalized)) { - return false; - } - if (["1", "true", "yes", "on", "enable", "enabled"].includes(normalized)) { - return true; - } - return true; - } - if (urlParams.has("allowscreenvideo")) { - session.screenVideoOverride = parseScreenToggleParam(urlParams.get("allowscreenvideo")); - log("screen share video override: " + session.screenVideoOverride); - } - if (urlParams.has("allowscreenaudio")) { - session.screenAudioOverride = parseScreenToggleParam(urlParams.get("allowscreenaudio")); - log("screen share audio override: " + session.screenAudioOverride); - } - - if (urlParams.has("excludeaudio") || urlParams.has("exaudio") || urlParams.has("silence")) { - session.excludeaudio = urlParams.get("excludeaudio") || urlParams.get("exaudio") || urlParams.get("silence"); - - if (!session.excludeaudio) { - session.excludeaudio = false; - } else { - session.excludeaudio = session.excludeaudio.split(","); - } - log("exclude audio playback"); - log(session.excludeaudio); - } - - if (urlParams.has("novideo") || urlParams.has("nv") || urlParams.has("hidevideo") || urlParams.has("showonly")) { - session.novideo = urlParams.get("novideo") || urlParams.get("nv") || urlParams.get("hidevideo") || urlParams.get("showonly"); - - if (!session.novideo) { - session.novideo = []; - } else { - session.novideo = session.novideo.split(","); - } - log("disable video playback"); - log(session.novideo); - } - - if (urlParams.has("noaudio") || urlParams.has("na") || urlParams.has("hideaudio")) { - session.noaudio = urlParams.get("noaudio") || urlParams.get("na") || urlParams.get("hideaudio"); - - if (!session.noaudio) { - session.noaudio = []; - } else { - session.noaudio = session.noaudio.split(","); - } - log("disable audio playback"); - } - - - if (urlParams.has("nodirectoraudio")) { - session.nodirectoraudio = true; - log("disable audio playback from Directors"); - } - if (urlParams.has("nodirectorvideo")) { - session.nodirectoraudio = true; - log("disable audio playback from Directors"); - } - - if (urlParams.has("forceios")) { - log("allow iOS to work in video group chat; for this user at least"); - session.forceios = true; - } - - if (urlParams.has("nocursor") || urlParams.has("hidecursor") || urlParams.has("nomouse") || urlParams.has("hidemouse")) { - // on the screen, not in screen share - session.nocursor = true; - log("DISABLE CURSOR"); - var styletmp = document.createElement("style"); - styletmp.innerHTML = ` - video { - margin: 0; - padding: 0; - overflow: hidden; - cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=), none; - user-select: none; - } - `; - document.head.appendChild(styletmp); - } - - if (urlParams.has("cursor") || urlParams.has("screensharecursor")) { - session.screensharecursor = true; - } - - if (urlParams.has("distort")) { - session.voicechanger = 1; - } - - if (urlParams.has("dtx") || urlParams.has("usedtx")) { - session.dtx = true; - session.cbr = 0; // no point dtx on if cbr is on, right? - } - - if (urlParams.has("youtube")) { - // Set with a Youtube v3 clientID + "," + API key, then run YoutubeChatInterface(); - session.youtubeKey = urlParams.get("youtube") || ""; - //YoutubeChatInterface(); // queries Youtube for chat messages. Forwards them to the parent IFRAME only at the moment. - } - - if (urlParams.has("vbr")) { - session.cbr = 0; - getById("whipoutvbrcbr").classList.add("hidden"); - } else if (urlParams.has("cbr")) { - session.cbr = 1; - getById("whipoutvbrcbr").classList.add("hidden"); - } - - if (urlParams.has("order")) { - session.order = parseInt(urlParams.get("order")) || 1; - } - - if (urlParams.has("orderby")) { - session.orderby = urlParams.get("orderby") || "id"; // "label" also an option; the default is stream ID tho. - } - - if (urlParams.has("slotmode") || urlParams.has("slotsmode")) { - session.slotmode = parseInt(urlParams.get("slotmode")) || parseInt(urlParams.get("slotsmode")) || 1; - } - - if (urlParams.has("slot")) { - var slotValue = parseInt(urlParams.get("slot")); - session.slot = isNaN(slotValue) ? false : slotValue; // 0 = exclude from slots, N = prefer slot N, false = auto-assign - } - - if (urlParams.has("slots")) { - session.slots = parseInt(urlParams.get("slots")) || 4; // first N slots can be filled - } else if (urlParams.has('slotslist')) { // select which slots you want to be processed - session.slotsList = urlParams.get('slotslist').split(',').map(slot => parseInt(slot.trim())).filter(slot => !isNaN(slot)); - if (!session.slotsList.length){ - session.slotsList = false; - } - } - - if (urlParams.has("maxslots")) { - // hard coded default is 12; if &maxslots used, it changes to 20 unless value passed. - session.maxAvailableSlots = parseInt(urlParams.get("maxslots")) || session.maxAvailableSlots; - } - - if (session.slotmode){ - populateSlotPicker(); - } - - if (urlParams.has("alpha")) { - session.alpha = true; - } - - if (urlParams.has("chunked") || urlParams.has("chunk")) { - session.chunked = parseInt(urlParams.get("chunked")) || parseInt(urlParams.get("chunk")) || 2500; // sender side; enables to allows. - // session.alpha = true; - if (Firefox || SafariVersion) { - if (!session.cleanOutput) { - warnUser("Only Chromium-based browsers support chunked mode.\n\nPlease switch to Chrome or another compatible browser to use &chunked mode."); - } - session.chunked = false; - console.warn("Disabling chunked mode since not using a compatible browser."); - } - } - if (urlParams.has("chunkedbuffer") || urlParams.has("sendingbuffer")) { - session.sendingBuffer = parseInt(urlParams.get("chunkedbuffer")) || parseInt(urlParams.get("sendingbuffer")) || 5000; // sender side; enables to allows. - } - - const chunkProfiles = { - mobile: { - chunkfec: 3, - chunknack: true, - chunkbuffer: 900, - chunkbufferfloor: 600, - chunkbufferceil: 1600, - chunkjitterslack: 250, - chunkadapt: "framerate", - chunkadaptfloor: 320, - chunkadaptceil: 1400, - chunkadaptthreshold: 260, - chunkadaptmaxdrop: 10, - chunkadaptinterval: 1200 - }, - balanced: { - chunkfec: 4, - chunknack: true, - chunkbuffer: 750, - chunkbufferfloor: 450, - chunkbufferceil: 1400, - chunkjitterslack: 220, - chunkadapt: "hybrid", - chunkadaptfloor: 420, - chunkadaptceil: 2600, - chunkadaptthreshold: 340, - chunkadaptmaxdrop: 6, - chunkadaptinterval: 900 - }, - desktop: { - chunkfec: 5, - chunknack: true, - chunkbuffer: 620, - chunkbufferfloor: 400, - chunkbufferceil: 1100, - chunkjitterslack: 180, - chunkadapt: "bitrate", - chunkadaptfloor: 580, - chunkadaptceil: 4200, - chunkadaptthreshold: 360, - chunkadaptmaxdrop: 4, - chunkadaptinterval: 700 - } - }; - - function applyChunkPreset(preset) { - if (!preset) { - return; - } - Object.keys(preset).forEach(key => { - const value = preset[key]; - const sessionKey = key; - session[sessionKey] = value; - }); - } - - if (urlParams.has("chunkprofile")) { - const profileName = (urlParams.get("chunkprofile") || "").toLowerCase(); - if (chunkProfiles[profileName]) { - session.chunkprofile = profileName; - applyChunkPreset(chunkProfiles[profileName]); - } - } - - function parseIntegerParam(name, target, clamp = null) { - if (!urlParams.has(name)) { - return; - } - let value = parseInt(urlParams.get(name)); - if (!Number.isFinite(value)) { - return; - } - if (clamp && Array.isArray(clamp)) { - const [min, max] = clamp; - if (typeof min === "number") { - value = Math.max(min, value); - } - if (typeof max === "number") { - value = Math.min(max, value); - } - } - session[target] = value; - } - - function parseBooleanParam(name, target) { - if (!urlParams.has(name)) { - return; - } - const raw = urlParams.get(name); - if (raw === null || raw === "" || raw === "1" || raw.toLowerCase() === "true") { - session[target] = true; - } else if (raw.toLowerCase() === "0" || raw.toLowerCase() === "false") { - session[target] = false; - } else { - session[target] = true; - } - } - - parseIntegerParam("chunkfec", "chunkfec", [0, 12]); - parseBooleanParam("chunknack", "chunknack"); - parseIntegerParam("chunkbuffer", "chunkbuffer", [0, 30000]); - parseIntegerParam("chunkbufferfloor", "chunkbufferfloor", [0, 30000]); - parseIntegerParam("chunkbufferceil", "chunkbufferceil", [0, 60000]); - parseIntegerParam("chunkjitterslack", "chunkjitterslack", [0, 10000]); - - if (urlParams.has("chunkadapt")) { - const adaptMode = (urlParams.get("chunkadapt") || "").toLowerCase(); - if (["bitrate", "framerate", "hybrid"].includes(adaptMode)) { - session.chunkadapt = adaptMode; - } - } - parseIntegerParam("chunkadaptfloor", "chunkadaptfloor", [0, 10000]); - parseIntegerParam("chunkadaptceil", "chunkadaptceil", [0, 100000]); - parseIntegerParam("chunkadaptthreshold", "chunkadaptthreshold", [0, 10000]); - parseIntegerParam("chunkadaptmaxdrop", "chunkadaptmaxdrop", [0, 120]); - parseIntegerParam("chunkadaptinterval", "chunkadaptinterval", [100, 60000]); - - parseIntegerParam("chunkretry", "chunkretry", [0, 60000]); - parseIntegerParam("chunkcache", "chunkcache", [0, 60000]); - parseIntegerParam("chunkchunksize", "chunkchunksize", [2048, 65536]); - - if (urlParams.has("nochunk") || urlParams.has("nochunked")) { - // viewer side - session.nochunk = true; - } - - if (urlParams.has("nochunkaudio") || urlParams.has("nochunkedaudio")) { - // viewer side - session.nochunkaudio = true; - } - - if (urlParams.has("audiobuffer") || urlParams.has("bufferaudio")) { - // viewer side - session.audioBuffer = urlParams.get("audiobuffer") || urlParams.get("bufferaudio") || 0; - session.audioBuffer = parseInt(session.audioBuffer); - } - - //if (urlParams.has('viewchunked') || urlParams.has('viewchunk') || urlParams.has('allowchunked') || urlParams.has('allowchunk')) { // viewer side - // session.forceChunked = true; - //} - - if (urlParams.has("token")) { - session.token = urlParams.get("token") || false; - // checkToken(); // this is sycnhonous - } - - if (urlParams.has("maindirectorpassword") || urlParams.has("maindirpass")) { - session.mainDirectorPassword = urlParams.get("maindirectorpassword") || urlParams.get("maindirpass") || false; - if (!session.mainDirectorPassword) { - window.focus(); - session.mainDirectorPassword = await promptAlt(getTranslation("director-password"), true, true); - if (session.mainDirectorPassword) { - session.mainDirectorPassword = session.mainDirectorPassword.trim(); - try { - session.mainDirectorPassword = decodeURIComponent(session.mainDirectorPassword); - } catch (e) { - errorlog(e); - } - } - } - // registerToken(); - } - - if (urlParams.has("debug")) { - DebugLog = true; - if (!errorReport) { - errorReport = []; - } - if (urlParams.get("debug") == "1") { - debugStart(); - } else if (urlParams.get("debug")) { - debugStart(urlParams.get("debug")); - } - } - - if (urlParams.has("group") || urlParams.has("groups")) { - session.group = urlParams.get("group") || urlParams.get("groups") || ""; - session.group = session.group.split(","); - } - - if (urlParams.has("groupview") || urlParams.has("viewgroup") || urlParams.has("gv")) { - session.groupView = urlParams.get("groupview") || urlParams.get("viewgroup") || urlParams.get("gv") || ""; - session.groupView = session.groupView.split(","); - } - - if (urlParams.has("groupaudio") || urlParams.has("ga")) { - session.groupAudio = true; - } - - if (urlParams.has("groupmode") || urlParams.has("gm")) { - session.allowNoGroup = true; - } - - if (urlParams.has("host")) { - session.roomhost = true; - } - - if (urlParams.has("sensors") || urlParams.has("sensor") || urlParams.has("gyro") || urlParams.has("gyros") || urlParams.has("accelerometer")) { - session.sensorData = urlParams.get("sensors") || urlParams.get("sensor") || urlParams.get("gyro") || urlParams.get("gyros") || urlParams.get("accelerometer") || 30; - session.sensorData = parseInt(session.sensorData); - } - if (urlParams.has("sensorfilter") || urlParams.has("sensorsfilter") || urlParams.has("filtersensor") || urlParams.has("filtersensors")) { - session.sensorDataFilter = urlParams.get("sensorfilter") || urlParams.get("sensorsfilter") || urlParams.get("filtersensor") || urlParams.get("filtersensors") || ""; - session.sensorDataFilter = session.sensorDataFilter.split(","); // ["pos","lin","ori","mag","gyro","acc"]; - } - if (urlParams.has("webxrbridge") || urlParams.has("externalsensors") || urlParams.has("sensorsbridge")) { - session.externalSensorBridge = true; - session.externalSensorOrigin = urlParams.get("sensorsorigin") || ""; - } - - if (urlParams.has("ptime")) { - session.ptime = parseInt(urlParams.get("ptime")) || 20; - if (session.ptime < 10) { - session.ptime = 10; - } - } - - if (urlParams.has("minptime")) { - session.minptime = parseInt(urlParams.get("minptime")) || 10; - if (session.minptime < 10) { - session.minptime = 10; - } - if (session.minptime > 300) { - session.minptime = 300; - } - } - - if (urlParams.has("maxptime")) { - session.maxptime = parseInt(urlParams.get("maxptime")) || 60; - if (session.maxptime < 10) { - session.maxptime = 10; - } - if (session.maxptime > 300) { - session.maxptime = 300; - } - } - - if (urlParams.has("contenthint") || urlParams.has("contenttype") || urlParams.has("content") || urlParams.has("hint")) { - session.contentHint = urlParams.get("contenthint") || urlParams.get("contenttype") || urlParams.get("content") || urlParams.get("hint") || "detail"; - } - - if (urlParams.has("audiocontenthint") || urlParams.has("audiocontenttype") || urlParams.has("audiocontent") || urlParams.has("audiohint")) { - session.audioContentHint = urlParams.get("audiocontenthint") || urlParams.get("audiocontenttype") || urlParams.get("audiocontent") || urlParams.get("audiohint") || "music"; - } - - if (urlParams.has("screensharecontenthint") || urlParams.has("sscontenthint") || urlParams.has("screensharecontenttype") || urlParams.has("sscontent") || urlParams.has("sshint")) { - session.screenshareContentHint = urlParams.get("screensharecontenthint") || urlParams.get("sscontenthint") || urlParams.get("screensharecontenttype") || urlParams.get("sscontent") || urlParams.get("sshint") || "detail"; - } - - if (urlParams.has("vred")) { - session.videoErrorCorrection = true; - } - if (urlParams.has("pvred")) { - session.preferredVideoErrorCorrection = true; - } - - if (urlParams.has("codec") || urlParams.has("codecs") || urlParams.has("videocodec")) { - log("codecs CHANGED"); - session.codecs = urlParams.get("codec") || urlParams.get("codecs") || urlParams.get("videocodec") || false; - if (session.codecs) { - session.codecs = session.codecs.toLowerCase(); - session.codecs = session.codecs.split(","); - if (session.codecs.length) { - session.codec = session.codecs.shift(); - if (!session.codec) { - session.codec = false; - session.codecs = false; - } - if (!session.codecs.length) { - session.codecs = false; - } - } else { - session.codecs = false; - } - } - } else if (OperaGx) { - session.codec = "vp8"; - warnlog("Defaulting to VP8 manually, as H264 with remote iOS devices is not supported"); - } - - if (urlParams.has("redaudio")) { - // just for experimenting - session.redAudio = true; - } - if (urlParams.has("fecaudio")) { - // - session.fecAudio = true; - } - if (urlParams.has("predaudio")) { - // - session.predAudio = true; - } - if (urlParams.has("pfecaudio")) { - // - session.pfecAudio = true; - } - - if (urlParams.has("audiocodec")) { - log("CODEC CHANGED"); - session.audioCodec = urlParams.get("audiocodec") || false; - if (session.audioCodec) { - session.audioCodec = session.audioCodec.toLowerCase(); - } - } - if (session.audioCodec && session.audioCodec == "red") { - session.audiobitratePRO = 216; // higher than this seems to break the RED mode. default 256. - } - - if (urlParams.has("preferaudiocodec")) { - log("PREFER CODEC CHANGED"); - session.preferAudioCodec = urlParams.get("preferaudiocodec") || false; - if (session.preferAudioCodec) { - session.preferAudioCodec = session.preferAudioCodec.toLowerCase(); - } - } - - if (urlParams.has("prefervideocodec")) { - log("PREFER VIDEO CODEC CHANGED"); - session.preferVideoCodec = urlParams.get("prefervideocodec") || false; - if (session.preferVideoCodec) { - session.preferVideoCodec = session.preferVideoCodec.toLowerCase(); - } - } - - if (urlParams.has("scenelinkcodec")) { - // this is mainly for a niche iframe API use - log("codecGroupFlag CHANGED"); - session.codecGroupFlag = urlParams.get("scenelinkcodec") || false; - if (session.codecGroupFlag) { - session.codecGroupFlag = "&codec=" + session.codecGroupFlag.toLowerCase(); - } - } - if (session.codecGroupFlag) { - getById("codecGroupFlag").disabled = true; - } - if (urlParams.has("scenelinkbitrate")) { - // this is mainly for a niche iframe API use - log("bitrateGroupFlag CHANGED"); - session.bitrateGroupFlag = urlParams.get("scenelinkbitrate") || false; - if (session.bitrateGroupFlag) { - session.bitrateGroupFlag = "&totalbitrate=" + parseInt(session.bitrateGroupFlag); - } - } - - if (urlParams.has("h264profile")) { - session.h264profile = urlParams.get("h264profile") || "42e01f"; // 42001f - session.h264profile = session.h264profile.substring(0, 6); - session.h264profile = session.h264profile.toLowerCase(); - if (session.h264profile == "0") { - session.h264profile = false; - } else if (session.h264profile == "off") { - session.h264profile = false; - } else if (session.h264profile == "disabl") { - session.h264profile = false; - } else if (session.h264profile == "defaul") { - session.h264profile = false; - } else if (session.h264profile == "false") { - session.h264profile = false; - } - } else if (session.codec === "hardware" && Android) { - // same as &h264profile, but easier for me to remember. I'll try to automate this in the future. - session.codec = "h264"; - session.h264profile = "42e01f"; - } - - if (urlParams.has("nofec")) { - // disables error control / throttling -- currently on audio - session.noFEC = true; - } - if (urlParams.has("nonacks") || urlParams.has("nonack")) { - // disables error control / throttling. - session.noNacks = true; - } - if (urlParams.has("nopli")) { - // disables error control / throttling. - session.noPLIs = true; - } - if (urlParams.has("noremb")) { - // disables Receiver Estimated Maximum Bitrate (throttling) - session.noREMB = true; - } - - if (urlParams.has("scale")) { - if (urlParams.get("scale") == "false") { - } else if (urlParams.get("scale") == "0") { - } else if (urlParams.get("scale") == "no") { - } else if (urlParams.get("scale") == "off") { - } else { - log("Resolution scale requested"); - session.scale = parseFloat(urlParams.get("scale")) || 100; - } - session.dynamicScale = false; // default true - } else { - if (urlParams.has("viewwidth") || urlParams.has("vw")) { - session.viewwidth = urlParams.get("viewwidth") || urlParams.get("vw") || false; - if (session.viewwidth) { - session.viewwidth = parseInt(session.viewwidth); - } - session.dynamicScale = false; // default true - } - if (urlParams.has("viewheight") || urlParams.has("vh")) { - session.viewheight = urlParams.get("viewheight") || urlParams.get("vh") || false; - session.dynamicScale = false; // default true - if (session.viewheight) { - session.viewheight = parseInt(session.viewheight); - } - } - } - - if (urlParams.has("sharperscreen")) { - // sets scale to 100 for inbound screenshares only - session.sharperScreen = true; - } - - if (urlParams.has("mcscale") || urlParams.has("meshcastscale") || urlParams.has("woscale") || urlParams.has("whipoutscale")) { - session.whipOutScale = parseFloat(urlParams.get("mcscale")) || parseFloat(urlParams.get("meshcastscale")) || parseFloat(urlParams.get("woscale")) || parseFloat(urlParams.get("whipoutscale")) || 100; - } - - if (isIFrame) { - getById("helpbutton").style.display = "none"; - getById("helpbutton").style.opacity = 0; - getById("reportbutton").style.display = "none"; - getById("reportbutton").style.opacity = 0; - getById("calendarButton").style.display = "none"; - getById("calendarButton").style.opacity = 0; - getById("chatBody").innerHTML = ""; - } - - if (urlParams.has("poke")){ - session.poke = urlParams.get("poke").replace(/[\W]+/g, "_").replace(/_+/g, "_") || true; - } - - if (urlParams.has("beep") || urlParams.has("notify") || urlParams.has("tone")) { - let beepValue = urlParams.get("beep") || urlParams.get("notify") || urlParams.get("tone") || ""; - let beepTypes = []; - - if (beepValue) { - beepTypes = beepValue - .split(",") - .map(type => type.trim().toLowerCase()) - .filter(type => type !== ""); - session.beepToNotify = beepTypes.length ? beepTypes : true; - } else { - beepTypes = []; - session.beepToNotify = true; // enable all, since nothing was specified - } - - if (beepTypes.length === 0 || beepTypes.includes("knock")) { - // Allow callers to request the louder knock tone without extra flags - session.knockToneEnabled = true; - } - - if (beepTypes.length === 0 || beepTypes.includes("join")) { - const addtone = createAudioElement(); - addtone.id = "jointone"; - addtone.src = "./media/join.mp3"; - getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling); - } - - if (beepTypes.length === 0 || beepTypes.includes("leave")) { - const addtone = createAudioElement(); - addtone.id = "leavetone"; - addtone.src = "./media/leave.mp3"; - getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling); - } - - if (!Notification) { - warnlog("Desktop notifications are not available in your browser."); - } else if (Notification.permission !== "granted") { - Notification.requestPermission(); - } - } - - if (urlParams.has("r2d2")) { - /* var addtone = createAudioElement(); - addtone.id = "jointone"; - addtone.src = "./media/join.mp3"; - getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling) - var addtone = createAudioElement(); - addtone.id = "leavetone"; - addtone.src = "./media/leave.mp3"; - getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling) */ - getById("testtone").innerHTML = ""; - getById("testtone").src = "./media/robot.mp3"; - session.beepToNotify = true; - } - - if (urlParams.get("custombeep")) { - updateAudioSource(urlParams.get("custombeep"), "testtone"); - } - if (urlParams.get("customleave")) { - updateAudioSource(urlParams.get("customleave"), "leavetone"); - } - if (urlParams.get("customjoin")) { - updateAudioSource(urlParams.get("customjoin"), "jointone"); - } - - if (urlParams.has("beepvolume")) { - const volume = parseInt(urlParams.get("beepvolume")) / 100 || 0; - ["testtone", "jointone", "leavetone"].forEach(id => { - const audio = document.getElementById(id); - if (!audio) return; - - try { - if (volume > 1 && session.audioCtx) { - audio.volume = 1; // Set base volume to 100% - audio.crossOrigin = "anonymous"; // Try to enable CORS - const source = session.audioCtx.createMediaElementSource(audio); - const gainNode = session.audioCtx.createGain(); - gainNode.gain.value = volume; - source.connect(gainNode); - gainNode.connect(session.audioCtx.destination); - audio.gainNode = gainNode; - console.warn("note: If the audio file is protected by CORS, increasing the volume will cause it to fail"); - } else { - audio.volume = volume; - } - } catch (e) { - console.warn("Volume boost failed, falling back to normal volume", e); - audio.volume = Math.min(1, volume); // Fallback to normal volume, capped at 100% - } - }); - } - - if (urlParams.has("easyexit") || urlParams.has("ee")) { - session.noExitPrompt = true; - } - - if (urlParams.has("entrymsg") || urlParams.has("welcome") || urlParams.has("welcomeb64")) { - session.welcomeMessage = urlParams.get("entrymsg") || urlParams.get("welcome") || urlParams.get("welcomeb64"); - - if (urlParams.get("welcomeb64")) { - try { - session.welcomeMessage = atob(session.welcomeMessage); - } catch (e) {} - } - try { - session.welcomeMessage = session.welcomeMessage.replace(/(\r\n|\n|\r)/gm, " "); - session.welcomeMessage = decodeURIComponent(session.welcomeMessage); - } catch (e) {} - } - - if (urlParams.has("welcomehtml")) { - session.welcomeHTML = urlParams.get("welcomehtml"); - - try { - session.welcomeHTML = atob(session.welcomeHTML); - } catch (e) {} - try { - session.welcomeHTML = session.welcomeHTML.replace(/(\r\n|\n|\r)/gm, " "); - session.welcomeHTML = decodeURIComponent(session.welcomeHTML); - } catch (e) {} - } - - if (urlParams.has("welcomeimage") || urlParams.has("welcomeimg")) { - session.welcomeImage = urlParams.get("welcomeimage") || urlParams.get("welcomeimg"); - try { - session.welcomeImage = decodeURIComponent(session.welcomeImage); - } catch (e) {} - } - - if (urlParams.has("mixminus") || urlParams.has("mm")) { - session.mixMinus = true; - // Director/co-director mix-minus: director mixes audio and sends custom mix to each guest - if (session.director || session.codirector) { - session.directorMixMinus = true; - session.mixMinusState = {}; // Per-guest mix-minus state - session.mixMinusDefaults = { - allGuestsEnabled: true, // Default state for new guests - includeDirectorAudio: true, // Include director's audio by default - includeAllGuests: true // Include all other guests by default - }; - } - } - - if (urlParams.has("clearstorage") || urlParams.has("clear")) { - clearStorage(); - } - - if (urlParams.has("videobitrate") || urlParams.has("bitrate") || urlParams.has("vb")) { - session.bitrate = urlParams.get("videobitrate") || urlParams.get("bitrate") || urlParams.get("vb") || 8000; - if (session.bitrate) { - if (session.view_set && session.bitrate.split(",").length > 1) { - session.bitrate_set = session.bitrate.split(","); - session.bitrate = parseInt(session.bitrate_set[0]); - } else { - session.bitrate = parseInt(session.bitrate); - } - if (session.bitrate < 1) { - session.bitrate = false; - } - log("BITRATE ENABLED"); - log(session.bitrate); - } - } - - if (urlParams.has("maxvideobitrate") || urlParams.has("maxbitrate") || urlParams.has("maxvb") || urlParams.has("mvb")) { - session.maxvideobitrate = urlParams.get("maxvideobitrate") || urlParams.get("maxbitrate") || urlParams.get("maxvb") || urlParams.get("mvb"); - session.maxvideobitrate = parseInt(session.maxvideobitrate); - - if (session.maxvideobitrate < 1) { - session.maxvideobitrate = false; - } - log("maxvideobitrate ENABLED"); - log(session.maxvideobitrate); - } - - if (urlParams.has("totalroombitrate") || urlParams.has("totalroomvideobitrate") || urlParams.has("trb") || urlParams.has("totalbitrate") || urlParams.has("tb")) { - session.totalRoomBitrate = urlParams.get("totalroombitrate") || urlParams.get("totalroomvideobitrate") || urlParams.get("trb") || urlParams.get("totalbitrate") || urlParams.get("tb") || ""; - - if (session.totalRoomBitrate.split(",").length > 1) { - if (session.mobile) { - session.totalRoomBitrate = session.totalRoomBitrate.split(",")[1]; - } else { - session.totalRoomBitrate = session.totalRoomBitrate.split(",")[0]; - } - } - - if ((session.totalRoomBitrate == "false") || (session.totalRoomBitrate == "off")){ - session.totalRoomBitrate = 0; - } - - session.totalRoomBitrate = parseInt(session.totalRoomBitrate) || 0; - - if (session.totalRoomBitrate < 1) { - session.totalRoomBitrate = 0; - } - log("totalRoomBitrate ENABLED"); - log(session.totalRoomBitrate); - } - - if (session.totalRoomBitrate === false) { - session.totalRoomBitrate = session.bitrate || session.totalRoomBitrate_default; // sneaky sneaky - } else { - session.totalRoomBitrate_default = session.totalRoomBitrate; // trb_default doesn't change dynamically, but trb can (per director I guess) - } - - if (session.totalRoomBitrate_default > 4000) { - getById("trbSettingInput").max = Math.ceil(session.totalRoomBitrate_default); - } - - if (urlParams.has("maxtotalscenebitrate") || urlParams.has("totalscenebitrate") || urlParams.has("mtsb") || urlParams.has("tsb") || urlParams.has("totalbitrate") || urlParams.has("tb")) { - session.totalSceneBitrate = urlParams.get("maxtotalscenebitrate") || urlParams.get("totalscenebitrate") || urlParams.get("mtsb") || urlParams.get("tsb") || urlParams.get("totalbitrate") || urlParams.get("tb") || false; - if (session.totalSceneBitrate) { - session.totalSceneBitrate = parseInt(session.totalSceneBitrate); - } - } - - if (urlParams.has("blur")) { - session.blurBackground = urlParams.get("blur") || 10; - session.blurBackground = parseInt(session.blurBackground) || 10; - if (session.blurBackground < 0) { - session.blurBackground = false; - } - session.structure = true; - } - - if (urlParams.has("limittotalbitrate") || urlParams.has("ltb")) { - session.limitTotalBitrate = urlParams.get("limittotalbitrate") || urlParams.get("ltb") || "2500"; - - if (session.limitTotalBitrate.split(",").length > 1) { - if (session.mobile) { - session.limitTotalBitrate = session.limitTotalBitrate.split(",")[1]; - } else { - session.limitTotalBitrate = session.limitTotalBitrate.split(",")[0]; - } - } - session.limitTotalBitrate = parseInt(session.limitTotalBitrate); - getById("limittotalbitrate_director").classList.remove("hidden"); - } - - if (session.limitTotalBitrate) { - if (session.limitTotalBitrate > session.limitTotalBitrate_defaultMax) { - getById("ltbSettingInputManual").max = Math.ceil(session.limitTotalBitrate); - } - getById("ltbSettingInputManual").value = session.limitTotalBitrate; - getById("ltbSettingInput").value = session.limitTotalBitrate; - getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled"; - } - - if (urlParams.has("mcscreensharebitrate") || urlParams.has("mcssbitrate") || urlParams.has("whipoutscreensharebitrate") || urlParams.has("wossbitrate")) { - session.whipOutScreenShareBitrate = urlParams.get("mcscreensharebitrate") || urlParams.get("mcssbitrate") || urlParams.get("whipoutscreensharebitrate") || urlParams.get("wossbitrate") || 2500; - session.whipOutScreenShareBitrate = parseInt(session.whipOutScreenShareBitrate); - } - - if (urlParams.has("mcscreensharecodec") || urlParams.has("mcsscodec") || urlParams.has("whipoutscreensharecodec") || urlParams.has("wosscodec")) { - session.whipOutScreenShareCodec = urlParams.get("mcscreensharecodec") || urlParams.get("mcsscodec") || urlParams.get("whipoutscreensharecodec") || urlParams.get("wosscodec") || false; - } - if (session.whipOutScreenShareCodec) { - session.whipOutScreenShareCodec = session.whipOutScreenShareCodec.toLowerCase(); - } - - if (urlParams.has("mccodec") || urlParams.has("meshcastcodec") || urlParams.has("whipoutcodec") || urlParams.has("whipoutvideocodec") || urlParams.has("woc") || urlParams.has("wovc")) { - session.whipOutCodec = urlParams.get("mccodec") || urlParams.get("meshcastcodec") || urlParams.get("whipoutcodec") || urlParams.get("whipoutvideocodec") || urlParams.get("woc") || urlParams.get("wovc") || false; - getById("whipoutcodecGroupFlag").classList.add("hidden"); - } - - if (session.whipOutCodec) { - session.whipOutCodec = session.whipOutCodec.toLowerCase(); - if (session.whipOutCodec == "h264") { - if (Firefox) { - session.whipOutCodec = false; - } - } - if (session.whipOutCodec) { - session.whipOutCodec = session.whipOutCodec.split(","); - } - getById("whipoutcodecGroupFlag").classList.add("hidden"); - } - - if (urlParams.has("whipoutaudiocodec") || urlParams.has("woac")) { - session.whipOutAudioCodec = urlParams.get("whipoutaudiocodec") || urlParams.get("woac") || false; - // getById("whipoutaudiocodecGroupFlag").classList.add("hidden"); // havne't added this in yet as an actual html element - } - - if (urlParams.has("mcab") || urlParams.has("mcaudiobitrate") || urlParams.has("meshcastab") || urlParams.has("meshcastaudiobitrate ") || urlParams.has("whipoutaudiobitrate") || urlParams.has("woab")) { - session.whipOutAudioBitrate = urlParams.get("mcab") || urlParams.get("mcaudiobitrate") || urlParams.get("meshcastab") || urlParams.get("meshcastaudiobitrate ") || urlParams.get("whipoutaudiobitrate") || urlParams.get("woab") || false; - if (session.whipOutAudioBitrate) { - session.whipOutAudioBitrate = parseInt(session.whipOutAudioBitrate); - } - getById("whipoutaudiobitrate").classList.add("hidden"); - } - - if (urlParams.has("mcb") || urlParams.has("mcbitrate") || urlParams.has("meshcastbitrate") || urlParams.has("whipoutvideobitrate") || urlParams.has("wovb")) { - session.whipOutVideoBitrate = urlParams.get("mcb") || urlParams.get("mcbitrate") || urlParams.get("meshcastbitrate") || urlParams.get("whipoutvideobitrate") || urlParams.get("wovb") || false; - if (session.whipOutVideoBitrate) { - session.whipOutVideoBitrate = parseInt(session.whipOutVideoBitrate); - } - getById("whipoutbitrateGroupFlag").classList.add("hidden"); - getById("whipoutvbrcbr").classList.add("hidden"); - } - - if (urlParams.has("height") || urlParams.has("h")) { - session.height = urlParams.get("height") || urlParams.get("h"); - session.height = parseInt(session.height); - } - - if (urlParams.has("width") || urlParams.has("w")) { - session.width = urlParams.get("width") || urlParams.get("w"); - session.width = parseInt(session.width); - } - - if (urlParams.has("quality") || urlParams.has("q")) { - try { - session.quality = urlParams.get("quality") || urlParams.get("q") || "0"; - if (session.quality.toLowerCase() == "4k") { - session.quality = -2; - } else if (session.quality.toLowerCase() == "2160p") { - session.quality = -2; - } else if (session.quality.toLowerCase() == "2160") { - session.quality = -2; - } else if (session.quality.toLowerCase() == "2k") { - session.quality = -3; - } else if (session.quality.toLowerCase() == "1440p") { - session.quality = -3; - } else if (session.quality.toLowerCase() == "1440") { - session.quality = -3; - } else if (session.quality.toLowerCase() == "hd") { - // - session.quality = 1; - } else if (session.quality.toLowerCase() == "720p") { - // - session.quality = 1; - } else if (session.quality.toLowerCase() == "720") { - // - session.quality = 1; - } else if (session.quality.toLowerCase() == "fullhd") { - session.quality = 0; - } else if (session.quality.toLowerCase() == "1080p") { - session.quality = 0; - } else if (session.quality.toLowerCase() == "1080") { - session.quality = 0; - } else if (session.quality.toLowerCase() == "high") { - session.quality = 0; - } else if (session.quality.toLowerCase() == "360p") { - session.quality = 2; - } else if (session.quality.toLowerCase() == "360") { - session.quality = 2; - } else if (session.quality.toLowerCase() == "low") { - session.quality = 2; - } - session.quality = parseInt(session.quality); - getById("gear_screen").parentNode.removeChild(getById("gear_screen")); - getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); - - } catch (e) { - errorlog(e); - } - } else if (urlParams.has("fullhd") || urlParams.has("1080p")) { - session.quality = 0; - getById("gear_screen").parentNode.removeChild(getById("gear_screen")); - getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); - - } else if (urlParams.has("4k")) { - session.quality = -2; - getById("gear_screen").parentNode.removeChild(getById("gear_screen")); - getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); - - } - - if (urlParams.has("sink")) { - session.sink = urlParams.get("sink"); - } else if (urlParams.has("outputdevice") || urlParams.has("od") || urlParams.has("audiooutput")) { - session.outputDevice = urlParams.get("outputdevice") || urlParams.get("od") || urlParams.get("audiooutput") || null; - - if (session.outputDevice) { - session.outputDevice = normalizeDeviceLabel(session.outputDevice); - } else { - session.outputDevice = null; - getById("headphonesDiv3").style.display = "none"; // - } - - if (session.outputDevice) { - try { - enumerateDevices().then(function (deviceInfos) { - for (let i = 0; i !== deviceInfos.length; ++i) { - if (deviceInfos[i].kind === "audiooutput") { - if (normalizeDeviceLabel(deviceInfos[i].label).includes(session.outputDevice)) { - session.sink = deviceInfos[i].deviceId; - log("AUDIO OUT DEVICE: " + deviceInfos[i].deviceId); - break; - } - } - } - }); - } catch (e) {} - } - - getById("headphonesDiv").classList.add("hidden"); - getById("headphonesDiv2").classList.add("hidden"); - } else if (session.sink) { - if (session.sink == "default") { - session.sink = false; - } else { - enumerateDevices().then(function (deviceInfos) { - var matched = false; - for (let i = 0; i !== deviceInfos.length; ++i) { - if (deviceInfos[i].kind === "audiooutput") { - if (deviceInfos[i].deviceId == session.sink) { - matched = true; - break; - } - } - } - if (!matched) { - session.sink = false; // make sure any saved output device exists. - } - }); - } - } - - if (session.studioSoftware || navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { - session.fullscreen = true; - } else if (urlParams.has("fullscreen")) { - session.fullscreen = true; - } - - if (urlParams.has("stats")) { - if (urlParams.get("stats") == "0") { - session.statsMenu = false; - } else if (urlParams.get("stats") == "false") { - session.statsMenu = false; - } else if (urlParams.get("stats") == "off") { - session.statsMenu = false; - } else { - session.statsMenu = true; - } - } else if (urlParams.has("nostats")) { - session.statsMenu = false; - } - - if (session.statsMenu === false) { - // hide menu option - try { - document.queryselector('[data-action="ShowStats"]').parentNode.classList.add("hidden"); - } catch (e) {} - } - - if (urlParams.has("statsinterval")) { - session.statsInterval = parseInt(urlParams.get("statsinterval")) || 3000; // milliseconds. interval of requesting stats of remote guests - } - - if (urlParams.has("cleandirector") || urlParams.has("cdv")) { - session.cleanDirector = true; - } - - if (urlParams.has("hidetranslate")) { - getById("translateButton").style.display = "none"; - } - - if (session.cleanOutput) { - session.screensharebutton = false; - getById("translateButton").style.display = "none"; - getById("credits").style.display = "none"; - // getById("legal").style.display = "none"; - getById("header").style.display = "none"; - getById("controlButtons").classList.add("hidden"); - getById("helpbutton").style.display = "none"; - getById("helpbutton").style.opacity = 0; - getById("reportbutton").style.display = "none"; - getById("reportbutton").style.opacity = 0; - getById("calendarButton").style.display = "none"; - getById("calendarButton").style.opacity = 0; - document.documentElement.style.setProperty("--myvideo-background", "#0000"); - var styleTmp = document.createElement("style"); - styleTmp.innerHTML = ` - video { - background-image: none; - } - `; - document.head.appendChild(styleTmp); - } - - if (urlParams.has("ssb") || urlParams.has("screensharebutton")) { - session.screensharebutton = true; - } - - if (urlParams.has("hideheader") || urlParams.has("noheader") || urlParams.has("hh")) { - // needs to happen the room and permaid applications - getById("header").style.display = "none"; - getById("header").style.opacity = 0; - getById("obsState").classList.add("noheader"); - } else if (urlParams.has("showheader")) { - // needs to happen the room and permaid applications - getById("header").style.display = "inherit"; - getById("header").style.opacity = 1; - setTimeout(function(){ - getById("header").classList.remove("hidden"); - getById("head2").classList.remove("hidden"); - },100); - } else if (session.studioSoftware) { - getById("header").style.display = "none"; - getById("header").style.opacity = 0; - } - - if (urlParams.has("minidirector")) { - try { - var cssStylesheet = document.createElement("link"); - cssStylesheet.rel = "stylesheet"; - cssStylesheet.type = "text/css"; - cssStylesheet.media = "screen"; - cssStylesheet.href = "minidirector.css"; - document.getElementsByTagName("head")[0].appendChild(cssStylesheet); - } catch (e) { - errorlog(e); - } - } - - if (urlParams.has("postinterval")) { - // interval to post snapimage images - session.postInterval = urlParams.get("postinterval") || session.postInterval; - session.postInterval = parseInt(session.postInterval) || 60; - if (session.postInterval < 5) { - session.postInterval = 5; - } - } - if (urlParams.has("postimage")) { - var postURL = decodeURIComponent(urlParams.get("postimage")) || session.postURL; // default will post to https://temp.vdo.ninja/images/STREAMIDHERE.jpg , at an interval. it will be cached unless using url params. - setInterval( - function (postURL) { - try { - uploadImageSnapshot(postURL); - } catch (e) {} - }, - session.postInterval * 1000, - postURL - ); - } - - if (urlParams.has("cleanish")) { - session.cleanish = true; - } - - if (session.cleanish || !session.cleanOutput) { - if (session.obsControls) { - getById("obscontrolbutton").classList.remove("hidden"); - getById("controlButtons").classList.remove("hidden"); - } - } - - if (urlParams.has("nocontrolbar")) { - getById("controlButtons").classList.add("hidden"); - getById("controlButtons").style.display = "none"; - session.dedicatedControlBarSpace = false; - } - - if (urlParams.has("channels")) { - // must be loaded before channelOffset - session.audioChannels = parseInt(urlParams.get("channels")) || 8; // for audio output ; not input. see: &channelcount instead. - session.offsetChannel = 0; - log("max channels is 32; channels offset"); - session.audioEffects = true; - } - if (urlParams.has("channeloffset")) { - session.offsetChannel = parseInt(urlParams.get("channeloffset")); - log("max channels is 32; channels offset"); - session.audioEffects = true; - } - if (urlParams.get("playchannel")) { - // must be loaded before channelOffset - session.playChannel = parseInt(urlParams.get("playchannel")); // for audio output ; not input. see: &channelcount instead. - session.audioEffects = true; - } - if (urlParams.has("enhance")) { - //if (parseInt(urlParams.get('enhance')>0){ - session.enhance = true; //parseInt(urlParams.get('enhance')); - //} - } - - if (urlParams.has("degrade")) { - session.degrade = urlParams.get("degrade") || true; // Firefox, and maybe Safari, supported I think. - // the possible values are maintain-framerate, maintain-resolution, or balanced. The default value is balanced - } - - if (urlParams.has("maxviewers") || urlParams.has("mv")) { - session.maxviewers = urlParams.get("maxviewers") || urlParams.get("mv"); - if (session.maxviewers.length == 0) { - session.maxviewers = 1; - } else { - session.maxviewers = parseInt(session.maxviewers); - } - log("maxviewers set"); - } - - if (urlParams.has("maxpublishers") || urlParams.has("mp")) { - session.maxpublishers = urlParams.get("maxpublishers") || urlParams.get("mp"); - if (session.maxpublishers.length == 0) { - session.maxpublishers = 1; - } else { - session.maxpublishers = parseInt(session.maxpublishers); - } - log("maxpublishers set"); - } - - if (urlParams.has("maxconnections") || urlParams.has("mc")) { - session.maxconnections = urlParams.get("maxconnections") || urlParams.get("maxconnections"); - if (session.maxconnections.length == 0) { - session.maxconnections = 1; - } else { - session.maxconnections = parseInt(session.maxconnections); - } - - log("maxconnections set"); - } - - if (urlParams.has("secure")) { - session.security = true; - if (!session.cleanOutput) { - delayedStartupFuncs.push([warnUser, "Enhanced Security Mode Enabled."]); - } - } - - if (urlParams.has("requireencryption")) { - session.requireencryption = true; - } - if (urlParams.has("unsafe")) { - session.unsafe = true; - } - - if (urlParams.has("random") || urlParams.has("randomize")) { - session.randomize = true; - } - - if (urlParams.has("frameRate") || urlParams.has("fr") || urlParams.has("fps")) { - session.frameRate = urlParams.get("frameRate") || urlParams.get("fr") || urlParams.get("fps"); - session.frameRate = parseInt(session.frameRate); - log("frameRate Changed"); - log(session.frameRate); - } - - if (urlParams.has("tz")) { - // being depreciated, but still works with meshcast (no longer turn) - session.tz = urlParams.get("tz"); - if (session.tz === null || session.tz === "") { - session.tz = false; - } else { - session.tz = parseInt(session.tz); - } - } - - if (urlParams.has("maxframerate") || urlParams.has("mfr") || urlParams.has("mfps")) { - session.maxframeRate = urlParams.get("maxframerate") || urlParams.get("mfr") || urlParams.get("mfps"); - session.maxframeRate = parseInt(session.maxframeRate); - log("max frameRate assigned"); - log(session.maxframeRate); - } - - if (urlParams.has("buffer") || urlParams.has("buffer2")) { - // needs to be before sync - if (ChromiumVersion > 50 && ChromiumVersion < 78) { - } else { - session.buffer = parseFloat(urlParams.get("buffer")) || parseFloat(urlParams.get("buffer2")) || 0; - log("buffer Changed: " + session.buffer); - } - if (urlParams.has("buffer2")) { - session.includeRTT = true; - } - } - - if (urlParams.has("panning") || urlParams.has("pan")) { - session.panning = urlParams.get("panning") || urlParams.get("pan"); - if (session.panning === "") { - session.panning = true; - } - session.audioEffects = true; - } - - if (urlParams.has("sync")) { - if (ChromiumVersion > 50 && ChromiumVersion < 78) { - } else { - session.sync = parseFloat(urlParams.get("sync")); - log("sync Changed; in milliseconds. If not set, defaults to auto."); - log(session.sync); - session.audioEffects = true; - if (session.buffer === false) { - session.buffer = 0; - } - } - } - - if (urlParams.has("nomirror")) { - session.nomirror = true; - } - - if (urlParams.has("mirror")) { - if (urlParams.get("mirror") == "3") { - getById("main").classList.add("mirror"); - } else if (urlParams.get("mirror") == "2") { - session.mirrored = 2; - } else if (urlParams.get("mirror") == "0") { - session.mirrored = 0; - } else if (urlParams.get("mirror") == "false") { - session.mirrored = 0; - } else if (urlParams.get("mirror") == "off") { - session.mirrored = 0; - } else { - session.mirrored = 1; - } - } - - if (urlParams.has("mirroroutput")) { - let mirrorOutputParam = urlParams.get("mirroroutput"); - let value = mirrorOutputParam ? mirrorOutputParam.toLowerCase() : ""; - let enableMirrorOutput = !(value === "0" || value === "false" || value === "off"); - session.mirrorOutput = enableMirrorOutput; - if (session.mirrorOutput) { - session.permaMirrored = true; - } else if (session.permaMirrored) { - session.permaMirrored = false; - } - } - - if (urlParams.has("flip")) { - if (urlParams.get("flip") == "0") { - session.flipped = false; - } else if (urlParams.get("flip") == "false") { - session.flipped = false; - } else if (urlParams.get("flip") == "off") { - session.flipped = false; - } else { - session.flipped = true; - } - } - - if (urlParams.has("flipoutput")) { - let flipOutputParam = urlParams.get("flipoutput"); - let value = flipOutputParam ? flipOutputParam.toLowerCase() : ""; - session.flipOutput = !(value === "0" || value === "false" || value === "off"); - if (session.flipOutput) { - session.flipped = true; - } - } - - if (session.mirrored && session.flipped) { - try { - log("Mirror all videos"); - var mirrorStyle = document.createElement("style"); - mirrorStyle.innerHTML = "video {transform: scaleX(-1) scaleY(-1); }"; - document.getElementsByTagName("head")[0].appendChild(mirrorStyle); - } catch (e) { - errorlog(e); - } - } else if (session.mirrored) { - // mirror the video horizontally - try { - log("Mirror all videos"); - var mirrorStyle = document.createElement("style"); - mirrorStyle.innerHTML = "video {transform: scaleX(-1);}"; - document.getElementsByTagName("head")[0].appendChild(mirrorStyle); - } catch (e) { - errorlog(e); - } - } else if (session.flipped) { - // mirror the video vertically - try { - log("Mirror all videos"); - var mirrorStyle = document.createElement("style"); - mirrorStyle.innerHTML = "video {transform: scaleY(-1);}"; - document.getElementsByTagName("head")[0].appendChild(mirrorStyle); - } catch (e) { - errorlog(e); - } - } - - if (urlParams.has("icefilter")) { - log("ICE FILTER ENABLED"); - session.icefilter = urlParams.get("icefilter"); - } - if (urlParams.has("lanonly")) { - session.localNetworkOnly = true; - session.configuration = { - sdpSemantics: session.sdpSemantics // future-proofing - }; - } - if (urlParams.has("stunonly")) { - session.stunOnly = true; - } - - // IPv6 handling: By default, prefer IPv4 over IPv6 when both are available. - // This helps on "half-broken" IPv6 networks where the IPv6 path is flaky. - // - &ipv6=0 (or &preferipv4): Disable IPv6 candidates if IPv4 exists (fallback to IPv6 if no IPv4) - // - &ipv6=1: Allow normal IPv6/IPv4 behavior (both used equally) - // - Default (no param): Prefer IPv4 by sending IPv4 candidates first, but still allow IPv6 - if (urlParams.has("ipv6")) { - var ipv6Value = urlParams.get("ipv6"); - if (ipv6Value === "0" || ipv6Value === "false") { - log("IPv6 disabled (will use IPv4 when available, fallback to IPv6 if needed)"); - session.disableIpv6 = true; - } else if (ipv6Value === "1" || ipv6Value === "true") { - log("IPv6 explicitly enabled (normal dual-stack behavior)"); - session.disableIpv6 = false; - session.preferIpv4 = false; - } - } else if (urlParams.has("preferipv4") || urlParams.has("ipv4")) { - log("IPv4 preferred: IPv6 candidates will be dropped if IPv4 exists"); - session.disableIpv6 = true; - } - // Note: By default, session.preferIpv4 is true (set in webrtc.js defaults) - // which reorders candidates to send IPv4 first but still allows IPv6. - - if (urlParams.has("activespeaker") || urlParams.has("speakerview") || urlParams.has("sas")) { - session.activeSpeaker = urlParams.get("activespeaker") || urlParams.get("speakerview") || urlParams.get("sas") || 1; - session.activeSpeaker = parseInt(session.activeSpeaker); - session.style = 6; - session.audioEffects = true; - //session.audioMeterGuest = true; - session.minipreview = 2; - if (session.activeSpeaker == 1 || session.activeSpeaker == 3) { - session.animatedMoves = false; - } - session.fadein = true; - document.querySelector(":root").style.setProperty("--fadein-speed", 0.5); - session.activeSpeakerInterval = setInterval(function () { - activeSpeaker(false); - }, 100); - } else if (urlParams.has("noisegate") || urlParams.has("gating") || urlParams.has("gate") || urlParams.has("ng")) { - session.quietOthers = urlParams.get("noisegate") || urlParams.get("gating") || urlParams.get("gate") || urlParams.get("ng") || 1; - session.quietOthers = parseInt(session.quietOthers); - - if (session.quietOthers == 1) { - session.quietOthers = false; - session.noisegate = true; - session.audioEffects = true; - //session.audioMeterGuest = true; - } else if (session.quietOthers == 4) { - session.quietOthers = 1; - session.audioEffects = true; - //session.audioMeterGuest = true; - session.activeSpeakerInterval = setInterval(function () { - activeSpeaker(false); - }, 100); - } else if (!session.quietOthers) { - session.noisegate = false; - session.quietOthers = false; - } else { - session.audioEffects = true; - //session.audioMeterGuest = true; - session.activeSpeakerInterval = setInterval(function () { - activeSpeaker(false); - }, 100); - } - } - if (urlParams.has("activespeakerdelay") || urlParams.has("speakerviewdelay") || urlParams.has("sasdelay")) { - session.activeSpeakerTimeout = urlParams.get("activespeakerdelay") || urlParams.get("speakerviewdelay") || urlParams.get("sasdelay") || 0; - session.activeSpeakerTimeout = parseInt(session.activeSpeakerTimeout); - } - - if (urlParams.has("noisegatesettings")) { - session.noisegateSettings = urlParams.get("noisegatesettings"); - session.noisegateSettings = session.noisegateSettings.split(","); - } - - if (urlParams.has("fadein")) { - session.fadein = true; - if (urlParams.get("fadein") || 0) { - try { - var fadeinspeed = parseInt(urlParams.get("fadein") || 0) / 1000.0; - fadeinspeed += "s"; - document.querySelector(":root").style.setProperty("--fadein-speed", fadeinspeed); - } catch (e) { - errorlog("variable css failed"); - } - } else { - try { - var fadeinspeed = 0.5; - fadeinspeed += "s"; - document.querySelector(":root").style.setProperty("--fadein-speed", fadeinspeed); - } catch (e) { - errorlog("variable css failed"); - } - } - } - - if (urlParams.has("widget")) { - session.widget = urlParams.get("widget") || false; - - if (session.widget === "false" || session.widget === "0" || session.widget === "off") { - session.noWidget = true; - session.widget = false; - } else if (session.widget) { - session.widget = decodeURI(session.widget) || false; - log(session.widget); - } - } - - if (urlParams.has("widgetleft")) { - session.widgetleft = true; - } - if (urlParams.get("widgetwidth")) { // default is 25% - try{ - session.widgetwidth = parseFloat(urlParams.get("widgetwidth")) || 25; - - if (session.widgetwidth>50){ - session.widgetwidth = 50; - } - - document.querySelector(":root").style.setProperty("--widget-width", session.widgetwidth+"%"); - - - } catch(e){ - errorlog(e); - } - } - - if (urlParams.has("animated") || urlParams.has("animate")) { - session.animatedMoves = urlParams.get("animated") || urlParams.get("animate"); - if (session.animatedMoves === "false") { - session.animatedMoves = false; - } else if (session.animatedMoves === "0") { - session.animatedMoves = false; - } else if (session.animatedMoves === "no") { - session.animatedMoves = false; - } else if (session.animatedMoves === "off") { - session.animatedMoves = false; - } else { - session.animatedMoves = parseInt(session.animatedMoves) || 100; - } - if (session.animatedMoves > 200) { - session.animatedMoves = 200; - } - } else if (session.mobile) { - session.animatedMoves = false; - } - - if (urlParams.has("meter") || urlParams.has("meterstyle")) { - // same as also adding &style=3 - session.meterStyle = urlParams.get("meter") || urlParams.get("meterstyle") || 1; - session.meterStyle = parseInt(session.meterStyle); - if (session.meterStyle < 4) { - session.style = 3; // black canvas - } else { - session.style = -1; // no canvas - } - session.audioEffects = true; - } - - if (session.meterStyle == 5) { - document.documentElement.style.setProperty("--video-background-image-size-talking", "auto 35%"); - document.documentElement.style.setProperty("--video-background-image-size-screaming", "auto 45%"); - } - - if (urlParams.has("directorchat") || urlParams.has("dc")) { - session.directorChat = true; - } - - if (urlParams.has("style") || urlParams.has("st")) { - session.style = urlParams.get("style") || urlParams.get("st"); - if (parseInt(session.style) === 0 || session.style == "controls") { - // no audio only - session.style = 0; - } else if (parseInt(session.style) == 1 || session.style == "justvideo") { - // no audio only - session.style = 1; - } else if (parseInt(session.style) == 2 || session.style == "waveform") { - // audio waveform - session.style = 2; - session.audioEffects = true; ////!!!!!!! Do I want to enable the audioEffects myself? or do it here? - } else if (parseInt(session.style) == 3 || session.style == "volume") { - // audio meter ; see &meterstyle , where optios include default(false), 1, and 2. - session.style = 3; - session.audioEffects = true; - } else if (parseInt(session.style) == 4) { - // black background - session.style = 4; - } else if (parseInt(session.style) == 5) { - // random colored background - session.style = 5; - } else if (parseInt(session.style) == 7) { - // shows video elements for all connections; even those without video/audio - session.style = parseInt(session.style); - session.showall = true; - } else if (parseInt(session.style)) { - // 6 is the first letter of the name, surrounded with a colored circle - session.style = parseInt(session.style); - } else { - session.style = 1; - } - } - //if (session.style){ - // getById("toggleWaveformButton").classList.remove("hidden"); - //} - - if (urlParams.has("showall")) { - // just an alternative; might be compoundable - session.showall = true; - } - - if (urlParams.has("samplerate") || urlParams.has("sr")) { - // playout sample rate - session.sampleRate = parseInt(urlParams.get("samplerate")) || parseInt(urlParams.get("samplerate")) || 48000; - if (session.audioCtx) { - session.audioCtx.close(); // close the default audio context. - } - session.audioCtx = new AudioContext({ - // create a new audio context with a higher sample rate. - sampleRate: session.sampleRate // default is 48000 already - }); - session.audioEffects = true; - } - - if (session.audioCodec === "lyra") { - // WIP. does not work - try { - var { default: Module } = await import("./thirdparty/lyra/webassembly_codec_wrapper.js"); - await Module() - .then(module => { - console.log("Initialized codec's wasmModule."); - session.lyraCodecModule = module; - }) - .catch(e => { - console.log(`Module() error: ${e.name} message: ${e.message}`); - }); - } catch (e) { - errorlog(e); - } - if (session.lyraCodecModule) { - console.log("Lyra module loaded"); - session.micSampleRate = 16000; - session.encodedInsertableStreams = "lyra"; - } else { - console.log("Lyra module failed to load"); - } - } - - if (urlParams.has("e2ee")) { - session.encodedInsertableStreams = "e2ee"; - } else if (urlParams.has("insertablestreams") || urlParams.has("is")) { - session.encodedInsertableStreams = urlParams.get("insertablestreams") || urlParams.get("is") || true; - } - - if (urlParams.has("outboundsamplerate") || urlParams.has("obsr")) { - session.outboundSampleRate = parseInt(urlParams.get("outboundsamplerate")) || parseInt(urlParams.get("obsr")) || false; // default null - } else { - session.outboundSampleRate = null; // tmp - } - - if (urlParams.has("micsamplerate") || urlParams.has("msr")) { - session.micSampleRate = parseInt(urlParams.get("micsamplerate")) || parseInt(urlParams.get("msr")) || 48000; - } - - if (urlParams.has("micsamplesize")) { - session.micSampleSize = parseInt(urlParams.get("micsamplesize")) || 16; - } - - if (urlParams.has("noaudioprocessing") || urlParams.has("noap")) { - session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? - session.disableViewerWebAudioPipeline = true; // this has the potential to break things. - session.audioEffects = false; // disable audio inbound effects also. - session.audioMeterGuest = false; - if (session.noisegate === null) { - session.noisegate = false; - } - } - - // For info, see this: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/availableOutgoingBitrate - if (urlParams.has("maxbandwidth")) { - // limits the bitrate based on the outbound total available bandwidth; chromium-based - session.maxBandwidth = urlParams.get("maxbandwidth") || 80; // 0 to 100; will reduce bitrate as a percentage of available - session.maxBandwidth = parseInt(session.maxBandwidth); - if (session.maxBandwidth > 200) { - // will over ride default 2500kbps if no bitrate is specified - session.maxBandwidth = 200; - } else if (session.maxBandwidth < 0) { - session.maxBandwidth = 0; - } - } - - if (urlParams.has("iframetarget")) { - session.iframetarget = urlParams.get("iframetarget"); // speciifies the IFRAME Hostname target - if (session.iframetarget) { - session.iframetarget = decodeURIComponent(session.iframetarget); - } else { - session.iframetarget = (window.location.protocol === "file:") ? "*" : window.location.origin; - } - } - - if (urlParams.has("sendframes")) { - session.sendframes = urlParams.get("sendframes"); - if (session.sendframes) { - try { - session.sendframes = decodeURIComponent(session.sendframes); - } catch (e) {} - } else { - session.sendframes = session.iframetarget || "*"; - } - } - - if (urlParams.has("tcp")) { - // forces the TURN servers to use TCP mode; still need to add &private to force TURN also tho - session.forceTcpMode = true; - } - - if (urlParams.has("stun")) { - var stunstring = urlParams.get("stun"); - stunstring = stunstring.split(";"); - if (stunstring[0] !== "false") { - // false disables the TURN server. Useful for debuggin - var stun = {}; - if (stunstring.length == 3) { - stun.username = stunstring[0]; // myusername - stun.credential = stunstring[1]; //mypassword - stun.urls = [stunstring[2]]; // ["turn:turn.obs.ninja:443"]; - } else if (stunstring.length == 1) { - stun.urls = [stunstring[0]]; - } - session.stunServers = [stun]; - } else { - session.stunServers = []; - } - } - if (urlParams.has("addstun")) { - var stunstring = urlParams.get("addstun"); - stunstring = stunstring.split(";"); - var stun = {}; - if (stunstring.length == 3) { - stun.username = stunstring[0]; // myusername - stun.credential = stunstring[1]; //mypassword - stun.urls = [stunstring[2]]; // ["turn:turn.obs.ninja:443"]; - } else if (stunstring.length == 1) { - stun.urls = [stunstring[0]]; - } - session.stunServers = session.stunServers.concat(stun); - } - - if (urlParams.has("bundle")) { - session.bundlePolicy = urlParams.get("bundle") || "max-bundle"; // default is browser default. - } - - if (urlParams.has("planb")) { - session.sdpSemantics = "plan-b"; // for legacy support, or debuggin, or whatever. - } - - if (urlParams.has("turn")) { - var turnstring = urlParams.get("turn"); - - if (turnstring == "twilio") { - // a sample function on loading remote credentials for TURN servers. - try { - session.ws = false; // prevents connection - var twillioRequest = new XMLHttpRequest(); - twillioRequest.onload = function () { - if (this.status === 200) { - try { - var res = JSON.parse(this.responseText); - } catch (e) { - console.error(e); - return; - } - session.configuration = { - iceServers: [ - { - username: res["1"], - credential: res["2"], - url: "turn:global.turn.twilio.com:3478?transport=tcp", - urls: "turn:global.turn.twilio.com:3478?transport=tcp" - }, - { - username: res["1"], - credential: res["2"], - url: "turn:global.turn.twilio.com:443?transport=tcp", - urls: "turn:global.turn.twilio.com:443?transport=tcp" - } - ], - sdpSemantics: session.sdpSemantics // future-proofing - }; - if (session.ws === false) { - session.ws = null; // allows connection (clears state) - session.connect(); // connect if not already connected. - } - } - // system does not connect if twilio API does not respond. - }; - twillioRequest.open("GET", "https://turn.example.com:443/twilio", true); // `false` makes the request synchronous - twillioRequest.send(); - } catch (e) { - errorlog("Twilio Failed"); - } - } else if (turnstring == "nostun") { - // disable TURN servers - session.configuration = { - sdpSemantics: session.sdpSemantics // future-proofing - }; - } else if (turnstring == "false" || turnstring == "off" || turnstring == "0") { - // disable TURN servers - session.configuration = { - iceServers: session.stunServers, - sdpSemantics: session.sdpSemantics // future-proofing - }; - } else { - try { - //session.configuration = {iceServers: [], sdpSemantics: session.sdpSemantics}; - turnstring = turnstring.split(";"); - if (turnstring !== "false") { - // false disables the TURN server. Useful for debuggin - var turn = {}; - if (turnstring.length == 3) { - turn.username = turnstring[0]; // myusername - turn.credential = turnstring[1]; //mypassword - turn.urls = [turnstring[2]]; // ["turn:turn.obs.ninja:443"]; - } else if (turnstring.length == 1) { - turn.urls = [turnstring[0]]; - } - session.configuration = { - iceServers: session.stunServers, - sdpSemantics: session.sdpSemantics // future-proofing - }; - - session.configuration.iceServers.push(turn); - } - } catch (e) { - if (!session.cleanOutput) { - warnUser("TURN server parameters were wrong."); - } - errorlog(e); - } - } - } - - if (urlParams.has("apiserver") && urlParams.get("apiserver")) { - // must set this after any custom TURN / STUN settings, else it might over-ride them. - session.apiserver = urlParams.get("apiserver"); - } - - if (urlParams.has("speedtest")) { - // must set this after any custom TURN / STUN settings, else it might over-ride them. - session.speedtest = true; - if (urlParams.get("speedtest")) { - // forces essentially UDP mode, unless TCP is specified, and some other stuff - session.speedtest = urlParams.get("speedtest").toLowerCase(); // also limits bitrate - } - setupSpeedtest(); - } - - if (urlParams.has("privacy") || urlParams.has("private") || urlParams.has("relay")) { - // please only use if you are also using your own TURN service. - session.privacy = urlParams.get("privacy") || urlParams.get("private") || urlParams.get("relay") || true; - - try { - // I'll re-apply this in the setupSpeedtest() promise callback, just to be case. - if (session.configuration) { - // this needs to set last, otherwise it might be overridden - session.configuration.iceTransportPolicy = "relay"; // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/address - } - } catch (e) { - if (!session.cleanOutput) { - warnUser("Privacy mode failed to configure."); - } - errorlog(e); - } - - if (session.speedtest) { - warnlog("Bitrate being throttled to max of 6000 kbps"); - if (session.maxvideobitrate !== false) { - if (session.maxvideobitrate > 6000) { - session.maxvideobitrate = 6000; // Please feel free to get rid of this if using your own TURN servers... - } - } else { - session.maxvideobitrate = 6000; // don't let people pull more than 6000 from you - } - if (session.bitrate !== false) { - if (session.bitrate > 6000) { - session.bitrate = 6000; // Please feel free to get rid of this if using your own TURN servers... - } - } - } else { - warnlog("Bitrate being throttled to max of 4000 kbps"); - if (session.maxvideobitrate !== false) { - if (session.maxvideobitrate > 4000) { - session.maxvideobitrate = 4000; // Please feel free to get rid of this if using your own TURN servers... - } - } else { - session.maxvideobitrate = 4000; // don't let people pull more than 4000 from you - } - if (session.bitrate !== false) { - if (session.bitrate > 4000) { - session.bitrate = 4000; // Please feel free to get rid of this if using your own TURN servers... - } - } - } - } - - if (urlParams.has("osc") || urlParams.has("api")) { - if (urlParams.get("osc") || urlParams.get("api")) { - session.api = urlParams.get("osc") || urlParams.get("api") || false; - if (session.api) { - setTimeout(function () { - oscClient(); - }, 1000); - } - } - } - - if (urlParams.has("postapi") || urlParams.has("posturl")) { - session.postApi = urlParams.get("postapi") || urlParams.get("posturl") || false; // ie: &postapi=https%3A%2F%2Fwebhook.site%2Fb190f5bf-e4f8-454a-bd51-78b5807df9c1 - if (session.postApi) { - try { - session.postApi = decodeURI(session.postApi) || session.postApi; // needs to be SSL enabled. - } catch (e) { - console.error(e); - } - } - } - - if (urlParams.has("queue")) { - session.queue = true; - if (urlParams.get("queue") === "false") { - session.queue = false; - } else if (urlParams.get("queue") === "0") { - session.queue = false; - } else if (urlParams.get("queue") === "off") { - session.queue = false; - } else if (urlParams.get("queue")) { - session.queue = urlParams.get("queue"); - } - } - - if (urlParams.has("queue2") || urlParams.has("screen")) { - // the guest can see the director, if the director doesn't have &queue - session.queue = true; - session.queueType = 2; - } - - if (urlParams.has("queue3") || urlParams.has("hold")) { - // &hold (alias: &queue3) - Full bidirectional isolation until activated. - // - // - Guest cannot see director or other guests - // - Director cannot see guest's video/audio (only control box with label) - // - Other guests cannot see the hold guest - // - On activation, all directions open and normal flow resumes - // - // Technical: Sets needsPublishing=true, skips initialPublish until activated. - // Use case: Green room / screening where director doesn't want to be seen either. - session.queue = true; - session.queueType = 3; - } - - if (urlParams.has("queue4") || urlParams.has("holdwithvideo")) { - // &holdwithvideo (alias: &queue4) - Like &hold but allows Guest→Director media. - // - // - Guest cannot see director or other guests (still isolated) - // - Director CAN see guest's video/audio (for preview/screening) - // - Other guests cannot see the hold guest - // - On activation, remaining directions open - // - // IMPORTANT: The name "holdwithvideo" is slightly misleading. It does NOT force - // video to be sent. It simply removes the publishing block that &hold creates. - // The actual video/audio that flows is still determined by: - // - What the director requests ({video: true/false, audio: true/false}) - // - Room-level rules (&novideo, &nodirectorvideo, etc.) - // - All normal gating logic - // - // Technical: Calls initialPublish normally (unlike queue3), respects allowVideo/allowAudio. - // Use case: Director wants to preview guest (check lighting, verify identity) before admission. - session.queue = true; - session.queueType = 4; - } - - if (session.director && (urlParams.has("approvepopup") || urlParams.has("approvalpopup"))) { - // Opt-in approval popup for directors - session.approval_popup = true; - try { log("[flags] &approvepopup detected; approval_popup=true"); } catch (e) {} - } - // do not reference stream ID before this point, as it might change after this point. - - if (urlParams.has("push") || urlParams.has("id") || urlParams.has("permaid") || (session.sticky && session.decrypted)) { - session.permaid = urlParams.get("push") || urlParams.get("id") || urlParams.get("permaid"); - - if (session.permaid) { - session.permaid = sanitizeStreamID(session.permaid) || null; - session.streamID = session.permaid || session.streamID; - } else if ((urlParams.has("permaid") || (session.sticky && session.decrypted)) && getStorage("permaid")) { - session.streamID = sanitizeStreamID(getStorage("permaid")) || session.streamID; - session.permaid = null; - } else { - session.permaid = null; - } - - if (session.permaid && ((session.permaid.length<3) || (session.permaid==="test"))) { - if (session.password === session.defaultPassword) { - if (location.hostname === "vdo.ninja") { - if (!session.cleanOutput){ - window.focus(); - warnUser(getTranslation("insecure-stream-id"),10000); - } - } - } - } - - if (urlParams.has("permaid") || (session.sticky && session.decrypted)) { - setStorage("permaid", session.streamID, 99999); - } - - if (urlParams.has("push")) { - updateURL("push=" + session.streamID, true, false); - } else if (urlParams.has("id")) { - updateURL("id=" + session.streamID, true, false); // not 'officially' supporting this yet; we'll see. - } else if (urlParams.has("permaid")) { - updateURL("permaid=" + session.streamID, true, false); - } else { - updateURL("push=" + session.streamID, true, false); - } - - if (session.director) { - // if I do a short form of this, it will cause duplications in the code elsewhere. - //var director_room_input = urlParams.get('director'); - //director_room_input = sanitizeRoomName(director_room_input); - //createRoom(director_room_input); - session.permaid = false; // used to avoid a trigger later on. - } else { - getById("container-1").className = "column columnfade hidden"; - getById("container-4").className = "column columnfade hidden"; - getById("dropButton").className = "column columnfade hidden"; - - getById("info").innerHTML = ""; - if (session.videoDevice === 0) { - miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); - } else { - miniTranslate(getById("add_camera"), "share-your-camera", "Share your Camera"); - } - miniTranslate(getById("add_screen"), "share-your-screen", "Share your Screen"); - getById("container-2").title = getById("add_screen").innerText; - getById("container-3").title = getById("add_camera").innerText; - - getById("passwordRoom").value = ""; - getById("videoname1").value = ""; - getById("dirroomid").innerHTML = ""; - getById("roomid").innerHTML = ""; - - getById("mainmenu").style.alignSelf = "center"; - getById("mainmenu").classList.add("mainmenuclass"); - getById("header").style.alignSelf = "center"; - - //if ((iOS) || (iPad)) { - //getById("header").style.display = "none"; // just trying to free up space. - //} - - if (session.webcamonly == true) { - // mobile or manual flag 'webcam' pflag set - getById("head1").innerHTML = '- Please accept any camera permissions'; - } else { - getById("head1").innerHTML = '
    - Please select which you wish to share'; - } - - if (!session.cleanOutput) { - try { - if (session.studioSoftware) { - getById("unexpectedPushLink").classList.remove("hidden"); - } - } catch (e) {} - } - } - } - - if (window.vdoAuth){ - if (session.streamID) { - await window.vdoAuth.assignStream(); - } - getById("mainmenu").classList.remove("hidden2"); - getById("header").classList.remove("hidden2"); - } - - if (session.roomid || urlParams.has("roomid") || urlParams.has("r") || urlParams.has("room") || filename || session.permaid !== false) { - var roomid = ""; - if (urlParams.has("room")) { - // needs to be first; takes priority - roomid = urlParams.get("room"); - } else if (urlParams.has("roomid")) { - roomid = urlParams.get("roomid"); - } else if (urlParams.has("r")) { - roomid = urlParams.get("r"); - } else if (session.roomid) { - roomid = session.roomid; - } else if (filename) { - roomid = filename; - } - session.roomid = sanitizeRoomName(roomid); - if (session.director) { - if (session.director !== session.roomid) { - if (!session.cleanOutput) { - warnUser("Conflicting director and room values were provided.\n\n Check your URL parameters; there should be only &director OR &room", 5000); - } - } - session.roomid = false; - } - if (session.quality===false){ - try { - document.getElementById("webcamquality").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : (session.quality_wb || 0)); - document.getElementById("webcamquality3").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : (session.quality_wb || 0)); - } catch(e){} - } - - } else if (session.quality===false){ - try { - document.getElementById("webcamquality").elements.namedItem("resolution").value = session.quality_wb || 0; - document.getElementById("webcamquality3").elements.namedItem("resolution").value = session.quality_wb || 0; - } catch(e){} - } - - if (session.permaid === false && session.roomid === false && session.view === false && session.effect === false && session.director === false) { - session.effect = null; - getById("credits").innerHTML = "Version: " + session.version + ' | ' + getById("credits").innerHTML; - } - - if (session.mobile && session.permaid === false && !session.roomid) { - getById("rememberStreamID").classList.remove("hidden"); - - let rememberStreamIDmobile = getStorage("rememberStreamIDmobile"); - if (rememberStreamIDmobile === "false") { - getById("rememberStreamIDcheck").checked = false; - } - } - - if (urlParams.has("hostwhep") || urlParams.has("whepout")) { - session.whepHost = urlParams.get("hostwhep") || urlParams.get("whepout") || session.streamID || false; - } - - // General WHIP output toggles - if (urlParams.has("whipnoscreen")) { - session.whipPublishScreen = false; - } - if (urlParams.has("whipprimaryonly")) { - session.whipPublishPrimary = true; - session.whipPublishScreen = false; - } - if (urlParams.has("whipscreenonly")) { - session.whipPublishPrimary = false; - session.whipPublishScreen = true; - } - - if (urlParams.get("mediamtx")){ - session.mediamtx = urlParams.get("mediamtx"); - } - - if (session.mediamtx){ - if (urlParams.has("mediamtxnoscreen")) { - session.whipPublishScreen = false; - } - if (urlParams.has("mediamtxscreenonly")) { - session.whipPublishPrimary = false; - session.whipPublishScreen = true; - } - if (!session.mediamtx.includes(".") && !session.mediamtx.includes("localhost")){ - session.mediamtx += ".com"; - } - if (!session.mediamtx.includes(":")){ - session.mediamtx += ":8889"; - } - let mediamtxBase = session.mediamtx; - let scheme = "https://"; - if (mediamtxBase.startsWith("http://") || mediamtxBase.startsWith("https://")) { - scheme = ""; - } else if (mediamtxBase.startsWith("localhost:")) { - scheme = "http://"; - } - if (scheme){ - mediamtxBase = scheme + mediamtxBase; - } - if (mediamtxBase.endsWith("/")) { - mediamtxBase = mediamtxBase.slice(0, -1); - } - const streamId = session.streamID; - const screenId = streamId + "_s"; - const buildEndpoint = id => `${mediamtxBase}/${id}`; - - const primaryWhipUrl = `${buildEndpoint(streamId)}/whip`; - const primaryWhepSettings = { - type: "whep", - url: `${buildEndpoint(streamId)}/whep`, - token: streamId, - media: "primary", - started: false - }; - const screenWhipUrl = `${buildEndpoint(screenId)}/whip`; - const screenWhepSettings = { - type: "whep", - url: `${buildEndpoint(screenId)}/whep`, - token: screenId, - media: "screen", - started: false - }; - - if (session.whipPublishPrimary) { - if (!session.whipOutputUserSet) { - session.whipOutput = primaryWhipUrl; - } - if (!session.whipoutSettingsUserSet) { - session.whipoutSettings = primaryWhepSettings; - } - } else { - if (!session.whipOutputUserSet) { - session.whipOutput = false; - } - if (!session.whipoutSettingsUserSet) { - session.whipoutSettings = false; - } - } - - if (session.whipPublishScreen) { - if (!session.whipOutputScreenUserSet) { - session.whipOutputScreen = screenWhipUrl; - } - if (!session.whipoutScreenSettingsUserSet) { - session.whipoutScreenSettings = screenWhepSettings; - } - } else { - if (!session.whipOutputScreenUserSet) { - session.whipOutputScreen = false; - } - if (!session.whipoutScreenSettingsUserSet) { - session.whipoutScreenSettings = false; - } - } - - if (session.whipPublishPrimary && session.whipoutSettings) { - console.log("WHIP OUT: " + session.whipOutput + ", WHEP SHARE: " + session.whipoutSettings.url); - } - if (session.whipPublishScreen && session.whipoutScreenSettings) { - console.log("WHIP OUT SCREEN: " + session.whipOutputScreen + ", WHEP SHARE: " + session.whipoutScreenSettings.url); - } - if (session.whipPublishScreen && session.whipOutputScreen && session.screenShareState) { - whipOutScreen(); - } - if (session.stereo === false){ - if (!session.whipOutAudioCodec || (session.whipOutAudioCodec=="opus")){ - session.stereo=3; - } - } - } - - if (urlParams.has("meshcastnoscreen")) { - session.whipPublishScreen = false; - } - if (urlParams.has("meshcastscreenonly")) { - session.whipPublishPrimary = false; - session.whipPublishScreen = true; - } - if (urlParams.has("meshcastprimaryonly")) { - session.whipPublishPrimary = true; - session.whipPublishScreen = false; - } - - if (urlParams.has("effects") || urlParams.has("effect")) { - session.effect = urlParams.get("effects") || urlParams.get("effect") || null; - } else if (urlParams.has("digitalzoom")) { - session.effect = "7"; - if (urlParams.get("digitalzoom")){ - session.effectValue_default = parseFloat(urlParams.get("digitalzoom")) || 1; - } - } - - if (session.effect && !session.cleanOutput) { - if (ChromiumVersion && ChromiumVersion === 122) { - warnUser("⚠️ Notice: A recent update to Chrome/Edge can cause the browser to crash, especially when using &effects or &zoom.\n\nBrowser updates are rolling out to fix the issue, however avoiding the use of digital video effects for now might be prudent", 30000); - } - } - - if (urlParams.get("zoom")){ - session.zoom = parseFloat(urlParams.get("zoom")) || false; - session.ptz = true; - } - if (urlParams.get("wb") || urlParams.get("whitebalance")) { - session.whiteBalance = urlParams.get("wb") || urlParams.get("whitebalance"); - } - if (urlParams.get("exposure")) { - session.exposure = urlParams.get("exposure"); - } - if (urlParams.get("saturation")) { - session.saturation = urlParams.get("saturation"); - } - if (urlParams.get("sharpness")) { - session.sharpness = urlParams.get("sharpness"); - } - if (urlParams.get("contrast")) { - session.contrast = urlParams.get("contrast"); - } - if (urlParams.get("brightness")) { - session.brightness = urlParams.get("brightness"); - } - if (urlParams.get("focus")) { - session.focusDistance = urlParams.get("focus"); - } - - - if (urlParams.has("wss")) { - session.customWSS = true; - session.wssSetViaUrl = true; - if (urlParams.get("wss")) { - session.wss = urlParams.get("wss"); - if (!session.wss.startsWith("wss://")) { - session.wss = "wss://" + session.wss; - } - } - } else if (urlParams.has("wss2")) { - session.wssSetViaUrl = true; - if (urlParams.get("wss2")) { - session.wss = urlParams.get("wss2"); - if (!session.wss.startsWith("wss://")) { - session.wss = "wss://" + session.wss; - } - } - } else if (urlParams.get("audience")) { - session.audience = urlParams.get("audience"); - if (urlParams.get("audience") && session.view !== false) { - session.wss = "wss://audience.vdo.ninja/listen/" + session.audience; - } else { - session.wss = "wss://audience.vdo.ninja/publish/" + session.audience; - } - } - - if (urlParams.has("bypass")) { - session.bypass = true; - if (!urlParams.get("bypass")){ - session.customWSS = true; - } - } - - if (window.FaceDetector !== undefined) { - document.querySelectorAll(".facetracker").forEach(ele => { - ele.disabled = null; - ele.removeAttribute("disabled"); - ele.title = "Will slowly pan, tilt, and zoom in on the first face detected"; - }); - } - - if (urlParams.has("imagelist")) { - // "&imagelist="+encodeURIComponent(JSON.stringify(["./media/bg_sample.webp", "./media/bg_sample2.webp"])) - var imageList = urlParams.get("imagelist"); // - if (imageList) { - try { - imageList = JSON.parse(decodeURIComponent(imageList)); - if (imageList.length) { - session.defaultBackgroundImages = imageList; // ["./media/bg_sample.webp", "./media/bg_sample2.webp"] - } else { - warnlog("empty image array; skipping"); - } - } catch (e) { - console.error(e); - try { - imageList = decodeURIComponent(imageList); - } catch (e) { - console.error(e); - } - if (imageList) { - session.defaultBackgroundImages = [imageList]; // ["./media/bg_sample.webp", "./media/bg_sample2.webp"] - } else { - warnlog("empty image array; skipping"); - } - } - } - } - - if (session.effect !== false) { - if (session.effect === null) { - getById("effectsDiv").style.display = "inline-block"; - session.effect = "0"; - } else if (session.effect === "0" || session.effect === "false" || session.effect === "off" || session.effect === 0) { - session.effect = false; - getById("effectSelector3").style.display = "none"; - getById("effectsDiv3").style.display = "none"; - getById("effectSelector").style.display = "none"; - getById("effectsDiv").style.display = "none"; - } - - if (session.effect === "5") { - loadContentEffectsImages(); - - getById("effectSelector").style.display = "none"; - getById("effectsDiv").style.display = "inline-block"; - } - if (session.effect === "3a") { - // heavier blur - session.effectValue = 5; - session.effect = "3"; - } else if (session.effect === "3") { - session.effectValue = 2; - } else if (session.effect === "7") { - session.effectValue = session.effectValue || 1; - } else if (["15", "14"].includes(session.effect)) { - session.effectValue = 25; - getById("effectSelector").style.display = "none"; - getById("effectsDiv").style.display = "inline-block"; - loadContentEffectsImages(); - } - // mirror == 2 - // face == 1 - // blur = 3 - // green = 4 - // image = 5 - } - - if (urlParams.has("effectvalue") || urlParams.has("ev")) { - session.effectValue = parseFloat(urlParams.get("effectvalue")) || parseFloat(urlParams.get("ev")) || 0; - session.effectValue_default = session.effectValue; - } - - if (session.webcamonly == true) { - if (session.introButton) { - getById("container-2").className = "column columnfade hidden"; // Hide screen share - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - } else { - getById("container-2").className = "column columnfade hidden"; // Hide screen share - getById("container-3").classList.add("skip-animation"); - getById("container-3").classList.remove("pointer"); - delayedStartupFuncs.push([previewWebcam]); - } - } - if (session.introOnClean && session.permaid === false && session.roomid === false) { - //getById("container-2").className = 'column columnfade hidden'; // Hide screen share - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - } else if (session.introOnClean && session.scene === false && (session.permaid !== false || session.roomid !== false)) { - getById("container-2").className = "column columnfade hidden"; // Hide screen share - getById("container-3").classList.add("skip-animation"); - getById("container-3").classList.remove("pointer"); - delayedStartupFuncs.push([previewWebcam]); - } - - //if (!session.director && ((ChromiumVersion == 86) || (ChromiumVersion == 77) || (ChromiumVersion == 62) || (ChromiumVersion == 51)) && (((session.permaid===false) && session.view) || (session.scene!==false))){ - // session.studioSoftware = true; // vmix - - if (session.cleanViewer) { - if (((session.view!==false) || session.whepInput || session.whipView) && !session.director && session.permaid === false) { - session.cleanOutput = true; - } - } - if (urlParams.has("clock") || urlParams.has("clock24")) { - let urlClock = urlParams.get("clock") || urlParams.get("clock24"); - if (urlParams.has("clock24")) { - session.clock24 = true; - } - session.showTime = true; - if (urlClock === "false") { - session.showTime = false; - } else if (urlClock === "0") { - session.showTime = false; - } else if (urlClock === "1") { - getById("overlayClockContainer2").classList.add("top"); - getById("overlayClockContainer2").classList.add("left"); - } else if (urlClock === "7") { - getById("overlayClockContainer2").classList.add("bottom"); - getById("overlayClockContainer2").classList.add("left"); - } else if (urlClock === "4") { - getById("overlayClockContainer2").classList.add("vmiddle"); - getById("overlayClockContainer2").classList.add("left"); - } else if (urlClock === "2") { - getById("overlayClockContainer2").classList.add("top"); - getById("overlayClockContainer2").classList.add("hmiddle"); - } else if (urlClock === "8") { - getById("overlayClockContainer2").classList.add("bottom"); - getById("overlayClockContainer2").classList.add("hmiddle"); - } else if (urlClock === "5") { - getById("overlayClockContainer2").classList.add("vmiddle"); - getById("overlayClockContainer2").classList.add("hmiddle"); - } else if (urlClock === "3") { - getById("overlayClockContainer2").classList.add("top"); - getById("overlayClockContainer2").classList.add("right"); - } else if (urlClock === "9") { - getById("overlayClockContainer2").classList.add("bottom"); - getById("overlayClockContainer2").classList.add("right"); - } else if (urlClock === "6") { - getById("overlayClockContainer2").classList.add("vmiddle"); - getById("overlayClockContainer2").classList.add("right"); - } - } else if (session.cleanOutput) { - session.showTime = false; - } - - if (urlParams.has("timer")) { - if (urlParams.get("timer") === "1") { - getById("overlayClockContainer").classList.add("top"); - getById("overlayClockContainer").classList.add("left"); - } else if (urlParams.get("timer") === "7") { - getById("overlayClockContainer").classList.add("bottom"); - getById("overlayClockContainer").classList.add("left"); - } else if (urlParams.get("timer") === "4") { - getById("overlayClockContainer").classList.add("vmiddle"); - getById("overlayClockContainer").classList.add("left"); - } else if (urlParams.get("timer") === "2") { - getById("overlayClockContainer").classList.add("top"); - getById("overlayClockContainer").classList.add("hmiddle"); - } else if (urlParams.get("timer") === "8") { - getById("overlayClockContainer").classList.add("bottom"); - getById("overlayClockContainer").classList.add("hmiddle"); - } else if (urlParams.get("timer") === "5") { - getById("overlayClockContainer").classList.add("vmiddle"); - getById("overlayClockContainer").classList.add("hmiddle"); - } else if (urlParams.get("timer") === "3") { - getById("overlayClockContainer").classList.add("top"); - getById("overlayClockContainer").classList.add("right"); - } else if (urlParams.get("timer") === "9") { - getById("overlayClockContainer").classList.add("bottom"); - getById("overlayClockContainer").classList.add("right"); - } else if (urlParams.get("timer") === "6") { - getById("overlayClockContainer").classList.add("vmiddle"); - getById("overlayClockContainer").classList.add("right"); - } else { - getById("overlayClockContainer").classList.add("top"); - getById("overlayClockContainer").classList.add("hmiddle"); - } - } - - if (urlParams.has("miconlyoption") || urlParams.has("moo")) { - session.optionalMicOnly = true; - } - - if (urlParams.has("hidescreenshare") || urlParams.has("hidess") || urlParams.has("sshide") || urlParams.has("screensharehide")) { - // this way I don't need to remember what it's called. I can just guess. :D - session.screenShareElementHidden = true; - } - - if (urlParams.has("sspaused") || urlParams.has("sspause") || urlParams.has("ssp")) { - // this way I don't need to remember what it's called. I can just guess. :D - session.screenShareStartPaused = true; - } - - if (urlParams.has("zoomedbitrate") || urlParams.has("zb")) { - // this way I don't need to remember what it's called. I can just guess. :D - session.zoomedBitrate = urlParams.get("zoomedbitrate") || urlParams.get("zb") || 2500; - session.zoomedBitrate = parseInt(session.zoomedBitrate); - } - - if (urlParams.has("screenshareid") || urlParams.has("ssid")) { - if (urlParams.get("screenshareid") || urlParams.get("ssid")) { - session.screenshareid = urlParams.get("screenshareid") || urlParams.get("ssid"); - session.screenshareid = sanitizeStreamID(session.screenshareid); - } else { - session.screenshareid = session.streamID + "_ss"; - } - } - - if (urlParams.has("screensharevideoonly") || urlParams.has("ssvideoonly") || urlParams.has("ssvo")) { - session.screenshareVideoOnly = true; - getById("audioScreenShare1").classList.add("hidden"); - } - - if (urlParams.has("screensharefps") || urlParams.has("ssfps")) { - if (urlParams.get("screensharefps") || urlParams.get("ssfps")) { - session.screensharefps = urlParams.get("screensharefps") || urlParams.get("ssfps"); - session.screensharefps = parseInt(session.screensharefps) || 2; - } - } - - if (urlParams.has("dropdown")){ - getById("dropButton").className = ""; - } - - if (urlParams.has("screensharequality") || urlParams.has("ssq")) { - session.screensharequality = urlParams.get("screensharequality") || urlParams.get("ssq") || "0"; - - if (session.screensharequality.toLowerCase() == "4k") { - session.screensharequality = -2; - } else if (session.screensharequality.toLowerCase() == "2160p") { - session.screensharequality = -2; - } else if (session.screensharequality.toLowerCase() == "2160") { - session.screensharequality = -2; - } else if (session.screensharequality.toLowerCase() == "2k") { - session.screensharequality = -3; - } else if (session.screensharequality.toLowerCase() == "1440p") { - session.screensharequality = -3; - } else if (session.screensharequality.toLowerCase() == "1440") { - session.screensharequality = -3; - } else if (session.screensharequality.toLowerCase() == "hd") { - session.screensharequality = 1; - } else if (session.screensharequality.toLowerCase() == "720p") { - session.screensharequality = 1; - } else if (session.screensharequality.toLowerCase() == "720") { - session.screensharequality = 1; - } else if (session.screensharequality.toLowerCase() == "fullhd") { - session.screensharequality = 0; - } else if (session.screensharequality.toLowerCase() == "1080p") { - session.screensharequality = 0; - } else if (session.screensharequality.toLowerCase() == "1080") { - session.screensharequality = 0; - } else if (session.screensharequality.toLowerCase() == "high") { - session.screensharequality = 0; - } else if (session.screensharequality.toLowerCase() == "360p") { - session.screensharequality = 2; - } else if (session.screensharequality.toLowerCase() == "360") { - session.screensharequality = 2; - } else if (session.screensharequality.toLowerCase() == "low") { - session.screensharequality = 2; - } else { - session.screensharequality = parseInt(session.screensharequality) || 0; - } - try { - getById("gear_screen").parentNode.removeChild(getById("gear_screen")); - } catch (e) {} - } - - if (urlParams.has("screensharebitrate") || urlParams.has("ssbitrate")) { - session.screenShareBitrate = urlParams.get("screensharebitrate") || urlParams.get("ssbitrate"); - session.screenShareBitrate = parseInt(session.screenShareBitrate) || 2500; - } - - if (urlParams.has("compressed") || urlParams.has("compresssdp") || urlParams.has("compress")){ - session.compressSDP = true; // WIP. - } - - if (urlParams.has("screensharelabel") || urlParams.has("sslabel")) { - session.screenShareLabel = urlParams.get("screensharelabel") || urlParams.get("sslabel"); - try { - session.screenShareLabel = decodeURIComponent(session.screenShareLabel); - } catch (e) {} - session.screenShareLabel = session.screenShareLabel.replace(/_/g, " "); - } - - if (urlParams.has("whepshare") || urlParams.has("whepsrc")) { - try { - session.whepSrc = urlParams.get("whepshare") || urlParams.get("whepsrc") || null; - log("WHEP SRC: " + session.whepSrc); - if (session.whepSrc) { - try { - session.whepSrc = decodeURIComponent(session.whepSrc); - } catch (e) { - session.whepSrc = session.whepSrc; - } - } else { - session.whepSrc = await promptAlt("Enter the WHEP source as a URL"); - } - if (session.whepSrc) { - session.whipoutSettings = { type: "whep", url: session.whepSrc }; - session.whipoutSettingsUserSet = true; - } - } catch (e) { - errorlog(e); - } - } - if (urlParams.has("whepsharetoken") || urlParams.has("whepsrctoken")) { - if (session.whipoutSettings) { - try { - session.whepSrcToken = urlParams.get("whepsharetoken") || urlParams.get("whepsrctoken") || null; - log("WHEP TOKEN: " + session.whepSrcToken); - if (session.whepSrcToken) { - try { - session.whepSrcToken = decodeURIComponent(session.whepSrcToken); - } catch (e) { - session.whepSrcToken = session.whepSrcToken; - } - } else { - session.whepSrcToken = await promptAlt("Enter the WHEP source token"); - } - if (session.whepSrcToken) { - session.whipoutSettings.token = session.whepSrcToken; - session.whipoutSettingsUserSet = true; - } - } catch (e) { - errorlog(e); - } - } - } - - if (session.roomid !== false) { - if (!session.cleanOutput) { - if (session.roomid === "test") { - if (session.password === session.defaultPassword) { - window.focus(); - var testRoomResponse = confirm(getTranslation("room-test-not-good")); - if (testRoomResponse == false) { - hangup(); - throw new Error("User requested to not enter room 'room'."); - } - } - } - } - - if (session.audioDevice === false && session.outputDevice === false) { - getById("headphonesDiv2").classList.remove("hidden"); - getById("headphonesDiv").classList.remove("hidden"); - } - getById("addPasswordBasic").style.display = "none"; - - getById("info").innerHTML = ""; - getById("info").style.color = "#CCC"; - getById("videoname1").value = session.roomid; - getById("dirroomid").innerText = session.roomid; - getById("roomid").innerText = session.roomid; - getById("container-1").className = "column columnfade hidden"; - getById("container-4").className = "column columnfade hidden"; - // container 5 is share media file; 6 is share website - getById("container-7").style.display = "none"; - getById("container-8").style.display = "none"; - getById("container-9").style.display = "none"; - getById("container-10").style.display = "none"; - getById("container-11").style.display = "none"; - getById("container-12").style.display = "none"; - getById("container-13").style.display = "none"; - getById("container-14").style.display = "none"; - getById("container-15").style.display = "none"; - getById("container-16").style.display = "none"; - getById("container-17").style.display = "none"; - getById("container-18").style.display = "none"; - getById("container-19").style.display = "none"; - getById("container-20").style.display = "none"; - getById("container-21").style.display = "none"; - getById("mainmenu").style.alignSelf = "center"; - getById("mainmenu").classList.add("mainmenuclass"); - getById("header").style.alignSelf = "center"; - - if (session.webcamonly == true) { - // mobile or manual flag 'webcam' pflag set - getById("head1").innerHTML = ""; - } else { - getById("head1").innerHTML = 'Please select an option to join.'; - } - - if (session.roomid.length > 0) { - if (session.videoDevice === 0) { - if (session.audioDevice === 0) { - miniTranslate(getById("add_camera"), "join-room", "Join Room"); - } else { - miniTranslate(getById("add_camera"), "join-room-with-mic", "Join Room with Microphone"); - } - } else if (session.audioDevice === 0) { - miniTranslate(getById("add_camera"), "join-room-with-camera", "Join Room with Camera"); - } else if (session.optionalMicOnly) { - miniTranslate(getById("add_camera"), "join-room-with-video", "Join Room with Video"); - miniTranslate(getById("add_microphone"), "join-room-with-mic-only", "Join Room with just Microphone"); - getById("container-3a").classList.remove("hidden"); - } else { - miniTranslate(getById("add_camera"), "join-room-with-camera", "Join Room with Camera"); - } - miniTranslate(getById("add_screen"), "share-screen-with-room", "Screenshare with Room"); - } else { - if (session.videoDevice === 0) { - miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); - } else { - miniTranslate(getById("add_camera"), "share-your-camera", "Share your Camera"); - } - miniTranslate(getById("add_screen"), "share-your-screen", "Share your Screen"); - } - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - getById("container-2").title = getById("add_screen").innerText; - getById("container-3").title = getById("add_camera").innerText; - - if (session.scene !== false) { - getById("container-4").className = "column columnfade"; - getById("container-3").className = "column columnfade"; - getById("container-2").className = "column columnfade"; - getById("container-1").className = "column columnfade"; - getById("header").className = "hidden"; - getById("info").className = "hidden"; - getById("head1").className = "hidden"; - getById("head2").className = "hidden"; - getById("mainmenu").style.display = "none"; - getById("translateButton").style.display = "none"; - // getById("legal").style.display = "none"; - log("Update Mixer Event on REsize SET"); - window.onresize = updateMixer; - window.onorientationchange = function () { - setTimeout(updateMixer, 200); - }; - joinRoom(session.roomid); // this is a scene, so we want high resolutions - getById("main").style.overflow = "hidden"; - - if (session.chatbutton === true) { - getById("chatbutton").classList.remove("hidden"); - getById("controlButtons").classList.remove("hidden"); - } else if (session.chatbutton === false) { - getById("chatbutton").classList.add("hidden"); - } - - if (session.hangupbutton === true) { - getById("controlButtons").classList.remove("hidden"); - getById("hangupbutton").classList.remove("hidden"); - getById("hangupbutton").className = "float"; - } - - } else if (session.permaid === null && session.roomid == "") { - if (!session.cleanOutput) { - // getById("head3").classList.remove('hidden'); - // getById("head3a").classList.remove('hidden'); - } - } else if (session.studioSoftware && !session.webcamonly && session.permaid === false && session.director === false && ((session.view!==false) || session.whepInput || session.whipView) && session.roomid.length > 0) { - // we already know roomid !== false - updateURL("scene", true, false); // we also know it's not a scene, but we will assume it is in this specific case. - session.scene = 0; - } else if (session.studioSoftware && !session.webcamonly && !session.cleanOutput && (session.permaid === false) && (session.director === false) && (session.view===false) && !session.whepInput && !session.whipView && (session.roomid.length > 0)) { - try { - getById("unexpectedPushLink").classList.remove("hidden"); - } catch (e) {} - } - } else if (session.director) { - // if I do a short form of this, it will cause duplications in the code elsewhere. - if (directorLanding == false) { - // implies director is not true or false, but a string - try { - var director_room_input = sanitizeRoomName(session.director); - log("director_room_input:" + director_room_input); - - if (urlParams.has("codirector") || urlParams.has("directorpassword") || urlParams.has("dirpass") || urlParams.has("dp")) { - session.directorPassword = urlParams.get("codirector") || urlParams.get("directorpassword") || urlParams.get("dirpass") || urlParams.get("dp"); - if (!session.directorPassword) { - window.focus(); - session.directorPassword = await promptAlt(getTranslation("enter-director-password"), true); - } else { - try { - session.directorPassword = decodeURIComponent(session.directorPassword); - } catch (e) {} - } - if (session.directorPassword) { - session.directorPassword = sanitizePassword(session.directorPassword); - await generateHash(session.directorPassword + session.salt + "abc123", 12) - .then(function (hash) { - // million to one error. - log("dir room hash is " + hash); - session.directorHash = hash; - return; - }) - .catch(errorlog); - } else { - session.directorPassword = false; - } - } - - setTimeout( - function (director_room_input) { - createRoom(director_room_input); - }, - 20, - director_room_input - ); - } catch (e) { - directorLanding = true; - session.director = true; - } - } - if (session.chatbutton === true) { - getById("chatbutton").classList.remove("hidden"); - getById("controlButtons").classList.remove("hidden"); - } else if (session.chatbutton === false) { - getById("chatbutton").classList.add("hidden"); - } - } else if (((session.view!==false) || session.whepInput || session.whipView) && session.permaid === false) { - //if (!session.activeSpeaker){ - session.audioMeterGuest = false; - //} - if (session.style === false && session.studioSoftware) { - session.style = 1; - } - if (session.audioEffects === null) { - session.audioEffects = false; - } - log("Update Mixer Event on REsize SET"); - getById("translateButton").style.display = "none"; - // getById("legal").style.display = "none"; - window.onresize = updateMixer; - window.onorientationchange = function () { - setTimeout(function () { - updateMixer(); - }, 200); - }; - getById("main").style.overflow = "hidden"; - - if (session.chatbutton === true) { - getById("chatbutton").classList.remove("hidden"); - getById("controlButtons").classList.remove("hidden"); - } else if (session.chatbutton === false) { - getById("chatbutton").classList.add("hidden"); - } - - if (session.hangupbutton === true) { - getById("controlButtons").classList.remove("hidden"); - getById("hangupbutton").classList.remove("hidden"); - getById("hangupbutton").className = "float"; - } - } - - if (urlParams.has("nofileshare") || urlParams.has("nodownloads") || urlParams.has("nofiles")) { - session.hostedFiles = false; - session.nodownloads = true; - getById("sharefilebutton").style.display = "none"; - getById("sharefilebutton").classList.add("hidden"); - } else if (urlParams.has("fileshare") || urlParams.has("fs")) { - // - } else if (session.mobile) { - getById("sharefilebutton").style.display = "none"; - getById("sharefilebutton").classList.add("hidden"); - } else if (session.roomid == false) { - getById("sharefilebutton").style.display = "none"; - getById("sharefilebutton").classList.add("hidden"); - } else if (session.scene !== false) { - getById("sharefilebutton").style.display = "none"; - getById("sharefilebutton").classList.add("hidden"); - } else if (session.cleanOutput) { - getById("sharefilebutton").style.display = "none"; - getById("sharefilebutton").classList.add("hidden"); - } - - if (session.audioEffects === null) { - session.audioEffects = true; - } - - if (session.audioEffects) { - getById("channelGroup1").style.display = "block"; - getById("channelGroup2").style.display = "block"; - } - - if (urlParams.has("hidemenu") || urlParams.has("hm")) { - // needs to happen the room and permaid applications - getById("mainmenu").style.display = "none"; - getById("header").style.display = "none"; - getById("mainmenu").style.opacity = 0; - getById("header").style.opacity = 0; - } - - if (urlParams.has("hideusermenu") || urlParams.has("nousermenu") || urlParams.has("hum")) { - getById("advancedOptionsGeneral").classList.add("hidden"); - } - - if ((session.view!==false) || session.whepInput || session.whipView) { - getById("main").className = "main"; - getById("credits").style.display = "none"; - // getById("legal").style.display = "none"; - try { - if (session.label === false) { - if (document.title == "") { - document.title = "View=" + session.view.toString(); - } else { - document.title += ", View=" + session.view.toString(); - } - } - } catch (e) { - errorlog(e); - } - } - - if (urlParams.get("auth")) { - session.auth = urlParams.get("auth"); - } - - if (urlParams.has("waitimage")) { - session.waitImage = urlParams.get("waitimage") || false; - } - - if ((((session.view!==false) || session.whepInput || session.whipView) && session.roomid === false) || (session.waitImage && session.scene !== false)) { - getById("container-4").className = "column columnfade"; - getById("container-3").className = "column columnfade"; - getById("container-2").className = "column columnfade"; - getById("container-1").className = "column columnfade"; - //getById("header").className = 'hidden'; - getById("info").className = "hidden"; - getById("header").className = "hidden"; - getById("head1").className = "hidden"; - getById("head2").className = "hidden"; - getById("head3").classList.add("hidden"); - getById("head3a").classList.add("hidden"); - - getById("mainmenu").style.backgroundRepeat = "no-repeat"; - getById("mainmenu").style.backgroundPosition = "bottom center"; - getById("mainmenu").style.minHeight = "100%"; - getById("mainmenu").style.minWidth = "100%"; - getById("mainmenu").style.backgroundSize = "100px 100px"; - getById("mainmenu").innerHTML = ""; - getById("mainmenu").classList.remove("row"); - getById("mainmenu").style.display = "block"; - - if (urlParams.has("waittimeout")) { - session.waitImageTimeout = parseInt(urlParams.get("waittimeout")) || 0; - } - if (!session.fakeFeeds) { - session.waitImageTimeoutObject = setTimeout(function () { - session.waitImageTimeoutObject = true; - try { - if ((session.view!==false) || session.whepInput || session.whipView) { - if (document.getElementById("mainmenu")) { - if (session.waitImage) { - getById("mainmenu").innerHTML += ''; - getById("retryimage").src = decodeURIComponent(session.waitImage); - getById("retryimage").onerror = function () { - this.style.display = "none"; - }; - - if (session.cover) { - getById("retryimage").style.objectFit = "cover"; - } - } else if (!session.cleanOutput) { - getById("mainmenu").innerHTML += '
    '; - getById("retrySpinner").onclick = function () { - updateURL("cleanoutput"); - location.reload(); - }; - getById("retrySpinner").title = getTranslation("waiting-for-the-stream"); - } - if (urlParams.has("waitmessage")) { - getById("mainmenu").innerHTML += '
    '; - getById("retrymessage").innerText = urlParams.get("waitmessage"); - getById("retrySpinner").title = urlParams.get("waitmessage"); - } - } - } - } catch (e) { - errorlog(e); - } - }, session.waitImageTimeout); - } - - log("auto request videos"); - if ((iPad || iOS) && navigator.userAgent.indexOf("Safari") != -1 && navigator.userAgent.indexOf("Chrome") == -1 && SafariVersion > 13) { - // Modern iOS doesn't need pop up - play(); - } else if (navigator.userAgent.indexOf("Safari") != -1 && (navigator.userAgent.indexOf("Chrome") == -1 && navigator.userAgent.indexOf("Chromium") == -1)) { - // Safari on Desktop does require pop up - try { - navigator.mediaDevices - .getUserMedia({ - audio: true - }) - .then(function () { - closeModal(); - play(); - }) - .catch(function () { - play(); - }); - if (!session.cleanOutput) { - warnUser("Safari requires us to ask for an audio permission to use peer-to-peer technology. You will need to accept it in a moment if asked to view this live video", 20000); - } - } catch(e){ - errorlog(e); - play(); - } - } else { - // everything else is OK. - play(); - } - } else if (session.roomid) { - try { - if (session.label === false) { - if (document.title == "") { - document.title = "Room=" + session.roomid.toString(); - } else { - document.title += ": " + session.roomid.toString(); - } - } - } catch (e) { - errorlog(e); - } - } else { - try { - let reloadOldRoom = getStorage("directorOtherSettings"); - if (reloadOldRoom && reloadOldRoom.roomid) { - getById("lastSavedRoomName").innerText = reloadOldRoom.roomid; - getById("lastSavedRoom").classList.remove("hidden"); - getById("goToLastSavedRoom").onclick = function () { - createRoom(false, true); - }; - } - } catch (e) { - errorlog(e); - } - } - - hideHomeCheck(); - - setTimeout(function () { - for (var i in delayedStartupFuncs) { - var cb = delayedStartupFuncs[i]; - log(cb.slice(1)); - cb[0](...cb.slice(1)); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#A_better_apply - } - delayedStartupFuncs = []; - }, 50); - - if (session.effect == "3" || session.effect == "4" || session.effect == "5") { - attemptTFLiteJsFileLoad(); - } else if (session.effect == "6") { - loadTensorflowJS(); - } else if (session.effect == "9") { - session.effect = "sample"; - //loadEffect(session.effect); - warnUser("Loading custom effects model...", 1000); - } else if (session.effect == "10") { - session.effect = "dog"; - //loadEffect(session.effect); - warnUser("Loading custom effects model...", 1000); - } else if (session.effect == "11") { - session.effect = "anon"; - //loadEffect(session.effect); - warnUser("Loading custom effects model...", 1000); - } else if (session.effect == "12") { - session.effect = "sample"; - //loadEffect(session.effect); - warnUser("Loading custom effects model...", 1000); - } else if (session.effect == "facetracking") { - session.effect = "1"; - } else if (session.effect == "zoom") { - session.effect = "7"; - } - - if (session.effect === "3") { - getById("selectEffectAmount").style.display = "block"; - getById("selectEffectAmount3").style.display = "block"; - getById("selectEffectAmountInput").value = session.effectValue; - getById("selectEffectAmountInput3").value = session.effectValue; - } else if ((session.effect === "14" || session.effect === "15") && session.effectValue_default == false){ - getById("selectEffectAmount").style.display = "block"; - getById("selectEffectAmount3").style.display = "block"; - - if (session.effectValue_default) { - session.effectValue = parseFloat(session.effectValue_default); - } else { - session.effectValue = parseFloat(session.effectValue); - } - - - getById("selectEffectAmountInput").min = 1; - getById("selectEffectAmountInput").max = 50; - getById("selectEffectAmountInput").step = 1; - getById("selectEffectAmountInput3").min = 1; - getById("selectEffectAmountInput3").max = 50; - getById("selectEffectAmountInput3").step = 1; - - getById("selectEffectAmountInput").value = session.effectValue; - getById("selectEffectAmountInput3").value = session.effectValue; - - - } else if (session.effect === "7") { - getById("selectEffectAmount").style.display = "block"; - getById("selectEffectAmount3").style.display = "block"; - if (session.effectValue_default) { - session.effectValue = session.effectValue_default; - } else { - session.effectValue = 1; - } - getById("selectEffectAmountInput").min = 1; - getById("selectEffectAmountInput").max = 3.99; - getById("selectEffectAmountInput").step = 0.01; - getById("selectEffectAmountInput3").min = 1; - getById("selectEffectAmountInput3").max = 3.99; - getById("selectEffectAmountInput3").step = 0.01; - - getById("selectEffectAmountInput").value = session.effectValue; - getById("selectEffectAmountInput3").value = session.effectValue; - - // Show zoom position controls - getById("zoomPositionControls").style.display = "block"; - getById("zoomPositionControls3").style.display = "block"; - } - - if (session.sensorData) { - setupSensorData(parseInt(session.sensorData)); - } - if (session.externalSensorBridge) { - setupExternalSensorBridge(); - } - - if (location.protocol !== "https:") { - try { - //if (!session.cleanOutput) { - if (["127.0.0.1", "localhost"].includes(window.location.hostname)){ - // these are allowed I believe. I do change the salt however to the default one though. - } else if (window.location.host.split(".")[0] !== "insecure") { - console.warn("⚠️ SSL (https) is not enabled. This site will not fully work without it!

    Try accessing the site from here instead.", false, false); - } - //} - } catch (e) {} - } else { - try { - if (navigator && navigator.mediaDevices && navigator.mediaDevices.ondevicechange) { - navigator.mediaDevices.ondevicechange = reconnectDevices; - } - } catch (e) { - errorlog(e); - } - } - - if (session.autohide && session.scene === false) { - // && (session.roomid!==false)){ - try { - getById("main").onmouseover = showControl; // this is correct. (it's not session.showControls) - document.ontouchstart = showControl; // this is correct. (it's not session.showControls) - getById("gridlayout").classList.add("nocontrolbar"); - if (session.autostart) { - showControl(); - } - } catch (e) {} - } - - if (urlParams.has("experimental")) { - session.experimental = true; - } - - if (urlParams.has("flagship")) { - session.flagship = true; - } - //if (!session.flagship && session.mobile && (session.limitTotalBitrate===false)){ - // session.limitTotalBitrate = session.totalRoomBitrate_default; // 500, with the max per guest stream out at maxMobileBitrate (350kbps) or 35-kbps if more than X in the room. - //} - - if (urlParams.has("maxmobilebitrate")) { - session.maxMobileBitrate = parseInt(urlParams.has("maxmobilebitrate")) || 0; - } - if (urlParams.has("lowmobilebitrate")) { - session.lowMobileBitrate = parseInt(urlParams.has("lowmobilebitrate")) || 0; - } - - // Please contact steve on discord.vdo.ninja if you'd like this iFRAME tweaked, expanded, etc -- it's updated based on user request - - session.remoteInterfaceAPI = function (e) { - // iFRAME api support - if (!e.data || typeof e.data !== "object") { - warnlog(e); - return; - } - // Ignore framegrab-audio-settings - handled by lib.js message listener - if (e.data.action === "framegrab-audio-settings") { - return; - } - log(e); - try { - if ("function" in e.data) { - // these are calling in-app functions, with perhaps a callback -- TODO: add callbacks - var ret = null; - if (e.data.function === "previewWebcam") { - ret = previewWebcam(); - } else if (e.data.function === "changeHTML") { - ret = getById(e.data.target); - ret.innerHTML = e.data.value; - } else if (e.data.function === "publishScreen") { - ret = publishScreen(); - } else if (e.data.function === "targetGuest") { - ret = targetGuest(data.target, data.action, data.value); - } else if (e.data.function === "commands" && data.action && Commands[data.action]) { - ret = Commands[data.action](data.value, data.value2 || null); - } else if (e.data.function === "routeMessage") { - try { - session.ws.onmessage({ data: e.data.value }); - } catch (e) { - warnlog("handshake not yet setup"); - } - } else if (e.data.function === "eval") { - eval(e.data.value); // eval == evil ; feedback welcomed ; - } - } - } catch (err) { - errorlog(err); - } - - if ("sendData" in e.data) { - // send generic data via p2p. Send whatever you want I guess; there is a max chunk size of course. Use filetransfer for large files? - var UUID = false; - var streamID = false; - var type = false; - if (e.data.UUID) { - UUID = e.data.UUID; - } else if (e.data.streamID) { - streamID = e.data.streamID; - } - if (e.data.type) { - type = e.data.type; - } - var ret = session.sendGenericData(e.data.sendData, UUID, streamID, type); // comes out the other side as: ("dataReceived", data, UUID); - if (!ret) { - warnlog("Not connected yet or no peers available"); - } - return; - } - - if ("PPT" in e.data) { - log("PTT activated-webmain"); - if (e.data.PPT === true) { - // unmute - session.muted = false; // set - getById("mutebutton").classList.add("PPTActive"); - log(session.muted); - toggleMute(true); // apply - } else if (e.data.PPT === false) { - // mute - session.muted = true; // set - getById("mutebutton").classList.remove("PPTActive"); - log(session.muted); - toggleMute(true); // apply - } else if (e.data.PPT === "toggle") { - // toggle - toggleMute(); - } - return; // this is a high-load call, so lets skip the rest of the checks to save cpu. - } - - if ("sendChat" in e.data) { - sendChat(e.data.sendChat); // sends to all peers; more options down the road - return; - } - // Chat out gets called via getChatMessage function - // Related code: parent.postMessage({"chat": {"msg":-----,"type":----,"time":---} }, "*"); - - // session.requestResolution(vid.dataset.UUID, wrw*window.devicePixelRatio, hrh*window.devicePixelRatio); - - if ("mic" in e.data) { - // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. - if (e.data.mic === true) { - // unmute - session.muted = false; // set - log(session.muted); - toggleMute(true); // apply - } else if (e.data.mic === false) { - // mute - session.muted = true; // set - log(session.muted); - toggleMute(true); // apply - } else if (e.data.mic === "toggle") { - // toggle - toggleMute(); - } - } - - if ("toggleSettings" in e.data) { - // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. - - if (e.data.toggleSettings && !toggleSettingsState) { - toggleSettings(); - } else if (e.data.toggleSettings == "toggle") { - toggleSettings(); - } else if (toggleSettingsState) { - toggleSettings(); - } - } - - if ("camera" in e.data) { - // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. - if (e.data.camera === true) { - // unmute - session.videoMuted = false; // set - log(session.videoMuted); - toggleVideoMute(true); // apply - } else if (e.data.camera === false) { - // mute - session.videoMuted = true; // set - log(session.videoMuted); - toggleVideoMute(true); // apply - } else if (e.data.camera === "toggle") { - // toggle - toggleVideoMute(); - } - } - - if ("pauseinvisible" in e.data) { // whether videos hidden in the update mixer are muted or not, based on if they are visible or not. - if (e.data.pauseinvisible === "toggle") { - // toggle - session.pauseInvisible = !session.pauseInvisible; - } else if (!e.data.pauseinvisible) { - session.pauseInvisible = false; - } else { - session.pauseInvisible = true; - } - updateMixer(); - } - - if ("keyframe" in e.data) { - session.sendKeyFrameScenes(); - } - - if ("groups" in e.data) { - if (typeof e.data.groups == "object") { - session.group = e.data.groups || []; - } else if (!e.data.group) { - session.group = []; - } else { - session.group = e.data.groups.split(","); - } - var eleGroup = getById("groups"); - eleGroup.querySelectorAll('[data-action-type="toggle-group"][data-group]').forEach(group => { - if (!(session.group && session.group.includes(group))) { - group.remove("green"); - } - }); - - if (session.group) { - session.group.forEach(group => { - var ele = eleGroup.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"'); - if (!ele) { - ele = document.createElement("div"); - ele.dataset.actionType = "toggle-group"; - ele.dataset.group = group; - ele.classList.add("float"); - ele.style.display = "inline-block"; - ele.role = "button"; - ele.innerHTML = '
    ' + group; - eleGroup.appendChild(ele); - ele.onclick = function () { - changeGroupDirectorAPI(this.dataset.group); - }; - } - ele.classList.add("green"); - }); - } - - updateMixer(); - - if (session.group.length || session.allowNoGroup) { - session.sendMessage({ group: session.group.join(",") }); - if (session.screenShareState && session.screenshareType === 3) { - session.sendMessage({ group: session.group.join(","), altUUID: true }); - } - } else { - session.sendMessage({ group: false }); - if (session.screenShareState && session.screenshareType === 3) { - session.sendMessage({ group: false, altUUID: true }); - } - } - } - - if (e.data.getSnapshotBySlot || e.data.getSnapshotByStreamID) { - let videoElement = false; - - let streamID = ("getSnapshotBySlot" in e.data) ? session.currentSlots[parseInt(e.data.getSnapshotBySlot) || 0] : e.data.getSnapshotByStreamID; - - let UUID = false; - if (streamID){ - for (let i in session.rpcs) { - if (session.rpcs[i].streamID == streamID) { - UUID = i; - videoElement = session.rpcs[i].videoElement; - break; - } - } - } - - if (streamID && videoElement && videoElement.srcObject) { - const videoTrack = videoElement.srcObject.getVideoTracks()[0]; - - if (videoTrack) { - const processor = new MediaStreamTrackProcessor({ track: videoTrack }); - const reader = processor.readable.getReader(); - - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d", { willReadFrequently: true }); - - try { - reader.read().then(({ done, value: frame }) => { - if (!done && frame) { - canvas.width = frame.displayWidth; - canvas.height = frame.displayHeight; - ctx.drawImage(frame, 0, 0); - - const format = typeof session.sendframes === "string" ? session.sendframes : "png"; - const imageData = canvas.toDataURL(`image/${format}`, 0.8); - - parent.postMessage({ - type: 'frame', - frame: imageData, - UUID: UUID, - streamID: streamID, - trackID: videoTrack.id, - kind: videoTrack.kind, - format: format, - slot: parseInt(Object.keys(session.currentSlots).find(key => session.currentSlots[key] === streamID)) || 0, - cib: e.data.cib || null - }, session.iframetarget); - - // Proper cleanup - frame.close(); - reader.cancel(); - - // Remove canvas from DOM if it was added - if (canvas.parentNode) { - canvas.parentNode.removeChild(canvas); - } - } - }).catch(error => { - console.error("Error processing image frame:", error); - }); - } catch (error) { - console.error("Error setting up frame capture:", error); - } - } - } - } - - if ("groupView" in e.data) { - if (typeof e.data.groupView == "object") { - session.groupView = e.data.groupView || []; - } else if (!e.data.groupView) { - session.groupView = []; - } else { - session.groupView = e.data.groupView.split(","); - } - updateMixer(); - } - - if ("mute" in e.data) { - if (e.data.mute === true) { - // unmute - session.speakerMuted = true; // set - toggleSpeakerMute(true); // apply - } else if (e.data.mute === false) { - // mute - session.speakerMuted = false; // set - toggleSpeakerMute(true); // apply - } else if (e.data.mute === "toggle") { - // toggle - toggleSpeakerMute(); - } - } else if ("speaker" in e.data) { - // same thing as mute. - if (e.data.speaker === true) { - // unmute - session.speakerMuted = false; // set - toggleSpeakerMute(true); // apply - } else if (e.data.speaker === false) { - // mute - session.speakerMuted = true; // set - toggleSpeakerMute(true); // apply - } else if (e.data.speaker === "toggle") { - // toggle - toggleSpeakerMute(); - } - } - - if ("record" in e.data) { - if (e.data.record == false) { - // mute - if ("recording" in session.videoElement) { - recordLocalVideo("stop"); - } - } else if (e.data.record === true) { - if ("recording" in session.videoElement) { - // already recording - } else { - recordLocalVideo("start"); - } - } else if (e.data.record) { - var video = document.getElementById(e.data.record); - if (video) { - var videoKbps = 4000; - if (session.recordLocal !== false) { - videoKbps = session.recordLocal; - } - recordLocalVideo(null, videoKbps, video); - } - } - } - - if ("volume" in e.data) { - // might not work with iframes or meshcast currently. - var requestedVolume = parseFloat(e.data.volume) || 0; - if (requestedVolume > 1.0) { - // this is a bit quasi improper. But the API is official 0 to 1.0; not 0 to 100, so this is mainly a catch for those not using the API right. - requestedVolume = requestedVolume / 100.0; - } - setSessionPlaybackVolume(requestedVolume, e.data.target); - } - - if ("enableYouTube" in e.data) { - // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. - if (typeof e.data.enableYouTube == "string") { - session.youtubeKey = e.data.enableYouTube; - } else if (!session.youtubeKey) { - errorlog("No Youtube Key provided"); - } - console.log(session.youtubeKey); - YoutubeChatInterface(true); - } - - if ("nextSlide" in e.data) { - // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. - nextSlide(); - } - - if ("prevSlide" in e.data) { - // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. - gobackSlide(); - } - - if ("panning" in e.data) { - // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. - if ("UUID" in e.data) { - try { - adjustPan(UUID, e.data.panning); - } catch (e) { - errorlog(e); - } - } else { - for (var i in session.rpcs) { - try { - adjustPan(i, e.data.panning); - } catch (e) { - errorlog(e); - } - } - } - } - - if ("targetBitrate" in e.data || "targetAudioBitrate" in e.data) { - // this sets the fundemental bitrate target, but does not necessarily "lock" . - - var msg = {}; - if ("targetBitrate" in e.data) { - msg.targetBitrate = e.data.targetBitrate; - } - if ("targetAudioBitrate" in e.data) { - msg.targetAudioBitrate = e.data.targetAudioBitrate; - } - if (e.data.requestAs) { - msg.requestAs = e.data.requestAs; - } - if (e.data.remote) { - msg.remote = e.data.remote; - } - for (var i in session.rpcs) { - try { - if ("streamID" in session.rpcs[i]) { - if ("target" in e.data) { - if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { - // specify a stream ID or let it apply to all videos - session.sendRequest(msg, i); - } - } else if (e.data.UUID && e.data.UUID === i) { - session.sendRequest(msg, i); - } else if (e.data.streamID) { - if (session.rpcs[i].streamID == e.data.streamID) { - // specify a stream ID or let it apply to all videos - session.sendRequest(msg, i); - } - } else { - session.sendRequest(msg, i); // bitrate = 0 pauses the video - } - } - } catch (e) { - errorlog(e); - } - } - } - - if ("manualBitrate" in e.data) { - for (var i in session.rpcs) { - try { - if ("streamID" in session.rpcs[i]) { - if ("target" in e.data) { - if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { - // specify a stream ID or let it apply to all videos - session.rpcs[i].manualBandwidth = e.data.manualBitrate; - session.requestRateLimit(false, i); - } - } else if (e.data.UUID && e.data.UUID === i) { - session.rpcs[i].manualBandwidth = e.data.manualBitrate; - session.requestRateLimit(false, i); - } else if (e.data.streamID) { - if (session.rpcs[i].streamID == e.data.streamID) { - // specify a stream ID or let it apply to all videos - session.rpcs[i].manualBandwidth = e.data.manualBitrate; - session.requestRateLimit(false, i); - } - } else { - session.rpcs[i].manualBandwidth = e.data.manualBitrate; - session.requestRateLimit(false, i); - } - } - } catch (e) { - errorlog(e); - } - } - } - - if ("bitrate" in e.data) { - /// set a video bitrate for a video; scene or view link; kbps - var lock = true; - if ("lock" in e.data) { - // since this is the iframe API, we're going to assume the default is manual over-ride. VDO.Ninja's automixer logic won't override a locked bitrate. - lock = e.data.lock; - } - for (var i in session.rpcs) { - try { - if ("streamID" in session.rpcs[i]) { - // we only target publishers with this call - if ("target" in e.data) { - if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { - // specify a stream ID or let it apply to all videos - session.requestRateLimit(e.data.bitrate, i, false, lock); - } - } else if (e.data.UUID && e.data.UUID === i) { - session.requestRateLimit(e.data.bitrate, i, false, lock); - } else if (e.data.streamID) { - if (session.rpcs[i].streamID == e.data.streamID) { - // specify a stream ID or let it apply to all videos - session.requestRateLimit(e.data.bitrate, i, false, lock); - } - } else { - session.requestRateLimit(e.data.bitrate, i, false, lock); // bitrate = 0 pauses the video - } - } - } catch (e) { - errorlog(e); - } - } - } - - if ("audiobitrate" in e.data) { - // changes the audio bitrate of a specific or all inbound media tracks. kbps - var lock = true; - if ("lock" in e.data) { - // since this is the iframe API, we're going to assume the default is manual over-ride. VDO.Ninja's automixer logic won't override a locked bitrate. - lock = e.data.lock; - } - for (var i in session.rpcs) { - try { - if ("streamID" in session.rpcs[i]) { - // we only target publishers with this call - if ("target" in e.data) { - if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { - // specify a stream ID or let it apply to all videos - session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); - } - } else if (e.data.UUID && e.data.UUID === i) { - session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); - } else if (e.data.streamID) { - if (session.rpcs[i].streamID == e.data.streamID) { - // specify a stream ID or let it apply to all videos - session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); - } - } else { - session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); // bitrate = 0 pauses the video - } - } - } catch (e) { - errorlog(e); - } - } - } - - if ("changeVideoDevice" in e.data) { - warnlog(e.data.changeVideoDevice); - changeVideoDevice(e.data.changeVideoDevice); - } - - if ("changeAudioDevice" in e.data) { - warnlog(e.data.changeAudioDevice); - changeAudioDevice(e.data.changeAudioDevice); - } - - if ("changeAudioOutputDevice" in e.data) { - warnlog(e.data.changeAudioOutputDevice); - changeAudioOutputDeviceById(e.data.changeAudioOutputDevice); - } - - if ("getDeviceList" in e.data) { - // get a list of local camera / audio devices - warnlog(e.data.getDeviceList); - enumerateDevices().then(function (deviceInfos) { - parent.postMessage( - { - deviceList: JSON.parse(JSON.stringify(deviceInfos)), - cib: e.data.cib || null - }, - session.iframetarget - ); - }); - } - - if ("sceneState" in e.data) { - // TRUE OR FALSE - tells the connected peers if they are live or not via a tally light change. - var msg = {}; - msg.obsState = {}; - msg.obsState.visibility = e.data.sceneState || false; - msg.obsState.recording = e.data.sceneState || false; - session.sendRequest(msg); - } - - if ("layouts" in e.data) { - session.layouts = e.data.layouts; - if ("obsSceneTriggers" in e.data) { - session.obsSceneTriggers = e.data.obsSceneTriggers; - } else { - session.obsSceneTriggers = false; - } - for (var uid in session.pcs) { - if (session.pcs[uid].layout) { - session.sendMessage(e.data, uid); - } - } - // session.obsSceneSync(); // not sure I need to trigger this? - log(e.data); - } - - if ("sendMessage" in e.data) { - // webrtc send to viewers - session.sendMessage(e.data.sendMessage); - } - - if ("sendRequest" in e.data) { - // webrtc send to publishers - session.sendRequest(e.data.sendRequest); - } - - if ("sendRawMIDI" in e.data) { - // webrtc send to publishers - //var msg = {}; - //msg.midi = {}; - //msg.midi.d = e.data.sendRawMIDI.data; aka [d1,d2,d3]; - //msg.midi.c = e.data.sendRawMIDI.channel; - //msg.midi.s = e.data.sendRawMIDI.timestamp; - // e.data.UUID or e.data.streamID or leave empty to send to all - if ("UUID" in e.data) { - sendRawMIDI(e.data.sendRawMIDI, e.data.UUID); // send to connection - } else if (e.data.streamID) { - sendRawMIDI(e.data.sendRawMIDI, false, e.data.streamID); // send to connection - } else { - sendRawMIDI(e.data.sendRawMIDI); // send to all - } - return; // make it send faster. - } - - if ("sendPeers" in e.data) { - // webrtc send message to every connected peer; like send and request; a hammer vs a knife. - session.sendPeers(e.data.sendPeers); - } - - if ("reload" in e.data) { - // reload the page - reloadRequested(); // location.reload();, but with no user prompt (force reload) - } - - if ("getFaces" in e.data) { - if (e.data.faceTrack) { - session.grabFaceData = true; - getFaces(); - } else { - session.grabFaceData = false; - } - } - - if ("getFreshStats" in e.data) { - // takes a second to query. - var stats = {}; - try { - stats.inbound = {}; - stats.total_outbound_connections = Object.keys(session.pcs).length; - stats.total_inbound_connections = Object.keys(session.rpcs).length; - for (var i in session.rpcs) { - stats.inbound[session.rpcs[i].streamID] = session.rpcs[i].stats; - } - for (var uuid in session.pcs) { - setTimeout( - function (UUID) { - session.pcs[UUID].getStats().then(function (stats) { - stats.forEach(stat => { - if (stat.id && stat.id.startsWith("DEPRECATED_")) { - return; - } - - if (stat.type == "outbound-rtp") { - if (stat.kind == "video") { - if ("qualityLimitationReason" in stat) { - session.pcs[UUID].stats.quality_limitation_reason = stat.qualityLimitationReason; - } - if ("framesPerSecond" in stat) { - session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond; - } - if ("encoderImplementation" in stat) { - session.pcs[UUID].stats.encoder = stat.encoderImplementation; - } - } - } else if (stat.type == "remote-candidate") { - if ("relayProtocol" in stat) { - if ("ip" in stat) { - session.pcs[UUID].stats.remote_relay_IP = stat.ip; - } - session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol; - } - if ("candidateType" in stat) { - session.pcs[UUID].stats.candidateType_remote = stat.candidateType; - } - } else if (stat.type == "local-candidate") { - if ("relayProtocol" in stat) { - if ("ip" in stat) { - session.pcs[UUID].stats.local_relayIP = stat.ip; - } - session.pcs[UUID].stats.local_relayProtocol = stat.relayProtocol; - } - if ("candidateType" in stat) { - session.pcs[UUID].stats.candidateType_local = stat.candidateType; - } - } else if (stat.type == "candidate-pair" && stat.nominated) { - if ("availableOutgoingBitrate" in stat) { - session.pcs[UUID].stats.available_outgoing_bitrate_kbps = parseInt(stat.availableOutgoingBitrate / 1024); - } - if ("totalRoundTripTime" in stat) { - if ("responsesReceived" in stat) { - session.pcs[UUID].stats.average_roundTripTime_ms = parseInt((stat.totalRoundTripTime / stat.responsesReceived) * 1000); - } - } - } - return; - }); - return; - }); - }, - 0, - uuid - ); - } - } catch (e) { - // disconnected probably - } - setTimeout(function () { - stats.outbound_stats = {}; - try { - for (var i in session.pcs) { - stats.outbound_stats[i] = session.pcs[i].stats; - } - } catch (e) {} - parent.postMessage( - { - stats: stats, - cib: e.data.cib || null - }, - session.iframetarget - ); - }, 1000); - } - - if ("getStats" in e.data) { - if (e.data.streamID) { - parent.postMessage( - { - stats: getQuickStats(e.data.streamID), - cib: e.data.cib || null - }, - session.iframetarget - ); - } else { - parent.postMessage( - { - stats: getQuickStats(), - cib: e.data.cib || null - }, - session.iframetarget - ); - } - } - - if ("getRemoteStats" in e.data) { - if (session.remote) { - session.sendRequest({ requestStats: true, remote: session.remote }); - } else { - session.sendRequest({ requestStats: true }); - } - } - - if ("requestStatsContinuous" in e.data) { - if (session.remote) { - session.sendRequest({ requestStatsContinuous: e.data.requestStatsContinuous, remote: session.remote }); - } else { - session.sendRequest({ requestStatsContinuous: e.data.requestStatsContinuous }); - } - } - - if ("getLoudness" in e.data) { - log("GOT LOUDNESS REQUEST"); - if (e.data.getLoudness == true) { - if (!session.pushLoudness && session.audioEffects !== true) { - session.pushLoudness = true; - for (var i in session.rpcs) { - updateIncomingAudioElement(i); // this can be called when turning on/off inbound audio processing. - } - } else { - session.pushLoudness = true; - } - - var loudness = {}; - for (var i in session.rpcs) { - loudness[session.rpcs[i].streamID] = session.rpcs[i].stats.Audio_Loudness; - } - - parent.postMessage( - { - loudness: loudness, - cib: e.data.cib || null - }, - session.iframetarget - ); - } else { - if (session.pushLoudness && !session.audioEffects) { - // turn off audio processing - session.pushLoudness = false; - for (var i in session.rpcs) { - updateIncomingAudioElement(i); - } - } else { - session.pushLoudness = false; // can't turn off audio processing - } - } - } - - if ("getEffectsData" in e.data) { - log("GOT getEffects Data REQUESTed"); // face tracking info, etc. - if (e.data.getEffectsData !== false) { - session.pushEffectsData = e.data.getEffectsData; // which effect do you want the data from? it won't enable the effect necessarily; just the ML pipeline - - //parent.postMessage({ - // "effectsData": effectsData, - // "effectsID": session.pushEffectsData - //}, session.iframetarget); - } else { - session.pushEffectsData = false; - } - } - - if ("getStreamIDs" in e.data) { - // get a list of stream Ids, with a label if it is present. label = false if not there - if (e.data.getStreamIDs) { - var streamIDs = {}; - for (var i in session.rpcs) { - streamIDs[session.rpcs[i].streamID] = session.rpcs[i].label; - } - parent.postMessage( - { - streamIDs: streamIDs, - cib: e.data.cib || null - }, - session.iframetarget - ); - } - } - - if ("getStreamInfo" in e.data) { - // get a list of stream Ids, with a label if it is present. label = false if not there - try { - var UUIDS = {}; - for (var i in session.rpcs) { - UUIDS[i] = {}; - UUIDS[i].label = session.rpcs[i].label || false; - UUIDS[i].streamID = session.rpcs[i].streamID || false; - if (session.rpcs[i].stats && session.rpcs[i].stats.info) { - UUIDS[i].info = session.rpcs[i].stats.info; - } else { - UUIDS[i].info = {}; - } - } - parent.postMessage( - { - streamInfo: UUIDS, - cib: e.data.cib || null - }, - session.iframetarget - ); - } catch (e) { - errorlog(e); - } - } - - if ("close" in e.data || "hangup" in e.data) { - // disconnect and hangup all inbound streams. - var tmp = e.data.close || e.data.hangup; - if (tmp == "estop") { - // try to stop the video recording even if not complete; if you can't wait even ms before a reload/exit. - console.log("ESTOP"); - session.hangup(false, true); - } else if (tmp == "reload") { - // stop and reload the page safely. - session.hangup(true); - } else { - // just hangup, but can take up to 1-second to do so fully. - session.hangup(); - } - } - //if ("hangup" in e.data) { - // disconnect and hangup all inbound streams. - // session.hangup(); - //} - - if ("style" in e.data) { - // insert a custom style sheet - try { - const style = document.createElement("style"); - style.textContent = e.data.style; - document.head.append(style); - log(style); - } catch (e) { - errorlog(e); - } - } - - if ("getDetailedState" in e.data) { - var detailedState = getDetailedState(); - parent.postMessage( - { - detailedState: detailedState, - cib: e.data.cib || null - }, - session.iframetarget - ); - } - - if ("getGuestList" in e.data) { - var guestList = getGuestList(); - parent.postMessage( - { - guestList: guestList, - cib: e.data.cib || null - }, - session.iframetarget - ); - } - - // saveVideoFrameToDisk(video); - - if ("saveVideoFrameToDisk" in e.data) { - let filename = false; - if ("filename" in e.data) { - filename = e.data.filename; - if (filename.split(".").length == 1) { - filename += ".png"; - } - } - if ("streamID" in e.data) { - let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); - if (session.rpcs[UUID]) { - if (session.rpcs[UUID].videoElement) { - saveVideoFrameToDisk(session.rpcs[UUID].videoElement, false, filename); - } else { - errorlog("The specified video does not exist"); - } - } else { - errorlog("The stream ID specified does not exist"); - } - } else if ("UUID" in e.data) { - if (e.data.UUID === "*") { - for (var uuid in session.rpcs) { - if (session.rpcs[uuid].videoElement) { - saveVideoFrameToDisk(session.rpcs[uuid].videoElement, false, filename); - } - } - } else if (session.rpcs[e.data.UUID]) { - if (session.rpcs[e.data.UUID].videoElement) { - saveVideoFrameToDisk(session.rpcs[e.data.UUID].videoElement, false, filename); - } else { - errorlog("The specified video does not exist"); - } - } else { - errorlog("The UUID specified does not exist"); - } - } else if (session.videoElement) { - saveVideoFrameToDisk(session.videoElement, false, filename); - } - } - - if ("getVideoFrame" in e.data) { - if ("streamID" in e.data) { - let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); - if (session.rpcs[UUID]) { - if (session.rpcs[UUID].videoElement) { - sendVideoFrameToIframe(session.rpcs[UUID].videoElement, false, e.data); - } else { - errorlog("The specified video does not exist"); - } - } else { - errorlog("The stream ID specified does not exist"); - } - } else if ("UUID" in e.data) { - if (session.rpcs[e.data.UUID]) { - if (session.rpcs[e.data.UUID].videoElement) { - sendVideoFrameToIframe(session.rpcs[e.data.UUID].videoElement, false, e.data); - } else { - errorlog("The specified video does not exist"); - } - } else { - errorlog("The UUID specified does not exist"); - } - } else if (session.videoElement) { - sendVideoFrameToIframe(session.videoElement, false, e.data); - } - } - - if ("copyVideoFrameToClipboard" in e.data) { - if ("streamID" in e.data) { - let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); - if (session.rpcs[UUID]) { - if (session.rpcs[UUID].videoElement) { - copyVideoFrameToClipboard(session.rpcs[UUID].videoElement, false); - } else { - errorlog("The specified video does not exist"); - } - } else { - errorlog("The stream ID specified does not exist"); - } - } else if ("UUID" in e.data) { - if (session.rpcs[e.data.UUID]) { - if (session.rpcs[e.data.UUID].videoElement) { - copyVideoFrameToClipboard(session.rpcs[e.data.UUID].videoElement, false); - } else { - errorlog("The specified video does not exist"); - } - } else { - errorlog("The UUID specified does not exist"); - } - } else if (session.videoElement) { - copyVideoFrameToClipboard(session.videoElement, false); - } - } - - if ("setBufferDelay" in e.data) { - // milliseconds - console.log(e.data); - let delay = parseInt(e.data.setBufferDelay) || 0; - - if ("streamID" in e.data) { - let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); - if (session.rpcs[UUID]) { - session.rpcs[UUID].buffer = delay; - playoutdelay(UUID); - } else { - errorlog("The stream ID specified does not exist"); - } - document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + UUID + '"] input[data-buffer-value]').forEach(ele => { - ele.value = delay; - }); - } else if ("label" in e.data) { - for (let uuid in session.rpcs) { - if (session.rpcs[uuid].label && session.rpcs[uuid].label === e.data.label) { - if (session.rpcs[uuid]) { - session.rpcs[uuid].buffer = delay; - playoutdelay(uuid); - } else { - errorlog("The label specified does not exist"); - } - } - } - document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + UUID + '"] input[data-buffer-value]').forEach(ele => { - ele.value = delay; - }); - } else if ("UUID" in e.data) { - if (e.data.UUID === "*") { - for (var uuid in session.rpcs) { - session.rpcs[uuid].buffer = delay; - playoutdelay(uuid); - document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + uuid + '"] input[data-buffer-value]').forEach(ele => { - ele.value = delay; - }); - } - } else if (session.rpcs[e.data.UUID]) { - session.rpcs[e.data.UUID].buffer = delay; - playoutdelay(e.data.UUID); - document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + e.data.UUID + '"] input[data-buffer-value]').forEach(ele => { - ele.value = delay; - }); - } else { - errorlog("The UUID specified does not exist"); - } - } else { - session.buffer = delay; // set the default buffer delay only - } - } - - if ("automixer" in e.data) { - // stop the auto mixer if you want to control the layout and bitrate yourself - if (e.data.automixer == true) { - session.manual = session.manual === null ? false : session.manual; - try { - updateMixer(); - } catch (e) {} - } else if (e.data.automixer == false) { - session.manual = session.manual === null ? true : session.manual; - } - } - - if ("advancedMode" in e.data) { - if (e.data.advancedMode) { - // Un-hiding advanced items - document.querySelectorAll(".advanced").forEach(element => { - element.classList.remove("hide"); - }); - } else { - // Hiding advanced items - document.querySelectorAll(".advanced").forEach(element => { - element.classList.add("hide"); - }); - } - } - - if ("requestStream" in e.data) { - if (e.data.requestStream) { - // load a specific stream ID - log("requestStream iframe api"); - session.requestStream(e.data.requestStream); - } // don't use if the stream is in your room (as not needed) - } // you can load a stream ID from inside a room that exists outside any room - - if ("layout" in e.data) { - if (Array.isArray(e.data.layout)){ - session.layout_array = e.data.layout; - session.layout = combinedLayout(session.layout_array); - } else { - session.layout_array = null; - session.layout = e.data.layout; - } - // update mixer is run later, some lines down. - pokeIframeAPI("layout-updated", session.layout, null, null, e.data.cib); - } - - if ("previewMode" in e.data) { - switchModes(e.data.previewMode); - } else if ("layout" in e.data) { - warnlog("changing layout request via IFRAME API"); - if (e.data.obsCommand) { - issueLayoutOBS(e.data); - } else if (session.director) { // only a director can issue a layout - if ("scene" in e.data) { - if ("UUID" in e.data) { - issueLayout(e.data.scene, e.data.UUID); - } else { - issueLayout(e.data.scene); - } - } else if ("UUID" in e.data) { - issueLayout(false, e.data.UUID); - } - } - updateMixer(); - } else if (e.data.obsCommand) { - var msg = {}; - msg.obsCommand = e.data.obsCommand; - if (e.data.remote) { - msg.remote = e.data.remote; - } else { - msg.remote = session.remote; - } - if (e.data.UUID) { - msg.UUID = e.data.UUID; - } - if (e.data.streamID) { - msg.streamID = e.data.streamID; - } - session.encodeRemote(msg).then(msgx => { - session.sendMessage(msgx); // just to be safe; avoids spamming of wss - log(msgx); - }); - } - - if ("slotmode" in e.data) { - if (session.slotmode) { - session.slotmode = parseInt(e.data.slotmode); - populateSlotPicker(); - } else { - session.slotmode = false; - } - } - - //////////// manual scale. Request a specific down-scaled resolution from a remote connection - var targetWidth = false; - var targetHeight = false; - if ("targetWidth" in e.data) { - targetWidth = e.data.targetWidth || 0; - } - if ("targetHeight" in e.data) { - targetHeight = e.data.targetHeight || 0; - } - // session.viewheight or session.viewwidth - if ((targetWidth || targetHeight) && e.data.UUID) { - var requestAs = false; - if (e.data.requestAs) { - requestAs = e.data.requestAs; - } - session.requestResolution(e.data.UUID, targetWidth || 4096, targetHeight || 2160, false, requestAs); // this is fine. - } - //////////////// - - if ("scale" in e.data) { - if (e.data.scale === false) { - session.dynamicScale = true; // disable manual scaling - updateMixer(); - var scale = false; - } else { - session.dynamicScale = false; - var scale = parseInt(e.data.scale) || 100; - } - - if (e.data.UUID) { - session.sendRequest({ scale: scale }, UUID); - } else if (e.data.target) { - for (var i in session.rpcs) { - try { - if ("streamID" in session.rpcs[i]) { - if ("target" in e.data) { - if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { - // specify a stream ID or let it apply to all videos - session.sendRequest({ scale: scale }, i); - } - } else { - session.sendRequest({ scale: scale }, i); - } - } - } catch (e) { - errorlog(e); - } - } - } else { - session.sendRequest({ scale: scale }); - } - } - - if ("action" in e.data && e.data.action != "null") { - /////////////// reuse the Companion API - var resp = processMessage(e.data); // reuse the companion API - if (resp !== null) { - log(resp); - parent.postMessage(resp, session.iframetarget, null, null, e.data.cib); - } - } else if ("target" in e.data) { - log(e.data); - for (var i in session.rpcs) { - try { - if ("streamID" in session.rpcs[i]) { - if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { - try { - if ("settings" in e.data) { - try { - for (const property in e.data.settings) { - try { - session.rpcs[i].videoElement[property] = e.data.settings[property]; - } catch (e) {} - } - } catch (e) {} - } - if ("add" in e.data) { - try { - getById("gridlayout").appendChild(session.rpcs[i].videoElement); - } catch (e) { - warnlog(e); - } - } else if ("remove" in e.data) { - try { - session.rpcs[i].videoElement.parentNode.removeChild(session.rpcs[i].videoElement); - } catch (e) { - try { - session.rpcs[i].videoElement.parentNode.parentNode.removeChild(session.rpcs[i].videoElement.parentNode); - } catch (e) {} - } - } else if ("replace" in e.data) { - // should allow for a cleaner cut between two video streams. - try { - getById("gridlayout").appendChild(session.rpcs[i].videoElement); - getById("gridlayout").childNodes.forEach(ele => { - if (!ele.id || ele.id !== session.rpcs[i].videoElement.id) { - getById("gridlayout").removeChild(ele); - } - }); - } catch (e) {} - } - } catch (e) { - errorlog(e); - } - } - } - } catch (e) { - errorlog(e); - } - } - } - }; - - if (isIFrame) { - // reduce CPU load if not needed. //iframe API - window.onmessage = session.remoteInterfaceAPI; - } - - if (session.midiHotkeys || session.midiOut !== false) { - var script = document.createElement("script"); - script.onload = function () { - WebMidi.enable({ sysex: true }) - .then(() => { - WebMidi.timeStart = Date.now(); // start time - - WebMidi.addListener("connected", function (e) { - log(e); - }); - - WebMidi.addListener("disconnected", function (e) { - log(e); - }); - - console.log(WebMidi.inputs); - - if (session.midiOut === true) { - for (var i = 0; i < WebMidi.inputs.length; i++) { - try { - var input = WebMidi.inputs[i]; - input.addListener("midimessage", function (e) { - //log(e); - e.timestamp += WebMidi.timeStart; - sendRawMIDI(e); - //var msg = {}; - //msg.midi = {}; - //msg.midi.d = e.data; aka [d1,d2,d3]; - //msg.midi.c = e.channel; - //msg.midi.s = e.timestamp; - }); - } catch (e) {} - } - } else if (session.midiOut == parseInt(session.midiOut)) { - try { - var input = WebMidi.inputs[parseInt(session.midiOut) - 1]; - input.addListener("midimessage", function (e) { - e.timestamp += WebMidi.timeStart; - sendRawMIDI(e); - }); - } catch (e) { - errorlog(e); - } - } - - for (var i = 0; i < WebMidi.inputs.length; i++) { - if (session.midiDevice && session.midiDevice !== i + 1) { - continue; - } - - var input = WebMidi.inputs[i]; - if (session.midiChannel) { - input = input.channels[session.midiChannel]; - } - if (session.midiHotkeys == 4) { - input.addListener("controlchange", function (e) { - log(e); - midiHotkeysCommand(e.controller.number, e.rawValue); - }); - } else if (session.midiHotkeys == 5) { - if (session.midiOffset !== false) { - input.addListener("controlchange", function (e) { - midiHotkeysCommand_offset(e.controller.number, e.rawValue, session.midiOffset); - }); - } - } else { - input.addListener("noteon", function (e) { - log(e); - var note = e.note.name + e.note.octave; - var velocity = e.velocity || false; - midiHotkeysNote(note, velocity); - }); - } - } - }) - .catch(errorlog); - }; - script.src = "./thirdparty/webmidi3.js"; // dynamically load this only if its needed. Keeps loading time down. - document.head.appendChild(script); - } else if (session.midiIn) { - var script = document.createElement("script"); - script.src = "./thirdparty/webmidi3.js"; // dynamically load this only if its needed. Keeps loading time down. - script.onload = function () { - WebMidi.enable({ sysex: true }) - .then(() => console.log(WebMidi.outputs)) - .catch(errorlog); - }; - document.head.appendChild(script); - } - - var languages = getById("languagesList").querySelectorAll("li a"); - var timezones = []; - - languages.forEach(language => { - if (language.dataset.tz) { - var languageTimezones = language.dataset.tz.split(";"); // each link can have multiple timezones separated by ; - languageTimezones.forEach(element => { - timezones.push(element); - }); - } - }); - - var currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - - if (timezones.includes(currentTimezone)) { - var list = getById("languagesList"); - // Find the link element matching the timezone - var el = list.querySelector("li a[data-tz*='" + currentTimezone + "']"); - - if (el) { - var targetLi = el.parentElement; // Get the parent
  • - var firstLi = list.querySelector("li:first-child"); // Get the first li (English) - - // Only move if it's not already the first or second element - if (targetLi && firstLi && targetLi !== firstLi && targetLi !== firstLi.nextSibling) { - // Insert the
  • after the first element (English) - list.insertBefore(targetLi, firstLi.nextSibling); - // The original
  • is automatically removed from its old position when inserted elsewhere. - } - } - } - - var visAudioTimeout = null; - document.addEventListener("visibilitychange", function () { - //log("hidden : " +document.hidden); - //log("vis : "+document.visibilityState); - if (iOS || iPad) { - // fixes a bug on iOS devices. Not need with other devices? - toggleAutoVideoMute(); - clearTimeout(visAudioTimeout); - if (document.visibilityState === "visible") { - visAudioTimeout = setTimeout(function () { - resetupAudioOut(); - activatedPreview = false; - grabAudio("#audioSource3"); - }, 500); - } - } - }); - - // Warns user about network going down - window.addEventListener("offline", function (e) { - warnlog("connection lost"); - if (((session.view!==false) || session.whepInput || session.whipView) && session.permaid === false) { - log("VDO.Ninja has no network connectivity and can't work properly."); - } else if (session.scene !== false) { - log("VDO.Ninja has no network connectivity and can't work properly."); - } else if (!session.cleanOutput) { - if (iOS || iPad) { - for (var UUID in session.pcs) { - session.pcs[UUID].close(); - delete session.pcs[UUID]; - session.applySoloChat(); - applySceneState(); - } - } - if (location.hostname === "vdo.ninja") { - warnUser(getTranslation("no-network-details")); - } else { - warnUser(getTranslation("no-network")); - } - } else { - log("VDO.Ninja has no network connectivity and can't work properly."); - } - }); - - window.addEventListener("online", function (e) { - log("Back ONLINE"); - closeModal(); - - if (!session.onceConnected) { - // never connected to websockets before. Let's not trigger retryWatchInterval if we don't have to. - return; - } - - if (!session.retryWatchInterval()) { - // ask for the streams again to watch - session.ping(); // if no streams requested, let's ping instead. - } - }); - - /* function updateConnectionStatus() { // no longer works in chrome. - - try{ - if (!session.stats){ - return; - } - - if (Connection.type){ - log("Connection type changed from " + session.stats.network_type + " to " + Connection.type); - - if (session.stats.network_type && (session.stats.network_type !== Connection.type)){ - var miniInfo = {}; - miniInfo.con = Connection.type; - session.sendMessage({"miniInfo":miniInfo}); - - if (!session.retryWatchInterval()){ // ask for the streams again to watch - session.ping(); // if no streams requested, let's ping instead. - } - - } else { // connection state changed, but doesn't seem like it actually changed... - session.ping(); // if no streams requested, let's ping instead. - } - - session.stats.network_type = Connection.type; - } - - } catch(e){warnlog(e);} - - } - - try { - var Connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; - if (Connection){ - if (Connection.type){ - session.stats.network_type = Connection.type - } - Connection.addEventListener('change', updateConnectionStatus); - } - } catch (e) {log(e);} // effectiveType is not yet supported by Firefox or Safari; 2021 */ - - setInterval(function () { - checkConnection(); - }, 5000); - - // Remove modal if network comes back up - window.addEventListener("online", function (e) { - if (!session.cleanOutput) { - // Remove last inserted modal; Could be improved by tagging the - // modal elements and only removing modals tagged 'offline' - let userWarnings = document.querySelectorAll(".alertModal"); - closeModal(userWarnings[userWarnings.length - 1]); - } else { - log("Network connectivity has been restored."); - } - }); - - document.addEventListener("DOMContentLoaded", function () { - var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy")); - - if ("IntersectionObserver" in window) { - var lazyVideoObserver = new IntersectionObserver(function (entries, observer) { - entries.forEach(function (video) { - if (video.isIntersecting) { - for (var source in video.target.children) { - var videoSource = video.target.children[source]; - if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") { - videoSource.src = videoSource.dataset.src; - } - } - - video.target.load(); - video.target.classList.remove("lazy"); - lazyVideoObserver.unobserve(video.target); - } - }); - }); - - lazyVideos.forEach(function (lazyVideo) { - lazyVideoObserver.observe(lazyVideo); - }); - } - - armWakeLockOnInteraction(); - acquireWakeLock(); - // Re-acquire wake lock when the page becomes visible again, as that's a requirement for wakelock - document.addEventListener('visibilitychange', handleVisibilityChangeWakeLock); - - // Initialize fullscreen/PIP button settings from localStorage - initButtonToggleSettings(); - - }); - - document.addEventListener("dragstart", event => { - var url = event.target.href || event.target.value; - if (!url || !url.startsWith("https://")) return; - if (event.target.dataset.drag != "1") { - return; - } - //event.target.ondragend = function(){event.target.blur();} - - var streamId = url.split("view="); - var label = url.split("label="); - - if (session.label !== false) { - url += "&layer-name=" + session.label; - } else { - url += "&layer-name=VDO.Ninja"; - } - if (streamId.length > 1) url += ": " + streamId[1].split("&")[0]; - if (label.length > 1) url += " - " + decodeURI(label[1].split("&")[0]); - - try { - if (document.getElementById("videosource")) { - var video = getById("videosource"); - if (typeof video.videoWidth == "undefined") { - url += "&layer-width=1920"; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += "&layer-height=1080"; - } else if (parseInt(video.videoWidth) < 360 || video.videoHeight < 640) { - url += "&layer-width=1920"; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += "&layer-height=1080"; - } else { - url += "&layer-width=" + video.videoWidth; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += "&layer-height=" + video.videoHeight; - } - } else { - url += "&layer-width=1920"; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += "&layer-height=1080"; - } - } catch (error) { - url += "&layer-width=1920"; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += "&layer-height=1080"; - } - - event.dataTransfer.setDragImage(getById("dragImage"), 24, 24); - event.dataTransfer.setData("text/uri-list", encodeURI(url)); - }); - - if (navigator.getBattery) { - navigator.getBattery().then(function (battery) { - session.batteryState = {}; - if ("level" in battery) { - session.batteryState.level = battery.level; - } - if ("charging" in battery) { - session.batteryState.charging = battery.charging; - } - - if (session.batteryState == {}) { - session.batteryState = null; - } - battery.addEventListener("chargingchange", function () { - session.batteryState = {}; - var miniInfo = {}; - if ("level" in battery) { - session.batteryState.level = battery.level; - miniInfo.bat = battery.level; - } - if ("charging" in battery) { - session.batteryState.charging = battery.charging; - miniInfo.chrg = battery.charging; - } - if (session.batteryState == {}) { - session.batteryState = null; - } - session.sendMessage({ miniInfo: miniInfo }); - }); - - battery.addEventListener("levelchange", function () { - session.batteryState = {}; - var miniInfo = {}; - console.log(session.batteryState); - if ("charging" in battery) { - session.batteryState.charging = battery.charging; - miniInfo.chrg = battery.charging; - } - if ("level" in battery) { - session.batteryState.level = battery.level; - miniInfo.bat = battery.level; - - if (!session.batteryState.charging && battery.level == 0.02) { - warnlog("Very Low Battery - triggering auto saves"); - try { - if (session.screenShareElement && session.screenShareElement.recorder && session.screenShareElement.recorder.setupWriter) { - session.screenShareElement.recorder.setupWriter(session.screenShareElement); - } - if (session.videoElement && session.videoElement.recorder && session.videoElement.recorder.setupWriter) { - session.videoElement.recorder.setupWriter(session.videoElement); - } - } catch (e) { - errorlog(e); - } - } - } - if (session.batteryState == {}) { - session.batteryState = null; - } - session.sendMessage({ miniInfo: miniInfo }); - }); - }); - } - - var lastTouchEnd = 0; - document.addEventListener( - "touchend", - function (event) { - var now = new Date().getTime(); - if (now - lastTouchEnd <= 300) { - event.preventDefault(); - } - lastTouchEnd = now; - }, - false - ); - - document.addEventListener("click", function (event) { - if (session.firstPlayTriggered == false) { - playAllVideos(); - session.firstPlayTriggered = true; - - try { - if (session.audioCtx && session.audioCtx.state == "suspended") { - session.audioCtx.resume(); - } - } catch (e) { - warnlog("session.audioCtx.resume(); failed 4"); - } - - history.pushState({}, ""); - } - }); - - document.addEventListener("keydown", event => { - keyDownEvent(event); - }); - - function keyDownEvent(event) { - if (event.ctrlKey || event.metaKey) { - // detect if CTRL is pressed - CtrlPressed = true; - } else { - CtrlPressed = false; - } - if (event.altKey) { - AltPressed = true; - } else { - AltPressed = false; - } - - if (event.key === "Escape") { - log("escape pressed; checking to see if modal box opened and will close"); - if (document.fullscreenElement) { - document.exitFullscreen(); - //updateMixer(); - } else { - let userWarnings = document.querySelectorAll(".alertModal, .promptModal"); - if (userWarnings.length) { - closeModal(userWarnings[userWarnings.length - 1]); - } - } - return; - } - - if (session.disableHotKeys) { - return; - } - - if (PPTHotkey) { - if (event.target && event.target.tagName == "INPUT") { - // skip, since an input field is selected - } else if (PPTHotkey.ctrl === event.ctrlKey && PPTHotkey.alt === AltPressed && PPTHotkey.meta === event.metaKey && (PPTHotkey.key === false || (PPTHotkey.key !== false && PPTHotkey.key === event.key))) { - if (session.muted && !PPTKeyPressed) { - session.muted = false; - PPTKeyPressed = true; - getById("mutebutton").classList.add("PPTActive"); - toggleMute(true); - } else if (!PPTKeyPressed) { - PPTKeyPressed = true; - getById("mutebutton").classList.add("PPTActive"); - } - event.preventDefault(); - event.stopPropagation(); - return; - } else if (PPTKeyPressed) { - PPTKeyPressed = false; - getById("mutebutton").classList.remove("PPTActive"); - if (!session.muted) { - session.muted = true; - toggleMute(true); - } - event.preventDefault(); - event.stopPropagation(); - return; - } - } - - if (KeyPressedTimeout || PPTKeyPressed) { - event.preventDefault(); - event.stopPropagation(); - return; - } - - if (CtrlPressed && event.keyCode) { - if (event.keyCode == 77) { - // M - if (event.metaKey) { - if (AltPressed) { - if (!KeyPressedTimeout) { - toggleMute(); // macOS - KeyPressedTimeout = Date.now(); - event.preventDefault(); - event.stopPropagation(); - return; - } - } - } else { - if (!KeyPressedTimeout) { - toggleMute(); // Windows - KeyPressedTimeout = Date.now(); - event.preventDefault(); - event.stopPropagation(); - return; - } - } - } else if (event.keyCode == 66) { - // B - toggleVideoMute(); - event.preventDefault(); - event.stopPropagation(); - return; - } - - if (AltPressed) { - // CTRL + ALT - if (event.keyCode == 70) { - // F - toggleFileshare(); - event.preventDefault(); - event.stopPropagation(); - return; - } else if (event.keyCode == 67) { - // C - cycleCameras(); - event.preventDefault(); - event.stopPropagation(); - return; - } else if (event.keyCode == 83) { - // S - toggleScreenShare(); - event.preventDefault(); - event.stopPropagation(); - return; - } else if (event.keyCode == 68) { - // D - if (!drawOnScreenObject) { - drawOnScreen(); - } else { - drawOnScreenObject.stop(); - } - event.preventDefault(); - event.stopPropagation(); - return; - } else if (event.keyCode == 80) { - // P - if (session.videoElement) { - togglePictureInPicture(session.videoElement); - event.preventDefault(); - event.stopPropagation(); - return; - } - } - } - } else if (AltPressed && event.keyCode) { - if (event.keyCode == 65) { - // A - toggleSpeakerMute(); - event.preventDefault(); - event.stopPropagation(); - return; - } else if (event.key === "s") { - if (document.getElementById("gowebcam") && document.getElementById("gowebcam").dataset.ready == "true") { - publishWebcam(document.getElementById("gowebcam")); - } - } - } - } - - document.addEventListener("mouseup", event => { - MousePressed = false; - }); - - document.addEventListener("mousedown", event => { - MousePressed = true; - }); - - document.addEventListener("keyup", event => { - if (PPTKeyPressed) { - PPTKeyPressed = false; - getById("mutebutton").classList.remove("PPTActive"); - if (!session.muted) { - session.muted = true; - toggleMute(true); - } - event.preventDefault(); - event.stopPropagation(); - return; - } - - if (!(event.ctrlKey || event.metaKey)) { - if (CtrlPressed) { - CtrlPressed = false; - for (var i in Callbacks) { - var cb = Callbacks[i]; - cb[0](...cb.slice(1)); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#A_better_apply - } - Callbacks = []; - } - } - if (!event.altKey) { - AltPressed = false; - } - - if (event.altKey && event.shiftKey && event.keyCode === 67 /* C */) { - toggleControlBar(); - } - if (KeyPressedTimeout && (event.keyCode == 77 || !(event.ctrlKey || event.metaKey))) { - if (Date.now() - KeyPressedTimeout > 300) { - toggleMute(); - } - if (event.keyCode == 77) { - KeyPressedTimeout = 0; - } - } - }); - - try { - navigator.serviceWorker - .getRegistrations() - .then(registrations => { - // getting rid of old service workers. - try { - log(registrations); - for (let registration of registrations) { - if (registration.scope) { - if (registration.scope.includes("/notifications/")) {continue;} - registration.unregister(); - console.warn("unregistering: " + registration.scope); - } - } - } catch (e) {} - }) - .catch(errorlog); - } catch (e) {} - - setTimeout(function () { - // lets lazy load the following.. - window.addEventListener("beforeunload", confirmUnload); // This just keeps people from killing the live stream accidentally. Also give me a headsup that the stream is ending - window.addEventListener("unload", function (e) { - try { - if (session.ws) { - session.ws.close(); - } - if (session.videoElement.recording) { - session.videoElement.recorder.writer.close(); - session.videoElement.recording = false; - } - for (var i in session.rpcs) { - if (session.rpcs[i].videoElement) { - if (session.rpcs[i].videoElement.recording) { - session.rpcs[i].videoElement.recorder.writer.close(); - session.rpcs[i].videoElement.recording = false; - } - } - } - session.hangup(); - } catch (e) { - errorlog(e); - } - }); - - var script = document.createElement("script"); - document.head.appendChild(script); - script.onload = function () { - var script = document.createElement("script"); - document.head.appendChild(script); - if (SafariVersion && SafariVersion <= 15) { - // blob mode not needed since iOS 15.6 - script.src = "./thirdparty/StreamSaver_legacy.js?v=2"; // blob mode for Safari - } else { - script.src = "./thirdparty/StreamSaver.js?v=29"; // do not use blob mode - } - }; - script.src = "./thirdparty/polyfill.min.js"; // dynamically load this only if its needed. Keeps loading time down. - }, 100); -} + + if (urlParams.has("audiogain") || urlParams.has("gain") || urlParams.has("g") || urlParams.has("muteguest")) { + log("audio gain ENABLED"); + session.audioGain = urlParams.get("audiogain") || urlParams.get("gain") || urlParams.get("g") || 0; + session.audioGain = parseInt(session.audioGain) || 0; + session.disableWebAudio = false; + } + if (urlParams.has("volume") || urlParams.has("vol")) { + // This sets the default volume for all new video playback elements; 0 to 100. + log("setting default volume for playback"); + session.volume = urlParams.get("volume") || urlParams.get("vol") || 100; + session.volume = parseInt(session.volume) || 0; + session.volume = session.volume / 100; // 0 to 1.0 + } + if (urlParams.has("compressor") || urlParams.has("comp")) { + log("audio gain ENABLED"); + session.compressor = 1; + session.disableWebAudio = false; + } else if (urlParams.has("limiter")) { + log("audio gain ENABLED"); + session.compressor = 2; + session.disableWebAudio = false; + } + if (urlParams.has("equalizer") || urlParams.has("eq")) { + session.equalizer = true; + session.disableWebAudio = false; + } + // Mic panning (publisher-side): downmix to mono, then pan to stereo output. + if (urlParams.has("micpanning") || urlParams.has("mpan")) { + let mp = urlParams.get("micpanning") || urlParams.get("mpan"); + if (mp === null || mp === "" || mp === "true") { + session.micPanning = 90; // center by default, but enable control + } else { + mp = parseInt(mp); + if (isNaN(mp)) { mp = 90; } + if (mp < 0) { mp = 0; } + if (mp > 180) { mp = 180; } + session.micPanning = mp; + } + // Ensure WebAudio outbound pipeline is enabled (unless &noap set later) + session.disableWebAudio = false; + } + if (urlParams.has("lowcut") || urlParams.has("lc") || urlParams.has("higpass")) { + session.lowcut = urlParams.get("lowcut") || urlParams.get("lc") || urlParams.get("higpass") || 100; + session.lowcut = parseInt(session.lowcut); + session.disableWebAudio = false; + } + + if (urlParams.has("pip")) { + session.pip = true; // togglePip + //session.manual=true; + //innerHTML = + } + if (urlParams.has("pip3") || urlParams.has("mypip") || urlParams.has("pipme")) { + session.pip3 = true; + } + + if (urlParams.has("manual")) { + session.manual = true; + } + + if (urlParams.has("keyframeinterval") || urlParams.has("keyframerate") || urlParams.has("keyframe") || urlParams.has("fki")) { + log("keyframeRate ENABLED"); + session.keyframeRate = parseInt(urlParams.get("keyframeinterval") || urlParams.get("keyframerate") || urlParams.get("keyframe") || urlParams.get("fki")) || 0; + } + + if (urlParams.has("obsoff") || urlParams.has("oo") || urlParams.has("disableobs")) { + log("OBS feedback disabled"); + session.disableOBS = true; + getById("obsState").style.setProperty("display", "none", "important"); + } + + if (urlParams.has("hidecodirectors") || urlParams.has("hidecodirector") || urlParams.has("hidedirector") || urlParams.has("hidedirectors") || urlParams.has("hd")) { + document.querySelector(":root").style.setProperty("--show-codirectors", "none", "important"); + session.hideDirector = true; + } + + if (urlParams.has("pptcontrols") || urlParams.has("slides") || urlParams.has("ppt") || urlParams.has("powerpoint")) { + session.pptControls = true; // shows powerpoint controls to remotely control a powerpoint slide. Requires additional remote setup. + } + + if (urlParams.has("allowedscenes")) { + session.filterOBSscenes = urlParams.get("allowedscenes"); + if (session.filterOBSscenes) { + session.filterOBSscenes = session.filterOBSscenes.split(","); + } else { + session.filterOBSscenes = true; + } + } + + if (urlParams.has("tallyoff") || urlParams.has("notally") || urlParams.has("disabletally") || urlParams.has("to")) { + log("Tally Light off"); + getById("obsState").style.setProperty("display", "none", "important"); + } else if (urlParams.has("tally")) { + session.tallyStyle = 1; + session.tallyStyleDefault = 1; + getById("obsState").classList.add("larger"); + } + + if (urlParams.has("automute") || urlParams.has("am")) { + session.automute = urlParams.get("automute") || true; + session.micIsolatedAutoMute = []; // default auto mutes + } + + if (urlParams.has("noobsstream")){ + session.obsState.streaming = false; + } + if (urlParams.has("noobsvirtual")){ + session.obsState.virtualcam = false; + } + if (urlParams.has("noobsrecord")){ + session.obsState.recording = false; + } + if (urlParams.has("noobssourceactive")){ + session.obsState.sourceActive = false; + } + if (urlParams.has("noobsvisibility")){ + session.obsState.visibility = false; + } + + if (window.obsstudio) { + session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? + session.audioMeterGuest = false; + + getById("miniTaskBar").classList.add("hidden"); + + if (session.audioEffects === null) { + session.audioEffects = false; + } + if (window.obsstudio.pluginVersion) { + if (macOS) { + // if mac, no fix + //session.obsfix = false; + } else if (window.obsstudio.pluginVersion == "2.17.4") { + // if obs v27.2 beta, no fix + //session.obsfix = false; + } else { + var ver = window.obsstudio.pluginVersion.split("."); + if (ver.length >= 2) { + if (parseInt(ver[0]) <= 2) { + if (parseInt(ver[0]) == 2) { + if (parseInt(ver[1]) <= 16) { + session.obsfix = 15; + } + } else { + session.obsfix = 15; + } + } + } + } + } + try { + log("OBS VERSION:" + window.obsstudio.pluginVersion); + log("macOS: " + macOS); + log(window.obsstudio); + + if (!urlParams.has("streamlabs")) { + var ver1 = window.obsstudio.pluginVersion.split("."); + + if (ver1.length == 3) { + // Should be 3, but disabled3 + if (ver1.length == 3 && parseInt(ver1[0]) == 2 && ChromiumVersion < 76 && macOS) { + updateURL("streamlabs"); + getById("main").innerHTML = + "

    Update OBS Studio to v26.1.2 or newer; older versions and StreamLabs OBS are not supported on macOS.\ +
    download here: https://github.com/obsproject/obs-studio/releases\ +



    \ +

    Please use the Electron Capture app if there are further problems or if you wish to use StreamLabs on macOS still.

    \ +
    You can bypass this error message by refreshing, Clicking Here, or by adding &streamlabs to the URL, but it may still not actually work.\ + \ +
    Please report this problem to steve@seguin.email if you feel it is an error.\ +
    "; + } + } + } + + //if (navigator.userAgent.indexOf('Mac OS X') != -1) { + // session.codec = "h264"; // default the codec to h264 if OBS is on macOS (that's all it supports with hardware) // oct 2021, OBS now supports vp8 and actually breaks with h264 android devices. + //} + + if (session.disableOBS === false) { + + + getOBSDetails(); + + window.addEventListener("obsSceneChanged", obsSceneChanged); + if (session.obsState.visibility!==false){ + window.addEventListener("obsSourceVisibleChanged", obsSourceVisibleChanged); + if (typeof document.visibilityState !== "undefined") { + session.obsState.visibility = document.visibilityState === "visible"; + } + } + if (session.obsState.sourceActive!==false){ + window.addEventListener("obsSourceActiveChanged", obsSourceActiveChanged); + } + if (session.obsState.streaming!==false){ + window.addEventListener("obsStreamingStarted", obsStreamingStarted); + window.addEventListener("obsStreamingStopped", obsStreamingStopped); + } + if (!session.obsState.recording!==false){ + window.addEventListener("obsRecordingStarted", obsRecordingStarted); + window.addEventListener("obsRecordingStopped", obsRecordingStopped); + } + if (session.obsState.virtualcam!==false){ + window.addEventListener("obsVirtualcamStarted", obsVirtualcamStarted); + window.addEventListener("obsVirtualcamStopped", obsVirtualcamStopped); + } + } + } catch (e) { + errorlog(e); + } + } else if (session.studioSoftware){ + session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? + session.audioMeterGuest = false; + + getById("miniTaskBar").classList.add("hidden"); + + if (session.audioEffects === null) { + session.audioEffects = false; + } + } + + if (urlParams.has("chroma")) { + log("Chroma ENABLED"); + getById("main").style.backgroundColor = "#" + (urlParams.get("chroma") || "0F0"); + } + + if (urlParams.has("margin")) { + try { + session.videoMargin = urlParams.get("margin") || 10; + session.videoMargin = parseInt(session.videoMargin); + //document.querySelector(':root').style.setProperty('--video-margin', session.videoMargin+"px"); + } catch (e) { + errorlog("variable css failed"); + } + } + + if (urlParams.has("rounded") || urlParams.has("round")) { + try { + session.borderRadius = urlParams.get("rounded") || urlParams.get("round") || 50; + session.borderRadius = parseInt(session.borderRadius); + document.querySelector(":root").style.setProperty("--video-rounded", session.borderRadius + "px"); + session.manual = session.manual === null ? false : session.manual; + session.windowed = session.windowed === null ? false : session.windowed; + } catch (e) { + errorlog("variable css failed"); + } + } + + if (urlParams.has("border")) { + try { + var videoBorder = urlParams.get("border") || 10; + videoBorder = parseInt(videoBorder); + session.border = videoBorder; + videoBorder += "px"; + document.querySelector(":root").style.setProperty("--video-border-color", "#000"); + document.querySelector(":root").style.setProperty("--video-border", videoBorder); + + session.manual = session.manual === null ? false : session.manual; + session.windowed = session.windowed === null ? false : session.windowed; + } catch (e) { + errorlog("variable css failed"); + } + } + + if (urlParams.has("bordercolor")) { + try { + session.borderColor = urlParams.get("bordercolor") || "#000"; + document.querySelector(":root").style.setProperty("--video-border-color", session.borderColor); + } catch (e) { + errorlog("variable css failed"); + } + } + + if (urlParams.has("holdercolor")) { + try { + session.holderColor = urlParams.get("holdercolor") || session.borderColor || "#000"; + if (parseInt(session.holderColor) == session.holderColor){ + session.holderColor = "#"+session.holderColor; + } + document.querySelector(":root").style.setProperty("--video-holder-color", session.holderColor); + } catch (e) { + errorlog("variable css failed"); + } + } + + if (urlParams.has("color")) { + session.colorVideosBackground = urlParams.get("color") || session.borderColor || "#000"; + } + + if (urlParams.has("retry")) { + session.forceRetry = parseInt(urlParams.get("retry")) || 30; + } + + if (session.forceRetry) { + clearInterval(session.forceRetryTimeout); + session.forceRetryTimeout = setTimeout(function () { + try { + session.retryWatchInterval(); + } catch (e) { + log(e); + clearTimeout(this); + } + }, session.forceRetry * 1000); + } + + if (urlParams.get("dropbox")) { + setupDropbox(urlParams.get("dropbox")).then(() => { + log("Loaded dropbox SDK"); + }).catch(e => errorlog(e)); + } + if (urlParams.has("gdrive")) { + session.gdrive = {}; + } + + try { + if (urlParams.has("darkmode") || urlParams.has("nightmode") || urlParams.has("darktheme")) { + session.darkmode = urlParams.get("darkmode") || urlParams.get("nightmode") || urlParams.get("darktheme") || null; + if (session.darkmode === null || session.darkmode === "") { + session.darkmode = true; + } else if (darkmode == "false" || darkmode == "0" || darkmode == 0 || darkmode == "off") { + session.darkmode = false; + } + } else if (urlParams.has("lightmode") || urlParams.has("lighttheme")) { + session.darkmode = false; + } else if (urlParams.has("whitemode") || urlParams.has("whitetheme")) { + document.body.classList.remove('darktheme'); + document.body.classList.add('whitetheme'); + session.darkmode = false; + } else if (session.studioSoftware) { + session.darkmode = false; // prevent OBS from defaulting to dark mode, avoiding possible overlooked bugs. + } else if (session.darkmode === null) { + session.darkmode = getComputedStyle(document.querySelector(":root")).getPropertyValue("--color-mode").trim(); + if (session.darkmode == "dark") { + session.darkmode = true; + } else { + session.darkmode = false; + } + } + + if (session.darkmode) { + document.body.classList.add("darktheme"); + //document.querySelector(':root').style.setProperty('--background-color',"#02050c" ); + } else { + document.body.classList.remove("darktheme"); + //document.querySelector(':root').style.setProperty('--background-color',"#141926" ); // already set as default. + } + } catch (e) { + errorlog(e); + console.warn("⚠️ If you are seeing this error, it's likely a third-party browser extension is breaking the site\n\nTry a different browser, incognito mode, or disable the problematic extension."); + } + + if (urlParams.has("videodevice") || urlParams.has("vdevice") || urlParams.has("vd") || urlParams.has("device") || urlParams.has("d") || urlParams.has("vdo")) { + session.videoDevice = urlParams.get("videodevice") || urlParams.get("vdevice") || urlParams.get("vd") || urlParams.get("device") || urlParams.get("d") || urlParams.get("vdo"); + + if (session.videoDevice === null) { + session.videoDevice = "1"; + } else if (session.videoDevice) { + session.videoDevice = normalizeDeviceLabel(session.videoDevice); + } + + if (session.videoDevice == "false") { + session.videoDevice = 0; + } else if (session.videoDevice == "0") { + session.videoDevice = 0; + } else if (session.videoDevice == "no") { + session.videoDevice = 0; + } else if (session.videoDevice == "off") { + session.videoDevice = 0; + } else if (session.videoDevice == "snapcam") { + session.videoDevice = "snap_camera"; + } else if (session.videoDevice == "canon") { + session.videoDevice = "eos"; + } else if (session.videoDevice == "camlink") { + session.videoDevice = "cam_link"; + } else if (session.videoDevice == "ndi") { + session.videoDevice = "newtek_ndi_video"; + } else if (session.videoDevice == "") { + session.videoDevice = 1; + } else if (session.videoDevice == "1") { + session.videoDevice = 1; + } else if (session.videoDevice == "default") { + session.videoDevice = 1; + } + + if (!urlParams.has("vdo")) { + getById("videoMenu").style.display = "none"; + getById("videoMenu").classList.add("hidden"); + // getById("videoMenu2").style.display = "none"; + // getById("videoMenu2").classList.add("hidden"); + // getById("videoMenu3").style.display = "none"; + // getById("videoMenu3").classList.add("hidden"); + } + log("session.videoDevice:" + session.videoDevice); + } + + // audioDevice + if (urlParams.has("audiodevice") || urlParams.has("adevice") || urlParams.has("ad") || urlParams.has("device") || urlParams.has("d") || urlParams.has("ado")) { + session.audioDevice = urlParams.get("audiodevice") || urlParams.get("adevice") || urlParams.get("ad") || urlParams.get("device") || urlParams.get("d") || urlParams.get("ado"); + + if (session.audioDevice === null) { + session.audioDevice = "1"; + } else if (session.audioDevice) { + session.audioDevice = normalizeDeviceLabel(session.audioDevice); + } + + if (session.audioDevice == "false") { + session.audioDevice = 0; + } else if (session.audioDevice == "0") { + session.audioDevice = 0; + } else if (session.audioDevice == "no") { + session.audioDevice = 0; + } else if (session.audioDevice == "off") { + session.audioDevice = 0; + } else if (session.audioDevice == "") { + session.audioDevice = 1; + } else if (session.audioDevice == "1") { + session.audioDevice = 1; + } else if (session.audioDevice == "default") { + session.audioDevice = 1; + } else if (session.audioDevice == "ndi") { + session.audioDevice = ["line_newtek_ndi_audio"]; + } else { + session.audioDevice = session.audioDevice.split(","); + } + + getById("headphonesDiv").classList.add("hidden"); + getById("headphonesDiv2").classList.add("hidden"); + + if (typeof session.audioDevice !== "object" && !urlParams.has("ado")) { + getById("audioMenu").style.display = "none"; + getById("audioScreenShare1").style.display = "none"; + getById("audioMenu").classList.add("hidden"); + getById("audioScreenShare1").classList.add("hidden"); + } + + if (session.audioDevice) { + // 0 or false, do not triger + log("requestAudioStream..()"); + try { + await requestAudioStream(); + } catch (e) { + errorlog(e); + } + } + + } + + if (session.videoDevice === 0) { + getById("previewWebcam").classList.add("miconly"); + if (session.audioDevice === 0) { + miniTranslate(getById("add_camera"), "click-start-to-join", "Click Start to Join"); + getById("container-2").className = "column columnfade hidden"; + getById("container-3").classList.add("skip-animation"); + getById("container-3").classList.remove("pointer"); + //delayedStartupFuncs.push([previewWebcam]); + session.webcamonly = true; + } else { + miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); + getById("container-3").classList.add("microphoneBackground"); + } + getById("container-3").title = getById("add_camera").innerText; + } + + if (session.mobile) { + getById("shareScreenGear").style.display = "none"; + getById("dropButton").style.display = "none"; + //getById("container-2").className = 'column columnfade hidden'; // Hide screen share on mobile + session.screensharebutton = false; + screensharesupport = false; + + if (session.audioDevice !== 0) { + getById("flipcamerabutton").classList.remove("hidden"); + } + } + + if (urlParams.has("androidfix")) { + session.AndroidFix = true; + } + + if (urlParams.has("consent")) { + session.consent = true; + getById("consentWarning").classList.remove("hidden"); + getById("consentWarning2").classList.remove("hidden"); + } + + if (urlParams.has("autojoin") || urlParams.has("autostart") || urlParams.has("aj") || urlParams.has("as")) { + session.autostart = true; + } + + if (urlParams.has("blackout") || urlParams.has("blackoutmode") || urlParams.has("bo") || urlParams.has("bom")) { + getById("blackoutmode").classList.remove("hidden"); + if (urlParams.get("blackout") || urlParams.get("blackoutmode") || urlParams.get("bo") || urlParams.get("bom")) { + blackoutMode(); + } + } + + if (session.dataMode) { + delayedStartupFuncs.push([joinDataMode]); + } else if (session.autostart) { + if (session.screenshare !== false) { + delayedStartupFuncs.push([publishScreen]); + } + if (session.consent) { + setTimeout(function () { + warnUser("⚠ Privacy warning: The director of this room can remotely switch your camera or microphone without permission.", 8000); + }, 1500); + } + } + + if (urlParams.has("noiframe") || urlParams.has("noiframes") || urlParams.has("nif") || urlParams.has("nowebsite")) { + session.noiframe = urlParams.get("noiframe") || urlParams.get("noiframes") || urlParams.get("nif") || urlParams.get("nowebsite"); + + if (!session.noiframe) { + session.noiframe = []; + } else { + session.noiframe = session.noiframe.split(","); + } + log("disable iframe playback"); + log(session.noiframe); + } + + if (urlParams.has("exclude") || urlParams.has("ex")) { + session.exclude = urlParams.get("exclude") || urlParams.get("ex"); + + if (!session.exclude) { + session.exclude = false; + } else { + session.exclude = session.exclude.split(","); + } + log("exclude audio/video playback"); + log(session.exclude); + } + + if (urlParams.has("noscreenshare") || urlParams.has("noscreenshares") || urlParams.has("noscreen") || urlParams.has("noscreens")) { + session.noScreenShare = true; + log("disable screen share playback"); + } + if (urlParams.has("screenp2p") || urlParams.has("noscreenwhep")) { + session.screenWhepPreference = "p2p"; + } + if (urlParams.has("screenwheponly")) { + session.screenWhepPreference = "whep"; + } + if (urlParams.has("screenwhep")) { + const pref = parseScreenToggleParam(urlParams.get("screenwhep")); + if (pref === false) { + session.screenWhepPreference = "p2p"; + } else if (pref === true) { + session.screenWhepPreference = "whep"; + } + } + function parseScreenToggleParam(value) { + if (value === null || value === undefined) { + return true; + } + const normalized = ("" + value).trim().toLowerCase(); + if (!normalized.length) { + return true; + } + if (["0", "false", "no", "off", "disable", "disabled", "none"].includes(normalized)) { + return false; + } + if (["1", "true", "yes", "on", "enable", "enabled"].includes(normalized)) { + return true; + } + return true; + } + if (urlParams.has("allowscreenvideo")) { + session.screenVideoOverride = parseScreenToggleParam(urlParams.get("allowscreenvideo")); + log("screen share video override: " + session.screenVideoOverride); + } + if (urlParams.has("allowscreenaudio")) { + session.screenAudioOverride = parseScreenToggleParam(urlParams.get("allowscreenaudio")); + log("screen share audio override: " + session.screenAudioOverride); + } + + if (urlParams.has("excludeaudio") || urlParams.has("exaudio") || urlParams.has("silence")) { + session.excludeaudio = urlParams.get("excludeaudio") || urlParams.get("exaudio") || urlParams.get("silence"); + + if (!session.excludeaudio) { + session.excludeaudio = false; + } else { + session.excludeaudio = session.excludeaudio.split(","); + } + log("exclude audio playback"); + log(session.excludeaudio); + } + + if (urlParams.has("novideo") || urlParams.has("nv") || urlParams.has("hidevideo") || urlParams.has("showonly")) { + session.novideo = urlParams.get("novideo") || urlParams.get("nv") || urlParams.get("hidevideo") || urlParams.get("showonly"); + + if (!session.novideo) { + session.novideo = []; + } else { + session.novideo = session.novideo.split(","); + } + log("disable video playback"); + log(session.novideo); + } + + if (urlParams.has("noaudio") || urlParams.has("na") || urlParams.has("hideaudio")) { + session.noaudio = urlParams.get("noaudio") || urlParams.get("na") || urlParams.get("hideaudio"); + + if (!session.noaudio) { + session.noaudio = []; + } else { + session.noaudio = session.noaudio.split(","); + } + log("disable audio playback"); + } + + + if (urlParams.has("nodirectoraudio")) { + session.nodirectoraudio = true; + log("disable audio playback from Directors"); + } + if (urlParams.has("nodirectorvideo")) { + session.nodirectoraudio = true; + log("disable audio playback from Directors"); + } + + if (urlParams.has("forceios")) { + log("allow iOS to work in video group chat; for this user at least"); + session.forceios = true; + } + + if (urlParams.has("nocursor") || urlParams.has("hidecursor") || urlParams.has("nomouse") || urlParams.has("hidemouse")) { + // on the screen, not in screen share + session.nocursor = true; + log("DISABLE CURSOR"); + var styletmp = document.createElement("style"); + styletmp.innerHTML = ` + video { + margin: 0; + padding: 0; + overflow: hidden; + cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=), none; + user-select: none; + } + `; + document.head.appendChild(styletmp); + } + + if (urlParams.has("cursor") || urlParams.has("screensharecursor")) { + session.screensharecursor = true; + } + + if (urlParams.has("distort")) { + session.voicechanger = 1; + } + + if (urlParams.has("dtx") || urlParams.has("usedtx")) { + session.dtx = true; + session.cbr = 0; // no point dtx on if cbr is on, right? + } + + if (urlParams.has("youtube")) { + // Set with a Youtube v3 clientID + "," + API key, then run YoutubeChatInterface(); + session.youtubeKey = urlParams.get("youtube") || ""; + //YoutubeChatInterface(); // queries Youtube for chat messages. Forwards them to the parent IFRAME only at the moment. + } + + if (urlParams.has("vbr")) { + session.cbr = 0; + getById("whipoutvbrcbr").classList.add("hidden"); + } else if (urlParams.has("cbr")) { + session.cbr = 1; + getById("whipoutvbrcbr").classList.add("hidden"); + } + + if (urlParams.has("order")) { + session.order = parseInt(urlParams.get("order")) || 1; + } + + if (urlParams.has("orderby")) { + session.orderby = urlParams.get("orderby") || "id"; // "label" also an option; the default is stream ID tho. + } + + if (urlParams.has("slotmode") || urlParams.has("slotsmode")) { + session.slotmode = parseInt(urlParams.get("slotmode")) || parseInt(urlParams.get("slotsmode")) || 1; + } + + if (urlParams.has("slot")) { + var slotValue = parseInt(urlParams.get("slot")); + session.slot = isNaN(slotValue) ? false : slotValue; // 0 = exclude from slots, N = prefer slot N, false = auto-assign + } + + if (urlParams.has("slots")) { + session.slots = parseInt(urlParams.get("slots")) || 4; // first N slots can be filled + } else if (urlParams.has('slotslist')) { // select which slots you want to be processed + session.slotsList = urlParams.get('slotslist').split(',').map(slot => parseInt(slot.trim())).filter(slot => !isNaN(slot)); + if (!session.slotsList.length){ + session.slotsList = false; + } + } + + if (urlParams.has("maxslots")) { + // hard coded default is 12; if &maxslots used, it changes to 20 unless value passed. + session.maxAvailableSlots = parseInt(urlParams.get("maxslots")) || session.maxAvailableSlots; + } + + if (session.slotmode){ + populateSlotPicker(); + } + + if (urlParams.has("alpha")) { + session.alpha = true; + } + + if (urlParams.has("chunked") || urlParams.has("chunk")) { + session.chunked = parseInt(urlParams.get("chunked")) || parseInt(urlParams.get("chunk")) || 2500; // sender side; enables to allows. + // session.alpha = true; + if (Firefox || SafariVersion) { + if (!session.cleanOutput) { + warnUser("Only Chromium-based browsers support chunked mode.\n\nPlease switch to Chrome or another compatible browser to use &chunked mode."); + } + session.chunked = false; + console.warn("Disabling chunked mode since not using a compatible browser."); + } + } + if (urlParams.has("chunkedbuffer") || urlParams.has("sendingbuffer")) { + session.sendingBuffer = parseInt(urlParams.get("chunkedbuffer")) || parseInt(urlParams.get("sendingbuffer")) || 5000; // sender side; enables to allows. + } + + const chunkProfiles = { + mobile: { + chunkfec: 3, + chunknack: true, + chunkbuffer: 900, + chunkbufferfloor: 600, + chunkbufferceil: 1600, + chunkjitterslack: 250, + chunkadapt: "framerate", + chunkadaptfloor: 320, + chunkadaptceil: 1400, + chunkadaptthreshold: 260, + chunkadaptmaxdrop: 10, + chunkadaptinterval: 1200 + }, + balanced: { + chunkfec: 4, + chunknack: true, + chunkbuffer: 750, + chunkbufferfloor: 450, + chunkbufferceil: 1400, + chunkjitterslack: 220, + chunkadapt: "hybrid", + chunkadaptfloor: 420, + chunkadaptceil: 2600, + chunkadaptthreshold: 340, + chunkadaptmaxdrop: 6, + chunkadaptinterval: 900 + }, + desktop: { + chunkfec: 5, + chunknack: true, + chunkbuffer: 620, + chunkbufferfloor: 400, + chunkbufferceil: 1100, + chunkjitterslack: 180, + chunkadapt: "bitrate", + chunkadaptfloor: 580, + chunkadaptceil: 4200, + chunkadaptthreshold: 360, + chunkadaptmaxdrop: 4, + chunkadaptinterval: 700 + } + }; + + function applyChunkPreset(preset) { + if (!preset) { + return; + } + Object.keys(preset).forEach(key => { + const value = preset[key]; + const sessionKey = key; + session[sessionKey] = value; + }); + } + + if (urlParams.has("chunkprofile")) { + const profileName = (urlParams.get("chunkprofile") || "").toLowerCase(); + if (chunkProfiles[profileName]) { + session.chunkprofile = profileName; + applyChunkPreset(chunkProfiles[profileName]); + } + } + + function parseIntegerParam(name, target, clamp = null) { + if (!urlParams.has(name)) { + return; + } + let value = parseInt(urlParams.get(name)); + if (!Number.isFinite(value)) { + return; + } + if (clamp && Array.isArray(clamp)) { + const [min, max] = clamp; + if (typeof min === "number") { + value = Math.max(min, value); + } + if (typeof max === "number") { + value = Math.min(max, value); + } + } + session[target] = value; + } + + function parseBooleanParam(name, target) { + if (!urlParams.has(name)) { + return; + } + const raw = urlParams.get(name); + if (raw === null || raw === "" || raw === "1" || raw.toLowerCase() === "true") { + session[target] = true; + } else if (raw.toLowerCase() === "0" || raw.toLowerCase() === "false") { + session[target] = false; + } else { + session[target] = true; + } + } + + parseIntegerParam("chunkfec", "chunkfec", [0, 12]); + parseBooleanParam("chunknack", "chunknack"); + parseIntegerParam("chunkbuffer", "chunkbuffer", [0, 30000]); + parseIntegerParam("chunkbufferfloor", "chunkbufferfloor", [0, 30000]); + parseIntegerParam("chunkbufferceil", "chunkbufferceil", [0, 60000]); + parseIntegerParam("chunkjitterslack", "chunkjitterslack", [0, 10000]); + + if (urlParams.has("chunkadapt")) { + const adaptMode = (urlParams.get("chunkadapt") || "").toLowerCase(); + if (["bitrate", "framerate", "hybrid"].includes(adaptMode)) { + session.chunkadapt = adaptMode; + } + } + parseIntegerParam("chunkadaptfloor", "chunkadaptfloor", [0, 10000]); + parseIntegerParam("chunkadaptceil", "chunkadaptceil", [0, 100000]); + parseIntegerParam("chunkadaptthreshold", "chunkadaptthreshold", [0, 10000]); + parseIntegerParam("chunkadaptmaxdrop", "chunkadaptmaxdrop", [0, 120]); + parseIntegerParam("chunkadaptinterval", "chunkadaptinterval", [100, 60000]); + + parseIntegerParam("chunkretry", "chunkretry", [0, 60000]); + parseIntegerParam("chunkcache", "chunkcache", [0, 60000]); + parseIntegerParam("chunkchunksize", "chunkchunksize", [2048, 65536]); + + if (urlParams.has("nochunk") || urlParams.has("nochunked")) { + // viewer side + session.nochunk = true; + } + + if (urlParams.has("nochunkaudio") || urlParams.has("nochunkedaudio")) { + // viewer side + session.nochunkaudio = true; + } + + if (urlParams.has("audiobuffer") || urlParams.has("bufferaudio")) { + // viewer side + session.audioBuffer = urlParams.get("audiobuffer") || urlParams.get("bufferaudio") || 0; + session.audioBuffer = parseInt(session.audioBuffer); + } + + //if (urlParams.has('viewchunked') || urlParams.has('viewchunk') || urlParams.has('allowchunked') || urlParams.has('allowchunk')) { // viewer side + // session.forceChunked = true; + //} + + if (urlParams.has("token")) { + session.token = urlParams.get("token") || false; + // checkToken(); // this is sycnhonous + } + + if (urlParams.has("maindirectorpassword") || urlParams.has("maindirpass")) { + session.mainDirectorPassword = urlParams.get("maindirectorpassword") || urlParams.get("maindirpass") || false; + if (!session.mainDirectorPassword) { + window.focus(); + session.mainDirectorPassword = await promptAlt(getTranslation("director-password"), true, true); + if (session.mainDirectorPassword) { + session.mainDirectorPassword = session.mainDirectorPassword.trim(); + try { + session.mainDirectorPassword = decodeURIComponent(session.mainDirectorPassword); + } catch (e) { + errorlog(e); + } + } + } + // registerToken(); + } + + if (urlParams.has("debug")) { + DebugLog = true; + if (!errorReport) { + errorReport = []; + } + if (urlParams.get("debug") == "1") { + debugStart(); + } else if (urlParams.get("debug")) { + debugStart(urlParams.get("debug")); + } + } + + if (urlParams.has("group") || urlParams.has("groups")) { + session.group = urlParams.get("group") || urlParams.get("groups") || ""; + session.group = session.group.split(","); + } + + if (urlParams.has("groupview") || urlParams.has("viewgroup") || urlParams.has("gv")) { + session.groupView = urlParams.get("groupview") || urlParams.get("viewgroup") || urlParams.get("gv") || ""; + session.groupView = session.groupView.split(","); + } + + if (urlParams.has("groupaudio") || urlParams.has("ga")) { + session.groupAudio = true; + } + + if (urlParams.has("groupmode") || urlParams.has("gm")) { + session.allowNoGroup = true; + } + + if (urlParams.has("host")) { + session.roomhost = true; + } + + if (urlParams.has("sensors") || urlParams.has("sensor") || urlParams.has("gyro") || urlParams.has("gyros") || urlParams.has("accelerometer")) { + session.sensorData = urlParams.get("sensors") || urlParams.get("sensor") || urlParams.get("gyro") || urlParams.get("gyros") || urlParams.get("accelerometer") || 30; + session.sensorData = parseInt(session.sensorData); + } + if (urlParams.has("sensorfilter") || urlParams.has("sensorsfilter") || urlParams.has("filtersensor") || urlParams.has("filtersensors")) { + session.sensorDataFilter = urlParams.get("sensorfilter") || urlParams.get("sensorsfilter") || urlParams.get("filtersensor") || urlParams.get("filtersensors") || ""; + session.sensorDataFilter = session.sensorDataFilter.split(","); // ["pos","lin","ori","mag","gyro","acc"]; + } + if (urlParams.has("webxrbridge") || urlParams.has("externalsensors") || urlParams.has("sensorsbridge")) { + session.externalSensorBridge = true; + session.externalSensorOrigin = urlParams.get("sensorsorigin") || ""; + } + + if (urlParams.has("ptime")) { + session.ptime = parseInt(urlParams.get("ptime")) || 20; + if (session.ptime < 10) { + session.ptime = 10; + } + } + + if (urlParams.has("minptime")) { + session.minptime = parseInt(urlParams.get("minptime")) || 10; + if (session.minptime < 10) { + session.minptime = 10; + } + if (session.minptime > 300) { + session.minptime = 300; + } + } + + if (urlParams.has("maxptime")) { + session.maxptime = parseInt(urlParams.get("maxptime")) || 60; + if (session.maxptime < 10) { + session.maxptime = 10; + } + if (session.maxptime > 300) { + session.maxptime = 300; + } + } + + if (urlParams.has("contenthint") || urlParams.has("contenttype") || urlParams.has("content") || urlParams.has("hint")) { + session.contentHint = urlParams.get("contenthint") || urlParams.get("contenttype") || urlParams.get("content") || urlParams.get("hint") || "detail"; + } + + if (urlParams.has("audiocontenthint") || urlParams.has("audiocontenttype") || urlParams.has("audiocontent") || urlParams.has("audiohint")) { + session.audioContentHint = urlParams.get("audiocontenthint") || urlParams.get("audiocontenttype") || urlParams.get("audiocontent") || urlParams.get("audiohint") || "music"; + } + + if (urlParams.has("screensharecontenthint") || urlParams.has("sscontenthint") || urlParams.has("screensharecontenttype") || urlParams.has("sscontent") || urlParams.has("sshint")) { + session.screenshareContentHint = urlParams.get("screensharecontenthint") || urlParams.get("sscontenthint") || urlParams.get("screensharecontenttype") || urlParams.get("sscontent") || urlParams.get("sshint") || "detail"; + } + + if (urlParams.has("vred")) { + session.videoErrorCorrection = true; + } + if (urlParams.has("pvred")) { + session.preferredVideoErrorCorrection = true; + } + + if (urlParams.has("codec") || urlParams.has("codecs") || urlParams.has("videocodec")) { + log("codecs CHANGED"); + session.codecs = urlParams.get("codec") || urlParams.get("codecs") || urlParams.get("videocodec") || false; + if (session.codecs) { + session.codecs = session.codecs.toLowerCase(); + session.codecs = session.codecs.split(","); + if (session.codecs.length) { + session.codec = session.codecs.shift(); + if (!session.codec) { + session.codec = false; + session.codecs = false; + } + if (!session.codecs.length) { + session.codecs = false; + } + } else { + session.codecs = false; + } + } + } else if (OperaGx) { + session.codec = "vp8"; + warnlog("Defaulting to VP8 manually, as H264 with remote iOS devices is not supported"); + } + + if (urlParams.has("redaudio")) { + // just for experimenting + session.redAudio = true; + } + if (urlParams.has("fecaudio")) { + // + session.fecAudio = true; + } + if (urlParams.has("predaudio")) { + // + session.predAudio = true; + } + if (urlParams.has("pfecaudio")) { + // + session.pfecAudio = true; + } + + if (urlParams.has("audiocodec")) { + log("CODEC CHANGED"); + session.audioCodec = urlParams.get("audiocodec") || false; + if (session.audioCodec) { + session.audioCodec = session.audioCodec.toLowerCase(); + } + } + if (session.audioCodec && session.audioCodec == "red") { + session.audiobitratePRO = 216; // higher than this seems to break the RED mode. default 256. + } + + if (urlParams.has("preferaudiocodec")) { + log("PREFER CODEC CHANGED"); + session.preferAudioCodec = urlParams.get("preferaudiocodec") || false; + if (session.preferAudioCodec) { + session.preferAudioCodec = session.preferAudioCodec.toLowerCase(); + } + } + + if (urlParams.has("prefervideocodec")) { + log("PREFER VIDEO CODEC CHANGED"); + session.preferVideoCodec = urlParams.get("prefervideocodec") || false; + if (session.preferVideoCodec) { + session.preferVideoCodec = session.preferVideoCodec.toLowerCase(); + } + } + + if (urlParams.has("scenelinkcodec")) { + // this is mainly for a niche iframe API use + log("codecGroupFlag CHANGED"); + session.codecGroupFlag = urlParams.get("scenelinkcodec") || false; + if (session.codecGroupFlag) { + session.codecGroupFlag = "&codec=" + session.codecGroupFlag.toLowerCase(); + } + } + if (session.codecGroupFlag) { + getById("codecGroupFlag").disabled = true; + } + if (urlParams.has("scenelinkbitrate")) { + // this is mainly for a niche iframe API use + log("bitrateGroupFlag CHANGED"); + session.bitrateGroupFlag = urlParams.get("scenelinkbitrate") || false; + if (session.bitrateGroupFlag) { + session.bitrateGroupFlag = "&totalbitrate=" + parseInt(session.bitrateGroupFlag); + } + } + + if (urlParams.has("h264profile")) { + session.h264profile = urlParams.get("h264profile") || "42e01f"; // 42001f + session.h264profile = session.h264profile.substring(0, 6); + session.h264profile = session.h264profile.toLowerCase(); + if (session.h264profile == "0") { + session.h264profile = false; + } else if (session.h264profile == "off") { + session.h264profile = false; + } else if (session.h264profile == "disabl") { + session.h264profile = false; + } else if (session.h264profile == "defaul") { + session.h264profile = false; + } else if (session.h264profile == "false") { + session.h264profile = false; + } + } else if (session.codec === "hardware" && Android) { + // same as &h264profile, but easier for me to remember. I'll try to automate this in the future. + session.codec = "h264"; + session.h264profile = "42e01f"; + } + + if (urlParams.has("nofec")) { + // disables error control / throttling -- currently on audio + session.noFEC = true; + } + if (urlParams.has("nonacks") || urlParams.has("nonack")) { + // disables error control / throttling. + session.noNacks = true; + } + if (urlParams.has("nopli")) { + // disables error control / throttling. + session.noPLIs = true; + } + if (urlParams.has("noremb")) { + // disables Receiver Estimated Maximum Bitrate (throttling) + session.noREMB = true; + } + + if (urlParams.has("scale")) { + if (urlParams.get("scale") == "false") { + } else if (urlParams.get("scale") == "0") { + } else if (urlParams.get("scale") == "no") { + } else if (urlParams.get("scale") == "off") { + } else { + log("Resolution scale requested"); + session.scale = parseFloat(urlParams.get("scale")) || 100; + } + session.dynamicScale = false; // default true + } else { + if (urlParams.has("viewwidth") || urlParams.has("vw")) { + session.viewwidth = urlParams.get("viewwidth") || urlParams.get("vw") || false; + if (session.viewwidth) { + session.viewwidth = parseInt(session.viewwidth); + } + session.dynamicScale = false; // default true + } + if (urlParams.has("viewheight") || urlParams.has("vh")) { + session.viewheight = urlParams.get("viewheight") || urlParams.get("vh") || false; + session.dynamicScale = false; // default true + if (session.viewheight) { + session.viewheight = parseInt(session.viewheight); + } + } + } + + if (urlParams.has("sharperscreen")) { + // sets scale to 100 for inbound screenshares only + session.sharperScreen = true; + } + + if (urlParams.has("mcscale") || urlParams.has("meshcastscale") || urlParams.has("woscale") || urlParams.has("whipoutscale")) { + session.whipOutScale = parseFloat(urlParams.get("mcscale")) || parseFloat(urlParams.get("meshcastscale")) || parseFloat(urlParams.get("woscale")) || parseFloat(urlParams.get("whipoutscale")) || 100; + } + + if (isIFrame) { + getById("helpbutton").style.display = "none"; + getById("helpbutton").style.opacity = 0; + getById("reportbutton").style.display = "none"; + getById("reportbutton").style.opacity = 0; + getById("calendarButton").style.display = "none"; + getById("calendarButton").style.opacity = 0; + getById("chatBody").innerHTML = ""; + } + + if (urlParams.has("poke")){ + session.poke = urlParams.get("poke").replace(/[\W]+/g, "_").replace(/_+/g, "_") || true; + } + + if (urlParams.has("beep") || urlParams.has("notify") || urlParams.has("tone")) { + let beepValue = urlParams.get("beep") || urlParams.get("notify") || urlParams.get("tone") || ""; + let beepTypes = []; + + if (beepValue) { + beepTypes = beepValue + .split(",") + .map(type => type.trim().toLowerCase()) + .filter(type => type !== ""); + session.beepToNotify = beepTypes.length ? beepTypes : true; + } else { + beepTypes = []; + session.beepToNotify = true; // enable all, since nothing was specified + } + + if (beepTypes.length === 0 || beepTypes.includes("knock")) { + // Allow callers to request the louder knock tone without extra flags + session.knockToneEnabled = true; + } + + if (beepTypes.length === 0 || beepTypes.includes("join")) { + const addtone = createAudioElement(); + addtone.id = "jointone"; + addtone.src = "./media/join.mp3"; + getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling); + } + + if (beepTypes.length === 0 || beepTypes.includes("leave")) { + const addtone = createAudioElement(); + addtone.id = "leavetone"; + addtone.src = "./media/leave.mp3"; + getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling); + } + + if (!Notification) { + warnlog("Desktop notifications are not available in your browser."); + } else if (Notification.permission !== "granted") { + Notification.requestPermission(); + } + } + + if (urlParams.has("r2d2")) { + /* var addtone = createAudioElement(); + addtone.id = "jointone"; + addtone.src = "./media/join.mp3"; + getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling) + var addtone = createAudioElement(); + addtone.id = "leavetone"; + addtone.src = "./media/leave.mp3"; + getById("testtone").parentNode.insertBefore(addtone, getById("testtone").nextSibling) */ + getById("testtone").innerHTML = ""; + getById("testtone").src = "./media/robot.mp3"; + session.beepToNotify = true; + } + + if (urlParams.get("custombeep")) { + updateAudioSource(urlParams.get("custombeep"), "testtone"); + } + if (urlParams.get("customleave")) { + updateAudioSource(urlParams.get("customleave"), "leavetone"); + } + if (urlParams.get("customjoin")) { + updateAudioSource(urlParams.get("customjoin"), "jointone"); + } + + if (urlParams.has("beepvolume")) { + const volume = parseInt(urlParams.get("beepvolume")) / 100 || 0; + ["testtone", "jointone", "leavetone"].forEach(id => { + const audio = document.getElementById(id); + if (!audio) return; + + try { + if (volume > 1 && session.audioCtx) { + audio.volume = 1; // Set base volume to 100% + audio.crossOrigin = "anonymous"; // Try to enable CORS + const source = session.audioCtx.createMediaElementSource(audio); + const gainNode = session.audioCtx.createGain(); + gainNode.gain.value = volume; + source.connect(gainNode); + gainNode.connect(session.audioCtx.destination); + audio.gainNode = gainNode; + console.warn("note: If the audio file is protected by CORS, increasing the volume will cause it to fail"); + } else { + audio.volume = volume; + } + } catch (e) { + console.warn("Volume boost failed, falling back to normal volume", e); + audio.volume = Math.min(1, volume); // Fallback to normal volume, capped at 100% + } + }); + } + + if (urlParams.has("easyexit") || urlParams.has("ee")) { + session.noExitPrompt = true; + } + + if (urlParams.has("entrymsg") || urlParams.has("welcome") || urlParams.has("welcomeb64")) { + session.welcomeMessage = urlParams.get("entrymsg") || urlParams.get("welcome") || urlParams.get("welcomeb64"); + + if (urlParams.get("welcomeb64")) { + try { + session.welcomeMessage = atob(session.welcomeMessage); + } catch (e) {} + } + try { + session.welcomeMessage = session.welcomeMessage.replace(/(\r\n|\n|\r)/gm, " "); + session.welcomeMessage = decodeURIComponent(session.welcomeMessage); + } catch (e) {} + } + + if (urlParams.has("welcomehtml")) { + session.welcomeHTML = urlParams.get("welcomehtml"); + + try { + session.welcomeHTML = atob(session.welcomeHTML); + } catch (e) {} + try { + session.welcomeHTML = session.welcomeHTML.replace(/(\r\n|\n|\r)/gm, " "); + session.welcomeHTML = decodeURIComponent(session.welcomeHTML); + } catch (e) {} + } + + if (urlParams.has("welcomeimage") || urlParams.has("welcomeimg")) { + session.welcomeImage = urlParams.get("welcomeimage") || urlParams.get("welcomeimg"); + try { + session.welcomeImage = decodeURIComponent(session.welcomeImage); + } catch (e) {} + } + + if (urlParams.has("mixminus") || urlParams.has("mm")) { + session.mixMinus = true; + // Director/co-director mix-minus: director mixes audio and sends custom mix to each guest + if (session.director || session.codirector) { + session.directorMixMinus = true; + session.mixMinusState = {}; // Per-guest mix-minus state + session.mixMinusDefaults = { + allGuestsEnabled: true, // Default state for new guests + includeDirectorAudio: true, // Include director's audio by default + includeAllGuests: true // Include all other guests by default + }; + } + } + + if (urlParams.has("clearstorage") || urlParams.has("clear")) { + clearStorage(); + } + + if (urlParams.has("videobitrate") || urlParams.has("bitrate") || urlParams.has("vb")) { + session.bitrate = urlParams.get("videobitrate") || urlParams.get("bitrate") || urlParams.get("vb") || 8000; + if (session.bitrate) { + if (session.view_set && session.bitrate.split(",").length > 1) { + session.bitrate_set = session.bitrate.split(","); + session.bitrate = parseInt(session.bitrate_set[0]); + } else { + session.bitrate = parseInt(session.bitrate); + } + if (session.bitrate < 1) { + session.bitrate = false; + } + log("BITRATE ENABLED"); + log(session.bitrate); + } + } + + if (urlParams.has("maxvideobitrate") || urlParams.has("maxbitrate") || urlParams.has("maxvb") || urlParams.has("mvb")) { + session.maxvideobitrate = urlParams.get("maxvideobitrate") || urlParams.get("maxbitrate") || urlParams.get("maxvb") || urlParams.get("mvb"); + session.maxvideobitrate = parseInt(session.maxvideobitrate); + + if (session.maxvideobitrate < 1) { + session.maxvideobitrate = false; + } + log("maxvideobitrate ENABLED"); + log(session.maxvideobitrate); + } + + if (urlParams.has("totalroombitrate") || urlParams.has("totalroomvideobitrate") || urlParams.has("trb") || urlParams.has("totalbitrate") || urlParams.has("tb")) { + session.totalRoomBitrate = urlParams.get("totalroombitrate") || urlParams.get("totalroomvideobitrate") || urlParams.get("trb") || urlParams.get("totalbitrate") || urlParams.get("tb") || ""; + + if (session.totalRoomBitrate.split(",").length > 1) { + if (session.mobile) { + session.totalRoomBitrate = session.totalRoomBitrate.split(",")[1]; + } else { + session.totalRoomBitrate = session.totalRoomBitrate.split(",")[0]; + } + } + + if ((session.totalRoomBitrate == "false") || (session.totalRoomBitrate == "off")){ + session.totalRoomBitrate = 0; + } + + session.totalRoomBitrate = parseInt(session.totalRoomBitrate) || 0; + + if (session.totalRoomBitrate < 1) { + session.totalRoomBitrate = 0; + } + log("totalRoomBitrate ENABLED"); + log(session.totalRoomBitrate); + } + + if (session.totalRoomBitrate === false) { + session.totalRoomBitrate = session.bitrate || session.totalRoomBitrate_default; // sneaky sneaky + } else { + session.totalRoomBitrate_default = session.totalRoomBitrate; // trb_default doesn't change dynamically, but trb can (per director I guess) + } + + if (session.totalRoomBitrate_default > 4000) { + getById("trbSettingInput").max = Math.ceil(session.totalRoomBitrate_default); + } + + if (urlParams.has("maxtotalscenebitrate") || urlParams.has("totalscenebitrate") || urlParams.has("mtsb") || urlParams.has("tsb") || urlParams.has("totalbitrate") || urlParams.has("tb")) { + session.totalSceneBitrate = urlParams.get("maxtotalscenebitrate") || urlParams.get("totalscenebitrate") || urlParams.get("mtsb") || urlParams.get("tsb") || urlParams.get("totalbitrate") || urlParams.get("tb") || false; + if (session.totalSceneBitrate) { + session.totalSceneBitrate = parseInt(session.totalSceneBitrate); + } + } + + if (urlParams.has("blur")) { + session.blurBackground = urlParams.get("blur") || 10; + session.blurBackground = parseInt(session.blurBackground) || 10; + if (session.blurBackground < 0) { + session.blurBackground = false; + } + session.structure = true; + } + + if (urlParams.has("limittotalbitrate") || urlParams.has("ltb")) { + session.limitTotalBitrate = urlParams.get("limittotalbitrate") || urlParams.get("ltb") || "2500"; + + if (session.limitTotalBitrate.split(",").length > 1) { + if (session.mobile) { + session.limitTotalBitrate = session.limitTotalBitrate.split(",")[1]; + } else { + session.limitTotalBitrate = session.limitTotalBitrate.split(",")[0]; + } + } + session.limitTotalBitrate = parseInt(session.limitTotalBitrate); + getById("limittotalbitrate_director").classList.remove("hidden"); + } + + if (session.limitTotalBitrate) { + if (session.limitTotalBitrate > session.limitTotalBitrate_defaultMax) { + getById("ltbSettingInputManual").max = Math.ceil(session.limitTotalBitrate); + } + getById("ltbSettingInputManual").value = session.limitTotalBitrate; + getById("ltbSettingInput").value = session.limitTotalBitrate; + getById("ltbSettingInputFeedback").innerHTML = session.limitTotalBitrate || "Disabled"; + } + + if (urlParams.has("mcscreensharebitrate") || urlParams.has("mcssbitrate") || urlParams.has("whipoutscreensharebitrate") || urlParams.has("wossbitrate")) { + session.whipOutScreenShareBitrate = urlParams.get("mcscreensharebitrate") || urlParams.get("mcssbitrate") || urlParams.get("whipoutscreensharebitrate") || urlParams.get("wossbitrate") || 2500; + session.whipOutScreenShareBitrate = parseInt(session.whipOutScreenShareBitrate); + } + + if (urlParams.has("mcscreensharecodec") || urlParams.has("mcsscodec") || urlParams.has("whipoutscreensharecodec") || urlParams.has("wosscodec")) { + session.whipOutScreenShareCodec = urlParams.get("mcscreensharecodec") || urlParams.get("mcsscodec") || urlParams.get("whipoutscreensharecodec") || urlParams.get("wosscodec") || false; + } + if (session.whipOutScreenShareCodec) { + session.whipOutScreenShareCodec = session.whipOutScreenShareCodec.toLowerCase(); + } + + if (urlParams.has("mccodec") || urlParams.has("meshcastcodec") || urlParams.has("whipoutcodec") || urlParams.has("whipoutvideocodec") || urlParams.has("woc") || urlParams.has("wovc")) { + session.whipOutCodec = urlParams.get("mccodec") || urlParams.get("meshcastcodec") || urlParams.get("whipoutcodec") || urlParams.get("whipoutvideocodec") || urlParams.get("woc") || urlParams.get("wovc") || false; + getById("whipoutcodecGroupFlag").classList.add("hidden"); + } + + if (session.whipOutCodec) { + session.whipOutCodec = session.whipOutCodec.toLowerCase(); + if (session.whipOutCodec == "h264") { + if (Firefox) { + session.whipOutCodec = false; + } + } + if (session.whipOutCodec) { + session.whipOutCodec = session.whipOutCodec.split(","); + } + getById("whipoutcodecGroupFlag").classList.add("hidden"); + } + + if (urlParams.has("whipoutaudiocodec") || urlParams.has("woac")) { + session.whipOutAudioCodec = urlParams.get("whipoutaudiocodec") || urlParams.get("woac") || false; + // getById("whipoutaudiocodecGroupFlag").classList.add("hidden"); // havne't added this in yet as an actual html element + } + + if (urlParams.has("mcab") || urlParams.has("mcaudiobitrate") || urlParams.has("meshcastab") || urlParams.has("meshcastaudiobitrate ") || urlParams.has("whipoutaudiobitrate") || urlParams.has("woab")) { + session.whipOutAudioBitrate = urlParams.get("mcab") || urlParams.get("mcaudiobitrate") || urlParams.get("meshcastab") || urlParams.get("meshcastaudiobitrate ") || urlParams.get("whipoutaudiobitrate") || urlParams.get("woab") || false; + if (session.whipOutAudioBitrate) { + session.whipOutAudioBitrate = parseInt(session.whipOutAudioBitrate); + } + getById("whipoutaudiobitrate").classList.add("hidden"); + } + + if (urlParams.has("mcb") || urlParams.has("mcbitrate") || urlParams.has("meshcastbitrate") || urlParams.has("whipoutvideobitrate") || urlParams.has("wovb")) { + session.whipOutVideoBitrate = urlParams.get("mcb") || urlParams.get("mcbitrate") || urlParams.get("meshcastbitrate") || urlParams.get("whipoutvideobitrate") || urlParams.get("wovb") || false; + if (session.whipOutVideoBitrate) { + session.whipOutVideoBitrate = parseInt(session.whipOutVideoBitrate); + } + getById("whipoutbitrateGroupFlag").classList.add("hidden"); + getById("whipoutvbrcbr").classList.add("hidden"); + } + + if (urlParams.has("height") || urlParams.has("h")) { + session.height = urlParams.get("height") || urlParams.get("h"); + session.height = parseInt(session.height); + } + + if (urlParams.has("width") || urlParams.has("w")) { + session.width = urlParams.get("width") || urlParams.get("w"); + session.width = parseInt(session.width); + } + + if (urlParams.has("quality") || urlParams.has("q")) { + try { + session.quality = urlParams.get("quality") || urlParams.get("q") || "0"; + if (session.quality.toLowerCase() == "4k") { + session.quality = -2; + } else if (session.quality.toLowerCase() == "2160p") { + session.quality = -2; + } else if (session.quality.toLowerCase() == "2160") { + session.quality = -2; + } else if (session.quality.toLowerCase() == "2k") { + session.quality = -3; + } else if (session.quality.toLowerCase() == "1440p") { + session.quality = -3; + } else if (session.quality.toLowerCase() == "1440") { + session.quality = -3; + } else if (session.quality.toLowerCase() == "hd") { + // + session.quality = 1; + } else if (session.quality.toLowerCase() == "720p") { + // + session.quality = 1; + } else if (session.quality.toLowerCase() == "720") { + // + session.quality = 1; + } else if (session.quality.toLowerCase() == "fullhd") { + session.quality = 0; + } else if (session.quality.toLowerCase() == "1080p") { + session.quality = 0; + } else if (session.quality.toLowerCase() == "1080") { + session.quality = 0; + } else if (session.quality.toLowerCase() == "high") { + session.quality = 0; + } else if (session.quality.toLowerCase() == "360p") { + session.quality = 2; + } else if (session.quality.toLowerCase() == "360") { + session.quality = 2; + } else if (session.quality.toLowerCase() == "low") { + session.quality = 2; + } + session.quality = parseInt(session.quality); + getById("gear_screen").parentNode.removeChild(getById("gear_screen")); + getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); + + } catch (e) { + errorlog(e); + } + } else if (urlParams.has("fullhd") || urlParams.has("1080p")) { + session.quality = 0; + getById("gear_screen").parentNode.removeChild(getById("gear_screen")); + getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); + + } else if (urlParams.has("4k")) { + session.quality = -2; + getById("gear_screen").parentNode.removeChild(getById("gear_screen")); + getById("gear_webcam").parentNode.removeChild(getById("gear_webcam")); + + } + + if (urlParams.has("sink")) { + session.sink = urlParams.get("sink"); + } else if (urlParams.has("outputdevice") || urlParams.has("od") || urlParams.has("audiooutput")) { + session.outputDevice = urlParams.get("outputdevice") || urlParams.get("od") || urlParams.get("audiooutput") || null; + + if (session.outputDevice) { + session.outputDevice = normalizeDeviceLabel(session.outputDevice); + } else { + session.outputDevice = null; + getById("headphonesDiv3").style.display = "none"; // + } + + if (session.outputDevice) { + try { + enumerateDevices().then(function (deviceInfos) { + for (let i = 0; i !== deviceInfos.length; ++i) { + if (deviceInfos[i].kind === "audiooutput") { + if (normalizeDeviceLabel(deviceInfos[i].label).includes(session.outputDevice)) { + session.sink = deviceInfos[i].deviceId; + log("AUDIO OUT DEVICE: " + deviceInfos[i].deviceId); + break; + } + } + } + }); + } catch (e) {} + } + + getById("headphonesDiv").classList.add("hidden"); + getById("headphonesDiv2").classList.add("hidden"); + } else if (session.sink) { + if (session.sink == "default") { + session.sink = false; + } else { + enumerateDevices().then(function (deviceInfos) { + var matched = false; + for (let i = 0; i !== deviceInfos.length; ++i) { + if (deviceInfos[i].kind === "audiooutput") { + if (deviceInfos[i].deviceId == session.sink) { + matched = true; + break; + } + } + } + if (!matched) { + session.sink = false; // make sure any saved output device exists. + } + }); + } + } + + if (session.studioSoftware || navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) { + session.fullscreen = true; + } else if (urlParams.has("fullscreen")) { + session.fullscreen = true; + } + + if (urlParams.has("stats")) { + if (urlParams.get("stats") == "0") { + session.statsMenu = false; + } else if (urlParams.get("stats") == "false") { + session.statsMenu = false; + } else if (urlParams.get("stats") == "off") { + session.statsMenu = false; + } else { + session.statsMenu = true; + } + } else if (urlParams.has("nostats")) { + session.statsMenu = false; + } + + if (session.statsMenu === false) { + // hide menu option + try { + document.queryselector('[data-action="ShowStats"]').parentNode.classList.add("hidden"); + } catch (e) {} + } + + if (urlParams.has("statsinterval")) { + session.statsInterval = parseInt(urlParams.get("statsinterval")) || 3000; // milliseconds. interval of requesting stats of remote guests + } + + if (urlParams.has("cleandirector") || urlParams.has("cdv")) { + session.cleanDirector = true; + } + + if (urlParams.has("hidetranslate")) { + getById("translateButton").style.display = "none"; + } + + if (session.cleanOutput) { + session.screensharebutton = false; + getById("translateButton").style.display = "none"; + getById("credits").style.display = "none"; + // getById("legal").style.display = "none"; + getById("header").style.display = "none"; + getById("controlButtons").classList.add("hidden"); + getById("helpbutton").style.display = "none"; + getById("helpbutton").style.opacity = 0; + getById("reportbutton").style.display = "none"; + getById("reportbutton").style.opacity = 0; + getById("calendarButton").style.display = "none"; + getById("calendarButton").style.opacity = 0; + document.documentElement.style.setProperty("--myvideo-background", "#0000"); + var styleTmp = document.createElement("style"); + styleTmp.innerHTML = ` + video { + background-image: none; + } + `; + document.head.appendChild(styleTmp); + } + + if (urlParams.has("ssb") || urlParams.has("screensharebutton")) { + session.screensharebutton = true; + } + + if (urlParams.has("hideheader") || urlParams.has("noheader") || urlParams.has("hh")) { + // needs to happen the room and permaid applications + getById("header").style.display = "none"; + getById("header").style.opacity = 0; + getById("obsState").classList.add("noheader"); + } else if (urlParams.has("showheader")) { + // needs to happen the room and permaid applications + getById("header").style.display = "inherit"; + getById("header").style.opacity = 1; + setTimeout(function(){ + getById("header").classList.remove("hidden"); + getById("head2").classList.remove("hidden"); + },100); + } else if (session.studioSoftware) { + getById("header").style.display = "none"; + getById("header").style.opacity = 0; + } + + if (urlParams.has("minidirector")) { + try { + var cssStylesheet = document.createElement("link"); + cssStylesheet.rel = "stylesheet"; + cssStylesheet.type = "text/css"; + cssStylesheet.media = "screen"; + cssStylesheet.href = "minidirector.css"; + document.getElementsByTagName("head")[0].appendChild(cssStylesheet); + } catch (e) { + errorlog(e); + } + } + + if (urlParams.has("postinterval")) { + // interval to post snapimage images + session.postInterval = urlParams.get("postinterval") || session.postInterval; + session.postInterval = parseInt(session.postInterval) || 60; + if (session.postInterval < 5) { + session.postInterval = 5; + } + } + if (urlParams.has("postimage")) { + var postURL = decodeURIComponent(urlParams.get("postimage")) || session.postURL; // default will post to https://temp.vdo.ninja/images/STREAMIDHERE.jpg , at an interval. it will be cached unless using url params. + setInterval( + function (postURL) { + try { + uploadImageSnapshot(postURL); + } catch (e) {} + }, + session.postInterval * 1000, + postURL + ); + } + + if (urlParams.has("cleanish")) { + session.cleanish = true; + } + + if (session.cleanish || !session.cleanOutput) { + if (session.obsControls) { + getById("obscontrolbutton").classList.remove("hidden"); + getById("controlButtons").classList.remove("hidden"); + } + } + + if (urlParams.has("nocontrolbar")) { + getById("controlButtons").classList.add("hidden"); + getById("controlButtons").style.display = "none"; + session.dedicatedControlBarSpace = false; + } + + if (urlParams.has("channels")) { + // must be loaded before channelOffset + session.audioChannels = parseInt(urlParams.get("channels")) || 8; // for audio output ; not input. see: &channelcount instead. + session.offsetChannel = 0; + log("max channels is 32; channels offset"); + session.audioEffects = true; + } + if (urlParams.has("channeloffset")) { + session.offsetChannel = parseInt(urlParams.get("channeloffset")); + log("max channels is 32; channels offset"); + session.audioEffects = true; + } + if (urlParams.get("playchannel")) { + // must be loaded before channelOffset + session.playChannel = parseInt(urlParams.get("playchannel")); // for audio output ; not input. see: &channelcount instead. + session.audioEffects = true; + } + if (urlParams.has("enhance")) { + //if (parseInt(urlParams.get('enhance')>0){ + session.enhance = true; //parseInt(urlParams.get('enhance')); + //} + } + + if (urlParams.has("degrade")) { + session.degrade = urlParams.get("degrade") || true; // Firefox, and maybe Safari, supported I think. + // the possible values are maintain-framerate, maintain-resolution, or balanced. The default value is balanced + } + + if (urlParams.has("maxviewers") || urlParams.has("mv")) { + session.maxviewers = urlParams.get("maxviewers") || urlParams.get("mv"); + if (session.maxviewers.length == 0) { + session.maxviewers = 1; + } else { + session.maxviewers = parseInt(session.maxviewers); + } + log("maxviewers set"); + } + + if (urlParams.has("maxpublishers") || urlParams.has("mp")) { + session.maxpublishers = urlParams.get("maxpublishers") || urlParams.get("mp"); + if (session.maxpublishers.length == 0) { + session.maxpublishers = 1; + } else { + session.maxpublishers = parseInt(session.maxpublishers); + } + log("maxpublishers set"); + } + + if (urlParams.has("maxconnections") || urlParams.has("mc")) { + session.maxconnections = urlParams.get("maxconnections") || urlParams.get("maxconnections"); + if (session.maxconnections.length == 0) { + session.maxconnections = 1; + } else { + session.maxconnections = parseInt(session.maxconnections); + } + + log("maxconnections set"); + } + + if (urlParams.has("secure")) { + session.security = true; + if (!session.cleanOutput) { + delayedStartupFuncs.push([warnUser, "Enhanced Security Mode Enabled."]); + } + } + + if (urlParams.has("requireencryption")) { + session.requireencryption = true; + } + if (urlParams.has("unsafe")) { + session.unsafe = true; + } + + if (urlParams.has("random") || urlParams.has("randomize")) { + session.randomize = true; + } + + if (urlParams.has("frameRate") || urlParams.has("fr") || urlParams.has("fps")) { + session.frameRate = urlParams.get("frameRate") || urlParams.get("fr") || urlParams.get("fps"); + session.frameRate = parseInt(session.frameRate); + log("frameRate Changed"); + log(session.frameRate); + } + + if (urlParams.has("tz")) { + // being depreciated, but still works with meshcast (no longer turn) + session.tz = urlParams.get("tz"); + if (session.tz === null || session.tz === "") { + session.tz = false; + } else { + session.tz = parseInt(session.tz); + } + } + + if (urlParams.has("maxframerate") || urlParams.has("mfr") || urlParams.has("mfps")) { + session.maxframeRate = urlParams.get("maxframerate") || urlParams.get("mfr") || urlParams.get("mfps"); + session.maxframeRate = parseInt(session.maxframeRate); + log("max frameRate assigned"); + log(session.maxframeRate); + } + + if (urlParams.has("buffer") || urlParams.has("buffer2")) { + // needs to be before sync + if (ChromiumVersion > 50 && ChromiumVersion < 78) { + } else { + session.buffer = parseFloat(urlParams.get("buffer")) || parseFloat(urlParams.get("buffer2")) || 0; + log("buffer Changed: " + session.buffer); + } + if (urlParams.has("buffer2")) { + session.includeRTT = true; + } + } + + if (urlParams.has("panning") || urlParams.has("pan")) { + session.panning = urlParams.get("panning") || urlParams.get("pan"); + if (session.panning === "") { + session.panning = true; + } + session.audioEffects = true; + } + + if (urlParams.has("sync")) { + if (ChromiumVersion > 50 && ChromiumVersion < 78) { + } else { + session.sync = parseFloat(urlParams.get("sync")); + log("sync Changed; in milliseconds. If not set, defaults to auto."); + log(session.sync); + session.audioEffects = true; + if (session.buffer === false) { + session.buffer = 0; + } + } + } + + if (urlParams.has("nomirror")) { + session.nomirror = true; + } + + if (urlParams.has("mirror")) { + if (urlParams.get("mirror") == "3") { + getById("main").classList.add("mirror"); + } else if (urlParams.get("mirror") == "2") { + session.mirrored = 2; + } else if (urlParams.get("mirror") == "0") { + session.mirrored = 0; + } else if (urlParams.get("mirror") == "false") { + session.mirrored = 0; + } else if (urlParams.get("mirror") == "off") { + session.mirrored = 0; + } else { + session.mirrored = 1; + } + } + + if (urlParams.has("mirroroutput")) { + let mirrorOutputParam = urlParams.get("mirroroutput"); + let value = mirrorOutputParam ? mirrorOutputParam.toLowerCase() : ""; + let enableMirrorOutput = !(value === "0" || value === "false" || value === "off"); + session.mirrorOutput = enableMirrorOutput; + if (session.mirrorOutput) { + session.permaMirrored = true; + } else if (session.permaMirrored) { + session.permaMirrored = false; + } + } + + if (urlParams.has("flip")) { + if (urlParams.get("flip") == "0") { + session.flipped = false; + } else if (urlParams.get("flip") == "false") { + session.flipped = false; + } else if (urlParams.get("flip") == "off") { + session.flipped = false; + } else { + session.flipped = true; + } + } + + if (urlParams.has("flipoutput")) { + let flipOutputParam = urlParams.get("flipoutput"); + let value = flipOutputParam ? flipOutputParam.toLowerCase() : ""; + session.flipOutput = !(value === "0" || value === "false" || value === "off"); + if (session.flipOutput) { + session.flipped = true; + } + } + + if (session.mirrored && session.flipped) { + try { + log("Mirror all videos"); + var mirrorStyle = document.createElement("style"); + mirrorStyle.innerHTML = "video {transform: scaleX(-1) scaleY(-1); }"; + document.getElementsByTagName("head")[0].appendChild(mirrorStyle); + } catch (e) { + errorlog(e); + } + } else if (session.mirrored) { + // mirror the video horizontally + try { + log("Mirror all videos"); + var mirrorStyle = document.createElement("style"); + mirrorStyle.innerHTML = "video {transform: scaleX(-1);}"; + document.getElementsByTagName("head")[0].appendChild(mirrorStyle); + } catch (e) { + errorlog(e); + } + } else if (session.flipped) { + // mirror the video vertically + try { + log("Mirror all videos"); + var mirrorStyle = document.createElement("style"); + mirrorStyle.innerHTML = "video {transform: scaleY(-1);}"; + document.getElementsByTagName("head")[0].appendChild(mirrorStyle); + } catch (e) { + errorlog(e); + } + } + + if (urlParams.has("icefilter")) { + log("ICE FILTER ENABLED"); + session.icefilter = urlParams.get("icefilter"); + } + if (urlParams.has("lanonly")) { + session.localNetworkOnly = true; + session.configuration = { + sdpSemantics: session.sdpSemantics // future-proofing + }; + } + if (urlParams.has("stunonly")) { + session.stunOnly = true; + } + + // IPv6 handling: By default, prefer IPv4 over IPv6 when both are available. + // This helps on "half-broken" IPv6 networks where the IPv6 path is flaky. + // - &ipv6=0 (or &preferipv4): Disable IPv6 candidates if IPv4 exists (fallback to IPv6 if no IPv4) + // - &ipv6=1: Allow normal IPv6/IPv4 behavior (both used equally) + // - Default (no param): Prefer IPv4 by sending IPv4 candidates first, but still allow IPv6 + if (urlParams.has("ipv6")) { + var ipv6Value = urlParams.get("ipv6"); + if (ipv6Value === "0" || ipv6Value === "false") { + log("IPv6 disabled (will use IPv4 when available, fallback to IPv6 if needed)"); + session.disableIpv6 = true; + } else if (ipv6Value === "1" || ipv6Value === "true") { + log("IPv6 explicitly enabled (normal dual-stack behavior)"); + session.disableIpv6 = false; + session.preferIpv4 = false; + } + } else if (urlParams.has("preferipv4") || urlParams.has("ipv4")) { + log("IPv4 preferred: IPv6 candidates will be dropped if IPv4 exists"); + session.disableIpv6 = true; + } + // Note: By default, session.preferIpv4 is true (set in webrtc.js defaults) + // which reorders candidates to send IPv4 first but still allows IPv6. + + if (urlParams.has("activespeaker") || urlParams.has("speakerview") || urlParams.has("sas")) { + session.activeSpeaker = urlParams.get("activespeaker") || urlParams.get("speakerview") || urlParams.get("sas") || 1; + session.activeSpeaker = parseInt(session.activeSpeaker); + session.style = 6; + session.audioEffects = true; + //session.audioMeterGuest = true; + session.minipreview = 2; + if (session.activeSpeaker == 1 || session.activeSpeaker == 3) { + session.animatedMoves = false; + } + session.fadein = true; + document.querySelector(":root").style.setProperty("--fadein-speed", 0.5); + session.activeSpeakerInterval = setInterval(function () { + activeSpeaker(false); + }, 100); + } else if (urlParams.has("noisegate") || urlParams.has("gating") || urlParams.has("gate") || urlParams.has("ng")) { + session.quietOthers = urlParams.get("noisegate") || urlParams.get("gating") || urlParams.get("gate") || urlParams.get("ng") || 1; + session.quietOthers = parseInt(session.quietOthers); + + if (session.quietOthers == 1) { + session.quietOthers = false; + session.noisegate = true; + session.audioEffects = true; + //session.audioMeterGuest = true; + } else if (session.quietOthers == 4) { + session.quietOthers = 1; + session.audioEffects = true; + //session.audioMeterGuest = true; + session.activeSpeakerInterval = setInterval(function () { + activeSpeaker(false); + }, 100); + } else if (!session.quietOthers) { + session.noisegate = false; + session.quietOthers = false; + } else { + session.audioEffects = true; + //session.audioMeterGuest = true; + session.activeSpeakerInterval = setInterval(function () { + activeSpeaker(false); + }, 100); + } + } + if (urlParams.has("activespeakerdelay") || urlParams.has("speakerviewdelay") || urlParams.has("sasdelay")) { + session.activeSpeakerTimeout = urlParams.get("activespeakerdelay") || urlParams.get("speakerviewdelay") || urlParams.get("sasdelay") || 0; + session.activeSpeakerTimeout = parseInt(session.activeSpeakerTimeout); + } + + if (urlParams.has("noisegatesettings")) { + session.noisegateSettings = urlParams.get("noisegatesettings"); + session.noisegateSettings = session.noisegateSettings.split(","); + } + + if (urlParams.has("fadein")) { + session.fadein = true; + if (urlParams.get("fadein") || 0) { + try { + var fadeinspeed = parseInt(urlParams.get("fadein") || 0) / 1000.0; + fadeinspeed += "s"; + document.querySelector(":root").style.setProperty("--fadein-speed", fadeinspeed); + } catch (e) { + errorlog("variable css failed"); + } + } else { + try { + var fadeinspeed = 0.5; + fadeinspeed += "s"; + document.querySelector(":root").style.setProperty("--fadein-speed", fadeinspeed); + } catch (e) { + errorlog("variable css failed"); + } + } + } + + if (urlParams.has("widget")) { + session.widget = urlParams.get("widget") || false; + + if (session.widget === "false" || session.widget === "0" || session.widget === "off") { + session.noWidget = true; + session.widget = false; + } else if (session.widget) { + session.widget = decodeURI(session.widget) || false; + log(session.widget); + } + } + + if (urlParams.has("widgetleft")) { + session.widgetleft = true; + } + if (urlParams.get("widgetwidth")) { // default is 25% + try{ + session.widgetwidth = parseFloat(urlParams.get("widgetwidth")) || 25; + + if (session.widgetwidth>50){ + session.widgetwidth = 50; + } + + document.querySelector(":root").style.setProperty("--widget-width", session.widgetwidth+"%"); + + + } catch(e){ + errorlog(e); + } + } + + if (urlParams.has("animated") || urlParams.has("animate")) { + session.animatedMoves = urlParams.get("animated") || urlParams.get("animate"); + if (session.animatedMoves === "false") { + session.animatedMoves = false; + } else if (session.animatedMoves === "0") { + session.animatedMoves = false; + } else if (session.animatedMoves === "no") { + session.animatedMoves = false; + } else if (session.animatedMoves === "off") { + session.animatedMoves = false; + } else { + session.animatedMoves = parseInt(session.animatedMoves) || 100; + } + if (session.animatedMoves > 200) { + session.animatedMoves = 200; + } + } else if (session.mobile) { + session.animatedMoves = false; + } + + if (urlParams.has("meter") || urlParams.has("meterstyle")) { + // same as also adding &style=3 + session.meterStyle = urlParams.get("meter") || urlParams.get("meterstyle") || 1; + session.meterStyle = parseInt(session.meterStyle); + if (session.meterStyle < 4) { + session.style = 3; // black canvas + } else { + session.style = -1; // no canvas + } + session.audioEffects = true; + } + + if (session.meterStyle == 5) { + document.documentElement.style.setProperty("--video-background-image-size-talking", "auto 35%"); + document.documentElement.style.setProperty("--video-background-image-size-screaming", "auto 45%"); + } + + if (urlParams.has("directorchat") || urlParams.has("dc")) { + session.directorChat = true; + } + + if (urlParams.has("style") || urlParams.has("st")) { + session.style = urlParams.get("style") || urlParams.get("st"); + if (parseInt(session.style) === 0 || session.style == "controls") { + // no audio only + session.style = 0; + } else if (parseInt(session.style) == 1 || session.style == "justvideo") { + // no audio only + session.style = 1; + } else if (parseInt(session.style) == 2 || session.style == "waveform") { + // audio waveform + session.style = 2; + session.audioEffects = true; ////!!!!!!! Do I want to enable the audioEffects myself? or do it here? + } else if (parseInt(session.style) == 3 || session.style == "volume") { + // audio meter ; see &meterstyle , where optios include default(false), 1, and 2. + session.style = 3; + session.audioEffects = true; + } else if (parseInt(session.style) == 4) { + // black background + session.style = 4; + } else if (parseInt(session.style) == 5) { + // random colored background + session.style = 5; + } else if (parseInt(session.style) == 7) { + // shows video elements for all connections; even those without video/audio + session.style = parseInt(session.style); + session.showall = true; + } else if (parseInt(session.style)) { + // 6 is the first letter of the name, surrounded with a colored circle + session.style = parseInt(session.style); + } else { + session.style = 1; + } + } + //if (session.style){ + // getById("toggleWaveformButton").classList.remove("hidden"); + //} + + if (urlParams.has("showall")) { + // just an alternative; might be compoundable + session.showall = true; + } + + if (urlParams.has("samplerate") || urlParams.has("sr")) { + // playout sample rate + session.sampleRate = parseInt(urlParams.get("samplerate")) || parseInt(urlParams.get("samplerate")) || 48000; + if (session.audioCtx) { + session.audioCtx.close(); // close the default audio context. + } + session.audioCtx = new AudioContext({ + // create a new audio context with a higher sample rate. + sampleRate: session.sampleRate // default is 48000 already + }); + session.audioEffects = true; + } + + if (session.audioCodec === "lyra") { + // WIP. does not work + try { + var { default: Module } = await import("./thirdparty/lyra/webassembly_codec_wrapper.js"); + await Module() + .then(module => { + console.log("Initialized codec's wasmModule."); + session.lyraCodecModule = module; + }) + .catch(e => { + console.log(`Module() error: ${e.name} message: ${e.message}`); + }); + } catch (e) { + errorlog(e); + } + if (session.lyraCodecModule) { + console.log("Lyra module loaded"); + session.micSampleRate = 16000; + session.encodedInsertableStreams = "lyra"; + } else { + console.log("Lyra module failed to load"); + } + } + + if (urlParams.has("e2ee")) { + session.encodedInsertableStreams = "e2ee"; + } else if (urlParams.has("insertablestreams") || urlParams.has("is")) { + session.encodedInsertableStreams = urlParams.get("insertablestreams") || urlParams.get("is") || true; + } + + if (urlParams.has("outboundsamplerate") || urlParams.has("obsr")) { + session.outboundSampleRate = parseInt(urlParams.get("outboundsamplerate")) || parseInt(urlParams.get("obsr")) || false; // default null + } else { + session.outboundSampleRate = null; // tmp + } + + if (urlParams.has("micsamplerate") || urlParams.has("msr")) { + session.micSampleRate = parseInt(urlParams.get("micsamplerate")) || parseInt(urlParams.get("msr")) || 48000; + } + + if (urlParams.has("micsamplesize")) { + session.micSampleSize = parseInt(urlParams.get("micsamplesize")) || 16; + } + + if (urlParams.has("noaudioprocessing") || urlParams.has("noap")) { + session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers? + session.disableViewerWebAudioPipeline = true; // this has the potential to break things. + session.audioEffects = false; // disable audio inbound effects also. + session.audioMeterGuest = false; + if (session.noisegate === null) { + session.noisegate = false; + } + } + + // For info, see this: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/availableOutgoingBitrate + if (urlParams.has("maxbandwidth")) { + // limits the bitrate based on the outbound total available bandwidth; chromium-based + session.maxBandwidth = urlParams.get("maxbandwidth") || 80; // 0 to 100; will reduce bitrate as a percentage of available + session.maxBandwidth = parseInt(session.maxBandwidth); + if (session.maxBandwidth > 200) { + // will over ride default 2500kbps if no bitrate is specified + session.maxBandwidth = 200; + } else if (session.maxBandwidth < 0) { + session.maxBandwidth = 0; + } + } + + if (urlParams.has("iframetarget")) { + session.iframetarget = urlParams.get("iframetarget"); // speciifies the IFRAME Hostname target + if (session.iframetarget) { + session.iframetarget = decodeURIComponent(session.iframetarget); + } else { + session.iframetarget = (window.location.protocol === "file:") ? "*" : window.location.origin; + } + } + + if (urlParams.has("sendframes")) { + session.sendframes = urlParams.get("sendframes"); + if (session.sendframes) { + try { + session.sendframes = decodeURIComponent(session.sendframes); + } catch (e) {} + } else { + session.sendframes = session.iframetarget || "*"; + } + } + + if (urlParams.has("tcp")) { + // forces the TURN servers to use TCP mode; still need to add &private to force TURN also tho + session.forceTcpMode = true; + } + + if (urlParams.has("stun")) { + var stunstring = urlParams.get("stun"); + stunstring = stunstring.split(";"); + if (stunstring[0] !== "false") { + // false disables the TURN server. Useful for debuggin + var stun = {}; + if (stunstring.length == 3) { + stun.username = stunstring[0]; // myusername + stun.credential = stunstring[1]; //mypassword + stun.urls = [stunstring[2]]; // ["turn:turn.obs.ninja:443"]; + } else if (stunstring.length == 1) { + stun.urls = [stunstring[0]]; + } + session.stunServers = [stun]; + } else { + session.stunServers = []; + } + } + if (urlParams.has("addstun")) { + var stunstring = urlParams.get("addstun"); + stunstring = stunstring.split(";"); + var stun = {}; + if (stunstring.length == 3) { + stun.username = stunstring[0]; // myusername + stun.credential = stunstring[1]; //mypassword + stun.urls = [stunstring[2]]; // ["turn:turn.obs.ninja:443"]; + } else if (stunstring.length == 1) { + stun.urls = [stunstring[0]]; + } + session.stunServers = session.stunServers.concat(stun); + } + + if (urlParams.has("bundle")) { + session.bundlePolicy = urlParams.get("bundle") || "max-bundle"; // default is browser default. + } + + if (urlParams.has("planb")) { + session.sdpSemantics = "plan-b"; // for legacy support, or debuggin, or whatever. + } + + if (urlParams.has("turn")) { + var turnstring = urlParams.get("turn"); + + if (turnstring == "twilio") { + // a sample function on loading remote credentials for TURN servers. + try { + session.ws = false; // prevents connection + var twillioRequest = new XMLHttpRequest(); + twillioRequest.onload = function () { + if (this.status === 200) { + try { + var res = JSON.parse(this.responseText); + } catch (e) { + console.error(e); + return; + } + session.configuration = { + iceServers: [ + { + username: res["1"], + credential: res["2"], + url: "turn:global.turn.twilio.com:3478?transport=tcp", + urls: "turn:global.turn.twilio.com:3478?transport=tcp" + }, + { + username: res["1"], + credential: res["2"], + url: "turn:global.turn.twilio.com:443?transport=tcp", + urls: "turn:global.turn.twilio.com:443?transport=tcp" + } + ], + sdpSemantics: session.sdpSemantics // future-proofing + }; + if (session.ws === false) { + session.ws = null; // allows connection (clears state) + session.connect(); // connect if not already connected. + } + } + // system does not connect if twilio API does not respond. + }; + twillioRequest.open("GET", "https://turn.example.com:443/twilio", true); // `false` makes the request synchronous + twillioRequest.send(); + } catch (e) { + errorlog("Twilio Failed"); + } + } else if (turnstring == "nostun") { + // disable TURN servers + session.configuration = { + sdpSemantics: session.sdpSemantics // future-proofing + }; + } else if (turnstring == "false" || turnstring == "off" || turnstring == "0") { + // disable TURN servers + session.configuration = { + iceServers: session.stunServers, + sdpSemantics: session.sdpSemantics // future-proofing + }; + } else { + try { + //session.configuration = {iceServers: [], sdpSemantics: session.sdpSemantics}; + turnstring = turnstring.split(";"); + if (turnstring !== "false") { + // false disables the TURN server. Useful for debuggin + var turn = {}; + if (turnstring.length == 3) { + turn.username = turnstring[0]; // myusername + turn.credential = turnstring[1]; //mypassword + turn.urls = [turnstring[2]]; // ["turn:turn.obs.ninja:443"]; + } else if (turnstring.length == 1) { + turn.urls = [turnstring[0]]; + } + session.configuration = { + iceServers: session.stunServers, + sdpSemantics: session.sdpSemantics // future-proofing + }; + + session.configuration.iceServers.push(turn); + } + } catch (e) { + if (!session.cleanOutput) { + warnUser("TURN server parameters were wrong."); + } + errorlog(e); + } + } + } + + if (urlParams.has("apiserver") && urlParams.get("apiserver")) { + // must set this after any custom TURN / STUN settings, else it might over-ride them. + session.apiserver = urlParams.get("apiserver"); + } + + if (urlParams.has("speedtest")) { + // must set this after any custom TURN / STUN settings, else it might over-ride them. + session.speedtest = true; + if (urlParams.get("speedtest")) { + // forces essentially UDP mode, unless TCP is specified, and some other stuff + session.speedtest = urlParams.get("speedtest").toLowerCase(); // also limits bitrate + } + setupSpeedtest(); + } + + if (urlParams.has("privacy") || urlParams.has("private") || urlParams.has("relay")) { + // please only use if you are also using your own TURN service. + session.privacy = urlParams.get("privacy") || urlParams.get("private") || urlParams.get("relay") || true; + + try { + // I'll re-apply this in the setupSpeedtest() promise callback, just to be case. + if (session.configuration) { + // this needs to set last, otherwise it might be overridden + session.configuration.iceTransportPolicy = "relay"; // https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/address + } + } catch (e) { + if (!session.cleanOutput) { + warnUser("Privacy mode failed to configure."); + } + errorlog(e); + } + + if (session.speedtest) { + warnlog("Bitrate being throttled to max of 6000 kbps"); + if (session.maxvideobitrate !== false) { + if (session.maxvideobitrate > 6000) { + session.maxvideobitrate = 6000; // Please feel free to get rid of this if using your own TURN servers... + } + } else { + session.maxvideobitrate = 6000; // don't let people pull more than 6000 from you + } + if (session.bitrate !== false) { + if (session.bitrate > 6000) { + session.bitrate = 6000; // Please feel free to get rid of this if using your own TURN servers... + } + } + } else { + warnlog("Bitrate being throttled to max of 4000 kbps"); + if (session.maxvideobitrate !== false) { + if (session.maxvideobitrate > 4000) { + session.maxvideobitrate = 4000; // Please feel free to get rid of this if using your own TURN servers... + } + } else { + session.maxvideobitrate = 4000; // don't let people pull more than 4000 from you + } + if (session.bitrate !== false) { + if (session.bitrate > 4000) { + session.bitrate = 4000; // Please feel free to get rid of this if using your own TURN servers... + } + } + } + } + + if (urlParams.has("osc") || urlParams.has("api")) { + if (urlParams.get("osc") || urlParams.get("api")) { + session.api = urlParams.get("osc") || urlParams.get("api") || false; + if (session.api) { + setTimeout(function () { + oscClient(); + }, 1000); + } + } + } + + if (urlParams.has("postapi") || urlParams.has("posturl")) { + session.postApi = urlParams.get("postapi") || urlParams.get("posturl") || false; // ie: &postapi=https%3A%2F%2Fwebhook.site%2Fb190f5bf-e4f8-454a-bd51-78b5807df9c1 + if (session.postApi) { + try { + session.postApi = decodeURI(session.postApi) || session.postApi; // needs to be SSL enabled. + } catch (e) { + console.error(e); + } + } + } + + if (urlParams.has("queue")) { + session.queue = true; + if (urlParams.get("queue") === "false") { + session.queue = false; + } else if (urlParams.get("queue") === "0") { + session.queue = false; + } else if (urlParams.get("queue") === "off") { + session.queue = false; + } else if (urlParams.get("queue")) { + session.queue = urlParams.get("queue"); + } + } + + if (urlParams.has("queue2") || urlParams.has("screen")) { + // the guest can see the director, if the director doesn't have &queue + session.queue = true; + session.queueType = 2; + } + + if (urlParams.has("queue3") || urlParams.has("hold")) { + // &hold (alias: &queue3) - Full bidirectional isolation until activated. + // + // - Guest cannot see director or other guests + // - Director cannot see guest's video/audio (only control box with label) + // - Other guests cannot see the hold guest + // - On activation, all directions open and normal flow resumes + // + // Technical: Sets needsPublishing=true, skips initialPublish until activated. + // Use case: Green room / screening where director doesn't want to be seen either. + session.queue = true; + session.queueType = 3; + } + + if (urlParams.has("queue4") || urlParams.has("holdwithvideo")) { + // &holdwithvideo (alias: &queue4) - Like &hold but allows Guest→Director media. + // + // - Guest cannot see director or other guests (still isolated) + // - Director CAN see guest's video/audio (for preview/screening) + // - Other guests cannot see the hold guest + // - On activation, remaining directions open + // + // IMPORTANT: The name "holdwithvideo" is slightly misleading. It does NOT force + // video to be sent. It simply removes the publishing block that &hold creates. + // The actual video/audio that flows is still determined by: + // - What the director requests ({video: true/false, audio: true/false}) + // - Room-level rules (&novideo, &nodirectorvideo, etc.) + // - All normal gating logic + // + // Technical: Calls initialPublish normally (unlike queue3), respects allowVideo/allowAudio. + // Use case: Director wants to preview guest (check lighting, verify identity) before admission. + session.queue = true; + session.queueType = 4; + } + + if (session.director && (urlParams.has("approvepopup") || urlParams.has("approvalpopup"))) { + // Opt-in approval popup for directors + session.approval_popup = true; + try { log("[flags] &approvepopup detected; approval_popup=true"); } catch (e) {} + } + // do not reference stream ID before this point, as it might change after this point. + + if (urlParams.has("push") || urlParams.has("id") || urlParams.has("permaid") || (session.sticky && session.decrypted)) { + session.permaid = urlParams.get("push") || urlParams.get("id") || urlParams.get("permaid"); + + if (session.permaid) { + session.permaid = sanitizeStreamID(session.permaid) || null; + session.streamID = session.permaid || session.streamID; + } else if ((urlParams.has("permaid") || (session.sticky && session.decrypted)) && getStorage("permaid")) { + session.streamID = sanitizeStreamID(getStorage("permaid")) || session.streamID; + session.permaid = null; + } else { + session.permaid = null; + } + + if (session.permaid && ((session.permaid.length<3) || (session.permaid==="test"))) { + if (session.password === session.defaultPassword) { + if (location.hostname === "vdo.ninja") { + if (!session.cleanOutput){ + window.focus(); + warnUser(getTranslation("insecure-stream-id"),10000); + } + } + } + } + + if (urlParams.has("permaid") || (session.sticky && session.decrypted)) { + setStorage("permaid", session.streamID, 99999); + } + + if (urlParams.has("push")) { + updateURL("push=" + session.streamID, true, false); + } else if (urlParams.has("id")) { + updateURL("id=" + session.streamID, true, false); // not 'officially' supporting this yet; we'll see. + } else if (urlParams.has("permaid")) { + updateURL("permaid=" + session.streamID, true, false); + } else { + updateURL("push=" + session.streamID, true, false); + } + + if (session.director) { + // if I do a short form of this, it will cause duplications in the code elsewhere. + //var director_room_input = urlParams.get('director'); + //director_room_input = sanitizeRoomName(director_room_input); + //createRoom(director_room_input); + session.permaid = false; // used to avoid a trigger later on. + } else { + getById("container-1").className = "column columnfade hidden"; + getById("container-4").className = "column columnfade hidden"; + getById("dropButton").className = "column columnfade hidden"; + + getById("info").innerHTML = ""; + if (session.videoDevice === 0) { + miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); + } else { + miniTranslate(getById("add_camera"), "share-your-camera", "Share your Camera"); + } + miniTranslate(getById("add_screen"), "share-your-screen", "Share your Screen"); + getById("container-2").title = getById("add_screen").innerText; + getById("container-3").title = getById("add_camera").innerText; + + getById("passwordRoom").value = ""; + getById("videoname1").value = ""; + getById("dirroomid").innerHTML = ""; + getById("roomid").innerHTML = ""; + + getById("mainmenu").style.alignSelf = "center"; + getById("mainmenu").classList.add("mainmenuclass"); + getById("header").style.alignSelf = "center"; + + //if ((iOS) || (iPad)) { + //getById("header").style.display = "none"; // just trying to free up space. + //} + + if (session.webcamonly == true) { + // mobile or manual flag 'webcam' pflag set + getById("head1").innerHTML = '- Please accept any camera permissions'; + } else { + getById("head1").innerHTML = '
    - Please select which you wish to share'; + } + + if (!session.cleanOutput) { + try { + if (session.studioSoftware) { + getById("unexpectedPushLink").classList.remove("hidden"); + } + } catch (e) {} + } + } + } + + if (window.vdoAuth){ + if (session.streamID) { + await window.vdoAuth.assignStream(); + } + getById("mainmenu").classList.remove("hidden2"); + getById("header").classList.remove("hidden2"); + } + + if (session.roomid || urlParams.has("roomid") || urlParams.has("r") || urlParams.has("room") || filename || session.permaid !== false) { + var roomid = ""; + if (urlParams.has("room")) { + // needs to be first; takes priority + roomid = urlParams.get("room"); + } else if (urlParams.has("roomid")) { + roomid = urlParams.get("roomid"); + } else if (urlParams.has("r")) { + roomid = urlParams.get("r"); + } else if (session.roomid) { + roomid = session.roomid; + } else if (filename) { + roomid = filename; + } + session.roomid = sanitizeRoomName(roomid); + if (session.director) { + if (session.director !== session.roomid) { + if (!session.cleanOutput) { + warnUser("Conflicting director and room values were provided.\n\n Check your URL parameters; there should be only &director OR &room", 5000); + } + } + session.roomid = false; + } + if (session.quality===false){ + try { + document.getElementById("webcamquality").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : (session.quality_wb || 0)); + document.getElementById("webcamquality3").elements.namedItem("resolution").value = (session.roomid ? (session.quality_room || 0) : (session.quality_wb || 0)); + } catch(e){} + } + + } else if (session.quality===false){ + try { + document.getElementById("webcamquality").elements.namedItem("resolution").value = session.quality_wb || 0; + document.getElementById("webcamquality3").elements.namedItem("resolution").value = session.quality_wb || 0; + } catch(e){} + } + + if (session.permaid === false && session.roomid === false && session.view === false && session.effect === false && session.director === false) { + session.effect = null; + getById("credits").innerHTML = "Version: " + session.version + ' | ' + getById("credits").innerHTML; + } + + if (session.mobile && session.permaid === false && !session.roomid) { + getById("rememberStreamID").classList.remove("hidden"); + + let rememberStreamIDmobile = getStorage("rememberStreamIDmobile"); + if (rememberStreamIDmobile === "false") { + getById("rememberStreamIDcheck").checked = false; + } + } + + if (urlParams.has("hostwhep") || urlParams.has("whepout")) { + session.whepHost = urlParams.get("hostwhep") || urlParams.get("whepout") || session.streamID || false; + } + + // General WHIP output toggles + if (urlParams.has("whipnoscreen")) { + session.whipPublishScreen = false; + } + if (urlParams.has("whipprimaryonly")) { + session.whipPublishPrimary = true; + session.whipPublishScreen = false; + } + if (urlParams.has("whipscreenonly")) { + session.whipPublishPrimary = false; + session.whipPublishScreen = true; + } + + if (urlParams.get("mediamtx")){ + session.mediamtx = urlParams.get("mediamtx"); + } + + if (session.mediamtx){ + if (urlParams.has("mediamtxnoscreen")) { + session.whipPublishScreen = false; + } + if (urlParams.has("mediamtxscreenonly")) { + session.whipPublishPrimary = false; + session.whipPublishScreen = true; + } + if (!session.mediamtx.includes(".") && !session.mediamtx.includes("localhost")){ + session.mediamtx += ".com"; + } + if (!session.mediamtx.includes(":")){ + session.mediamtx += ":8889"; + } + let mediamtxBase = session.mediamtx; + let scheme = "https://"; + if (mediamtxBase.startsWith("http://") || mediamtxBase.startsWith("https://")) { + scheme = ""; + } else if (mediamtxBase.startsWith("localhost:")) { + scheme = "http://"; + } + if (scheme){ + mediamtxBase = scheme + mediamtxBase; + } + if (mediamtxBase.endsWith("/")) { + mediamtxBase = mediamtxBase.slice(0, -1); + } + const streamId = session.streamID; + const screenId = streamId + "_s"; + const buildEndpoint = id => `${mediamtxBase}/${id}`; + + const primaryWhipUrl = `${buildEndpoint(streamId)}/whip`; + const primaryWhepSettings = { + type: "whep", + url: `${buildEndpoint(streamId)}/whep`, + token: streamId, + media: "primary", + started: false + }; + const screenWhipUrl = `${buildEndpoint(screenId)}/whip`; + const screenWhepSettings = { + type: "whep", + url: `${buildEndpoint(screenId)}/whep`, + token: screenId, + media: "screen", + started: false + }; + + if (session.whipPublishPrimary) { + if (!session.whipOutputUserSet) { + session.whipOutput = primaryWhipUrl; + } + if (!session.whipoutSettingsUserSet) { + session.whipoutSettings = primaryWhepSettings; + } + } else { + if (!session.whipOutputUserSet) { + session.whipOutput = false; + } + if (!session.whipoutSettingsUserSet) { + session.whipoutSettings = false; + } + } + + if (session.whipPublishScreen) { + if (!session.whipOutputScreenUserSet) { + session.whipOutputScreen = screenWhipUrl; + } + if (!session.whipoutScreenSettingsUserSet) { + session.whipoutScreenSettings = screenWhepSettings; + } + } else { + if (!session.whipOutputScreenUserSet) { + session.whipOutputScreen = false; + } + if (!session.whipoutScreenSettingsUserSet) { + session.whipoutScreenSettings = false; + } + } + + if (session.whipPublishPrimary && session.whipoutSettings) { + console.log("WHIP OUT: " + session.whipOutput + ", WHEP SHARE: " + session.whipoutSettings.url); + } + if (session.whipPublishScreen && session.whipoutScreenSettings) { + console.log("WHIP OUT SCREEN: " + session.whipOutputScreen + ", WHEP SHARE: " + session.whipoutScreenSettings.url); + } + if (session.whipPublishScreen && session.whipOutputScreen && session.screenShareState) { + whipOutScreen(); + } + if (session.stereo === false){ + if (!session.whipOutAudioCodec || (session.whipOutAudioCodec=="opus")){ + session.stereo=3; + } + } + } + + if (urlParams.has("meshcastnoscreen")) { + session.whipPublishScreen = false; + } + if (urlParams.has("meshcastscreenonly")) { + session.whipPublishPrimary = false; + session.whipPublishScreen = true; + } + if (urlParams.has("meshcastprimaryonly")) { + session.whipPublishPrimary = true; + session.whipPublishScreen = false; + } + + if (urlParams.has("effects") || urlParams.has("effect")) { + session.effect = urlParams.get("effects") || urlParams.get("effect") || null; + } else if (urlParams.has("digitalzoom")) { + session.effect = "7"; + if (urlParams.get("digitalzoom")){ + session.effectValue_default = parseFloat(urlParams.get("digitalzoom")) || 1; + } + } + + if (session.effect && !session.cleanOutput) { + if (ChromiumVersion && ChromiumVersion === 122) { + warnUser("⚠️ Notice: A recent update to Chrome/Edge can cause the browser to crash, especially when using &effects or &zoom.\n\nBrowser updates are rolling out to fix the issue, however avoiding the use of digital video effects for now might be prudent", 30000); + } + } + + if (urlParams.get("zoom")){ + session.zoom = parseFloat(urlParams.get("zoom")) || false; + session.ptz = true; + } + if (urlParams.get("wb") || urlParams.get("whitebalance")) { + session.whiteBalance = urlParams.get("wb") || urlParams.get("whitebalance"); + } + if (urlParams.get("exposure")) { + session.exposure = urlParams.get("exposure"); + } + if (urlParams.get("saturation")) { + session.saturation = urlParams.get("saturation"); + } + if (urlParams.get("sharpness")) { + session.sharpness = urlParams.get("sharpness"); + } + if (urlParams.get("contrast")) { + session.contrast = urlParams.get("contrast"); + } + if (urlParams.get("brightness")) { + session.brightness = urlParams.get("brightness"); + } + if (urlParams.get("focus")) { + session.focusDistance = urlParams.get("focus"); + } + + + if (urlParams.has("wss")) { + session.customWSS = true; + session.wssSetViaUrl = true; + if (urlParams.get("wss")) { + session.wss = urlParams.get("wss"); + if (!session.wss.startsWith("wss://")) { + session.wss = "wss://" + session.wss; + } + } + } else if (urlParams.has("wss2")) { + session.wssSetViaUrl = true; + if (urlParams.get("wss2")) { + session.wss = urlParams.get("wss2"); + if (!session.wss.startsWith("wss://")) { + session.wss = "wss://" + session.wss; + } + } + } else if (urlParams.get("audience")) { + session.audience = urlParams.get("audience"); + if (urlParams.get("audience") && session.view !== false) { + session.wss = "wss://audience.vdo.ninja/listen/" + session.audience; + } else { + session.wss = "wss://audience.vdo.ninja/publish/" + session.audience; + } + } + + if (urlParams.has("bypass")) { + session.bypass = true; + if (!urlParams.get("bypass")){ + session.customWSS = true; + } + } + + if (window.FaceDetector !== undefined) { + document.querySelectorAll(".facetracker").forEach(ele => { + ele.disabled = null; + ele.removeAttribute("disabled"); + ele.title = "Will slowly pan, tilt, and zoom in on the first face detected"; + }); + } + + if (urlParams.has("imagelist")) { + // "&imagelist="+encodeURIComponent(JSON.stringify(["./media/bg_sample.webp", "./media/bg_sample2.webp"])) + var imageList = urlParams.get("imagelist"); // + if (imageList) { + try { + imageList = JSON.parse(decodeURIComponent(imageList)); + if (imageList.length) { + session.defaultBackgroundImages = imageList; // ["./media/bg_sample.webp", "./media/bg_sample2.webp"] + } else { + warnlog("empty image array; skipping"); + } + } catch (e) { + console.error(e); + try { + imageList = decodeURIComponent(imageList); + } catch (e) { + console.error(e); + } + if (imageList) { + session.defaultBackgroundImages = [imageList]; // ["./media/bg_sample.webp", "./media/bg_sample2.webp"] + } else { + warnlog("empty image array; skipping"); + } + } + } + } + + if (session.effect !== false) { + if (session.effect === null) { + getById("effectsDiv").style.display = "inline-block"; + session.effect = "0"; + } else if (session.effect === "0" || session.effect === "false" || session.effect === "off" || session.effect === 0) { + session.effect = false; + getById("effectSelector3").style.display = "none"; + getById("effectsDiv3").style.display = "none"; + getById("effectSelector").style.display = "none"; + getById("effectsDiv").style.display = "none"; + } + + if (session.effect === "5") { + loadContentEffectsImages(); + + getById("effectSelector").style.display = "none"; + getById("effectsDiv").style.display = "inline-block"; + } + if (session.effect === "3a") { + // heavier blur + session.effectValue = 5; + session.effect = "3"; + } else if (session.effect === "3") { + session.effectValue = 2; + } else if (session.effect === "7") { + session.effectValue = session.effectValue || 1; + } else if (["15", "14"].includes(session.effect)) { + session.effectValue = 25; + getById("effectSelector").style.display = "none"; + getById("effectsDiv").style.display = "inline-block"; + loadContentEffectsImages(); + } + // mirror == 2 + // face == 1 + // blur = 3 + // green = 4 + // image = 5 + } + + if (urlParams.has("effectvalue") || urlParams.has("ev")) { + session.effectValue = parseFloat(urlParams.get("effectvalue")) || parseFloat(urlParams.get("ev")) || 0; + session.effectValue_default = session.effectValue; + } + + if (session.webcamonly == true) { + if (session.introButton) { + getById("container-2").className = "column columnfade hidden"; // Hide screen share + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + } else { + getById("container-2").className = "column columnfade hidden"; // Hide screen share + getById("container-3").classList.add("skip-animation"); + getById("container-3").classList.remove("pointer"); + delayedStartupFuncs.push([previewWebcam]); + } + } + if (session.introOnClean && session.permaid === false && session.roomid === false) { + //getById("container-2").className = 'column columnfade hidden'; // Hide screen share + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + } else if (session.introOnClean && session.scene === false && (session.permaid !== false || session.roomid !== false)) { + getById("container-2").className = "column columnfade hidden"; // Hide screen share + getById("container-3").classList.add("skip-animation"); + getById("container-3").classList.remove("pointer"); + delayedStartupFuncs.push([previewWebcam]); + } + + //if (!session.director && ((ChromiumVersion == 86) || (ChromiumVersion == 77) || (ChromiumVersion == 62) || (ChromiumVersion == 51)) && (((session.permaid===false) && session.view) || (session.scene!==false))){ + // session.studioSoftware = true; // vmix + + if (session.cleanViewer) { + if (((session.view!==false) || session.whepInput || session.whipView) && !session.director && session.permaid === false) { + session.cleanOutput = true; + } + } + if (urlParams.has("clock") || urlParams.has("clock24")) { + let urlClock = urlParams.get("clock") || urlParams.get("clock24"); + if (urlParams.has("clock24")) { + session.clock24 = true; + } + session.showTime = true; + if (urlClock === "false") { + session.showTime = false; + } else if (urlClock === "0") { + session.showTime = false; + } else if (urlClock === "1") { + getById("overlayClockContainer2").classList.add("top"); + getById("overlayClockContainer2").classList.add("left"); + } else if (urlClock === "7") { + getById("overlayClockContainer2").classList.add("bottom"); + getById("overlayClockContainer2").classList.add("left"); + } else if (urlClock === "4") { + getById("overlayClockContainer2").classList.add("vmiddle"); + getById("overlayClockContainer2").classList.add("left"); + } else if (urlClock === "2") { + getById("overlayClockContainer2").classList.add("top"); + getById("overlayClockContainer2").classList.add("hmiddle"); + } else if (urlClock === "8") { + getById("overlayClockContainer2").classList.add("bottom"); + getById("overlayClockContainer2").classList.add("hmiddle"); + } else if (urlClock === "5") { + getById("overlayClockContainer2").classList.add("vmiddle"); + getById("overlayClockContainer2").classList.add("hmiddle"); + } else if (urlClock === "3") { + getById("overlayClockContainer2").classList.add("top"); + getById("overlayClockContainer2").classList.add("right"); + } else if (urlClock === "9") { + getById("overlayClockContainer2").classList.add("bottom"); + getById("overlayClockContainer2").classList.add("right"); + } else if (urlClock === "6") { + getById("overlayClockContainer2").classList.add("vmiddle"); + getById("overlayClockContainer2").classList.add("right"); + } + } else if (session.cleanOutput) { + session.showTime = false; + } + + if (urlParams.has("timer")) { + if (urlParams.get("timer") === "1") { + getById("overlayClockContainer").classList.add("top"); + getById("overlayClockContainer").classList.add("left"); + } else if (urlParams.get("timer") === "7") { + getById("overlayClockContainer").classList.add("bottom"); + getById("overlayClockContainer").classList.add("left"); + } else if (urlParams.get("timer") === "4") { + getById("overlayClockContainer").classList.add("vmiddle"); + getById("overlayClockContainer").classList.add("left"); + } else if (urlParams.get("timer") === "2") { + getById("overlayClockContainer").classList.add("top"); + getById("overlayClockContainer").classList.add("hmiddle"); + } else if (urlParams.get("timer") === "8") { + getById("overlayClockContainer").classList.add("bottom"); + getById("overlayClockContainer").classList.add("hmiddle"); + } else if (urlParams.get("timer") === "5") { + getById("overlayClockContainer").classList.add("vmiddle"); + getById("overlayClockContainer").classList.add("hmiddle"); + } else if (urlParams.get("timer") === "3") { + getById("overlayClockContainer").classList.add("top"); + getById("overlayClockContainer").classList.add("right"); + } else if (urlParams.get("timer") === "9") { + getById("overlayClockContainer").classList.add("bottom"); + getById("overlayClockContainer").classList.add("right"); + } else if (urlParams.get("timer") === "6") { + getById("overlayClockContainer").classList.add("vmiddle"); + getById("overlayClockContainer").classList.add("right"); + } else { + getById("overlayClockContainer").classList.add("top"); + getById("overlayClockContainer").classList.add("hmiddle"); + } + } + + if (urlParams.has("miconlyoption") || urlParams.has("moo")) { + session.optionalMicOnly = true; + } + + if (urlParams.has("hidescreenshare") || urlParams.has("hidess") || urlParams.has("sshide") || urlParams.has("screensharehide")) { + // this way I don't need to remember what it's called. I can just guess. :D + session.screenShareElementHidden = true; + } + + if (urlParams.has("sspaused") || urlParams.has("sspause") || urlParams.has("ssp")) { + // this way I don't need to remember what it's called. I can just guess. :D + session.screenShareStartPaused = true; + } + + if (urlParams.has("zoomedbitrate") || urlParams.has("zb")) { + // this way I don't need to remember what it's called. I can just guess. :D + session.zoomedBitrate = urlParams.get("zoomedbitrate") || urlParams.get("zb") || 2500; + session.zoomedBitrate = parseInt(session.zoomedBitrate); + } + + if (urlParams.has("screenshareid") || urlParams.has("ssid")) { + if (urlParams.get("screenshareid") || urlParams.get("ssid")) { + session.screenshareid = urlParams.get("screenshareid") || urlParams.get("ssid"); + session.screenshareid = sanitizeStreamID(session.screenshareid); + } else { + session.screenshareid = session.streamID + "_ss"; + } + } + + if (urlParams.has("screensharevideoonly") || urlParams.has("ssvideoonly") || urlParams.has("ssvo")) { + session.screenshareVideoOnly = true; + getById("audioScreenShare1").classList.add("hidden"); + } + + if (urlParams.has("screensharefps") || urlParams.has("ssfps")) { + if (urlParams.get("screensharefps") || urlParams.get("ssfps")) { + session.screensharefps = urlParams.get("screensharefps") || urlParams.get("ssfps"); + session.screensharefps = parseInt(session.screensharefps) || 2; + } + } + + if (urlParams.has("dropdown")){ + getById("dropButton").className = ""; + } + + if (urlParams.has("screensharequality") || urlParams.has("ssq")) { + session.screensharequality = urlParams.get("screensharequality") || urlParams.get("ssq") || "0"; + + if (session.screensharequality.toLowerCase() == "4k") { + session.screensharequality = -2; + } else if (session.screensharequality.toLowerCase() == "2160p") { + session.screensharequality = -2; + } else if (session.screensharequality.toLowerCase() == "2160") { + session.screensharequality = -2; + } else if (session.screensharequality.toLowerCase() == "2k") { + session.screensharequality = -3; + } else if (session.screensharequality.toLowerCase() == "1440p") { + session.screensharequality = -3; + } else if (session.screensharequality.toLowerCase() == "1440") { + session.screensharequality = -3; + } else if (session.screensharequality.toLowerCase() == "hd") { + session.screensharequality = 1; + } else if (session.screensharequality.toLowerCase() == "720p") { + session.screensharequality = 1; + } else if (session.screensharequality.toLowerCase() == "720") { + session.screensharequality = 1; + } else if (session.screensharequality.toLowerCase() == "fullhd") { + session.screensharequality = 0; + } else if (session.screensharequality.toLowerCase() == "1080p") { + session.screensharequality = 0; + } else if (session.screensharequality.toLowerCase() == "1080") { + session.screensharequality = 0; + } else if (session.screensharequality.toLowerCase() == "high") { + session.screensharequality = 0; + } else if (session.screensharequality.toLowerCase() == "360p") { + session.screensharequality = 2; + } else if (session.screensharequality.toLowerCase() == "360") { + session.screensharequality = 2; + } else if (session.screensharequality.toLowerCase() == "low") { + session.screensharequality = 2; + } else { + session.screensharequality = parseInt(session.screensharequality) || 0; + } + try { + getById("gear_screen").parentNode.removeChild(getById("gear_screen")); + } catch (e) {} + } + + if (urlParams.has("screensharebitrate") || urlParams.has("ssbitrate")) { + session.screenShareBitrate = urlParams.get("screensharebitrate") || urlParams.get("ssbitrate"); + session.screenShareBitrate = parseInt(session.screenShareBitrate) || 2500; + } + + if (urlParams.has("compressed") || urlParams.has("compresssdp") || urlParams.has("compress")){ + session.compressSDP = true; // WIP. + } + + if (urlParams.has("screensharelabel") || urlParams.has("sslabel")) { + session.screenShareLabel = urlParams.get("screensharelabel") || urlParams.get("sslabel"); + try { + session.screenShareLabel = decodeURIComponent(session.screenShareLabel); + } catch (e) {} + session.screenShareLabel = session.screenShareLabel.replace(/_/g, " "); + } + + if (urlParams.has("whepshare") || urlParams.has("whepsrc")) { + try { + session.whepSrc = urlParams.get("whepshare") || urlParams.get("whepsrc") || null; + log("WHEP SRC: " + session.whepSrc); + if (session.whepSrc) { + try { + session.whepSrc = decodeURIComponent(session.whepSrc); + } catch (e) { + session.whepSrc = session.whepSrc; + } + } else { + session.whepSrc = await promptAlt("Enter the WHEP source as a URL"); + } + if (session.whepSrc) { + session.whipoutSettings = { type: "whep", url: session.whepSrc }; + session.whipoutSettingsUserSet = true; + } + } catch (e) { + errorlog(e); + } + } + if (urlParams.has("whepsharetoken") || urlParams.has("whepsrctoken")) { + if (session.whipoutSettings) { + try { + session.whepSrcToken = urlParams.get("whepsharetoken") || urlParams.get("whepsrctoken") || null; + log("WHEP TOKEN: " + session.whepSrcToken); + if (session.whepSrcToken) { + try { + session.whepSrcToken = decodeURIComponent(session.whepSrcToken); + } catch (e) { + session.whepSrcToken = session.whepSrcToken; + } + } else { + session.whepSrcToken = await promptAlt("Enter the WHEP source token"); + } + if (session.whepSrcToken) { + session.whipoutSettings.token = session.whepSrcToken; + session.whipoutSettingsUserSet = true; + } + } catch (e) { + errorlog(e); + } + } + } + + if (session.roomid !== false) { + if (!session.cleanOutput) { + if (session.roomid === "test") { + if (session.password === session.defaultPassword) { + window.focus(); + var testRoomResponse = confirm(getTranslation("room-test-not-good")); + if (testRoomResponse == false) { + hangup(); + throw new Error("User requested to not enter room 'room'."); + } + } + } + } + + if (session.audioDevice === false && session.outputDevice === false) { + getById("headphonesDiv2").classList.remove("hidden"); + getById("headphonesDiv").classList.remove("hidden"); + } + getById("addPasswordBasic").style.display = "none"; + + getById("info").innerHTML = ""; + getById("info").style.color = "#CCC"; + getById("videoname1").value = session.roomid; + getById("dirroomid").innerText = session.roomid; + getById("roomid").innerText = session.roomid; + getById("container-1").className = "column columnfade hidden"; + getById("container-4").className = "column columnfade hidden"; + // container 5 is share media file; 6 is share website + getById("container-7").style.display = "none"; + getById("container-8").style.display = "none"; + getById("container-9").style.display = "none"; + getById("container-10").style.display = "none"; + getById("container-11").style.display = "none"; + getById("container-12").style.display = "none"; + getById("container-13").style.display = "none"; + getById("container-14").style.display = "none"; + getById("container-15").style.display = "none"; + getById("container-16").style.display = "none"; + getById("container-17").style.display = "none"; + getById("container-18").style.display = "none"; + getById("container-19").style.display = "none"; + getById("container-20").style.display = "none"; + getById("container-21").style.display = "none"; + getById("mainmenu").style.alignSelf = "center"; + getById("mainmenu").classList.add("mainmenuclass"); + getById("header").style.alignSelf = "center"; + + if (session.webcamonly == true) { + // mobile or manual flag 'webcam' pflag set + getById("head1").innerHTML = ""; + } else { + getById("head1").innerHTML = 'Please select an option to join.'; + } + + if (session.roomid.length > 0) { + if (session.videoDevice === 0) { + if (session.audioDevice === 0) { + miniTranslate(getById("add_camera"), "join-room", "Join Room"); + } else { + miniTranslate(getById("add_camera"), "join-room-with-mic", "Join Room with Microphone"); + } + } else if (session.audioDevice === 0) { + miniTranslate(getById("add_camera"), "join-room-with-camera", "Join Room with Camera"); + } else if (session.optionalMicOnly) { + miniTranslate(getById("add_camera"), "join-room-with-video", "Join Room with Video"); + miniTranslate(getById("add_microphone"), "join-room-with-mic-only", "Join Room with just Microphone"); + getById("container-3a").classList.remove("hidden"); + } else { + miniTranslate(getById("add_camera"), "join-room-with-camera", "Join Room with Camera"); + } + miniTranslate(getById("add_screen"), "share-screen-with-room", "Screenshare with Room"); + } else { + if (session.videoDevice === 0) { + miniTranslate(getById("add_camera"), "share-your-mic", "Share your Microphone"); + } else { + miniTranslate(getById("add_camera"), "share-your-camera", "Share your Camera"); + } + miniTranslate(getById("add_screen"), "share-your-screen", "Share your Screen"); + } + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + getById("container-2").title = getById("add_screen").innerText; + getById("container-3").title = getById("add_camera").innerText; + + if (session.scene !== false) { + getById("container-4").className = "column columnfade"; + getById("container-3").className = "column columnfade"; + getById("container-2").className = "column columnfade"; + getById("container-1").className = "column columnfade"; + getById("header").className = "hidden"; + getById("info").className = "hidden"; + getById("head1").className = "hidden"; + getById("head2").className = "hidden"; + getById("mainmenu").style.display = "none"; + getById("translateButton").style.display = "none"; + // getById("legal").style.display = "none"; + log("Update Mixer Event on REsize SET"); + window.onresize = updateMixer; + window.onorientationchange = function () { + setTimeout(updateMixer, 200); + }; + joinRoom(session.roomid); // this is a scene, so we want high resolutions + getById("main").style.overflow = "hidden"; + + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("hidden"); + getById("controlButtons").classList.remove("hidden"); + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("hidden"); + } + + if (session.hangupbutton === true) { + getById("controlButtons").classList.remove("hidden"); + getById("hangupbutton").classList.remove("hidden"); + getById("hangupbutton").className = "float"; + } + + } else if (session.permaid === null && session.roomid == "") { + if (!session.cleanOutput) { + // getById("head3").classList.remove('hidden'); + // getById("head3a").classList.remove('hidden'); + } + } else if (session.studioSoftware && !session.webcamonly && session.permaid === false && session.director === false && ((session.view!==false) || session.whepInput || session.whipView) && session.roomid.length > 0) { + // we already know roomid !== false + updateURL("scene", true, false); // we also know it's not a scene, but we will assume it is in this specific case. + session.scene = 0; + } else if (session.studioSoftware && !session.webcamonly && !session.cleanOutput && (session.permaid === false) && (session.director === false) && (session.view===false) && !session.whepInput && !session.whipView && (session.roomid.length > 0)) { + try { + getById("unexpectedPushLink").classList.remove("hidden"); + } catch (e) {} + } + } else if (session.director) { + // if I do a short form of this, it will cause duplications in the code elsewhere. + if (directorLanding == false) { + // implies director is not true or false, but a string + try { + var director_room_input = sanitizeRoomName(session.director); + log("director_room_input:" + director_room_input); + + if (urlParams.has("codirector") || urlParams.has("directorpassword") || urlParams.has("dirpass") || urlParams.has("dp")) { + session.directorPassword = urlParams.get("codirector") || urlParams.get("directorpassword") || urlParams.get("dirpass") || urlParams.get("dp"); + if (!session.directorPassword) { + window.focus(); + session.directorPassword = await promptAlt(getTranslation("enter-director-password"), true); + } else { + try { + session.directorPassword = decodeURIComponent(session.directorPassword); + } catch (e) {} + } + if (session.directorPassword) { + session.directorPassword = sanitizePassword(session.directorPassword); + await generateHash(session.directorPassword + session.salt + "abc123", 12) + .then(function (hash) { + // million to one error. + log("dir room hash is " + hash); + session.directorHash = hash; + return; + }) + .catch(errorlog); + } else { + session.directorPassword = false; + } + } + + setTimeout( + function (director_room_input) { + createRoom(director_room_input); + }, + 20, + director_room_input + ); + } catch (e) { + directorLanding = true; + session.director = true; + } + } + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("hidden"); + getById("controlButtons").classList.remove("hidden"); + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("hidden"); + } + } else if (((session.view!==false) || session.whepInput || session.whipView) && session.permaid === false) { + //if (!session.activeSpeaker){ + session.audioMeterGuest = false; + //} + if (session.style === false && session.studioSoftware) { + session.style = 1; + } + if (session.audioEffects === null) { + session.audioEffects = false; + } + log("Update Mixer Event on REsize SET"); + getById("translateButton").style.display = "none"; + // getById("legal").style.display = "none"; + window.onresize = updateMixer; + window.onorientationchange = function () { + setTimeout(function () { + updateMixer(); + }, 200); + }; + getById("main").style.overflow = "hidden"; + + if (session.chatbutton === true) { + getById("chatbutton").classList.remove("hidden"); + getById("controlButtons").classList.remove("hidden"); + } else if (session.chatbutton === false) { + getById("chatbutton").classList.add("hidden"); + } + + if (session.hangupbutton === true) { + getById("controlButtons").classList.remove("hidden"); + getById("hangupbutton").classList.remove("hidden"); + getById("hangupbutton").className = "float"; + } + } + + if (urlParams.has("nofileshare") || urlParams.has("nodownloads") || urlParams.has("nofiles")) { + session.hostedFiles = false; + session.nodownloads = true; + getById("sharefilebutton").style.display = "none"; + getById("sharefilebutton").classList.add("hidden"); + } else if (urlParams.has("fileshare") || urlParams.has("fs")) { + // + } else if (session.mobile) { + getById("sharefilebutton").style.display = "none"; + getById("sharefilebutton").classList.add("hidden"); + } else if (session.roomid == false) { + getById("sharefilebutton").style.display = "none"; + getById("sharefilebutton").classList.add("hidden"); + } else if (session.scene !== false) { + getById("sharefilebutton").style.display = "none"; + getById("sharefilebutton").classList.add("hidden"); + } else if (session.cleanOutput) { + getById("sharefilebutton").style.display = "none"; + getById("sharefilebutton").classList.add("hidden"); + } + + if (session.audioEffects === null) { + session.audioEffects = true; + } + + if (session.audioEffects) { + getById("channelGroup1").style.display = "block"; + getById("channelGroup2").style.display = "block"; + } + + if (urlParams.has("hidemenu") || urlParams.has("hm")) { + // needs to happen the room and permaid applications + getById("mainmenu").style.display = "none"; + getById("header").style.display = "none"; + getById("mainmenu").style.opacity = 0; + getById("header").style.opacity = 0; + } + + if (urlParams.has("hideusermenu") || urlParams.has("nousermenu") || urlParams.has("hum")) { + getById("advancedOptionsGeneral").classList.add("hidden"); + } + + if ((session.view!==false) || session.whepInput || session.whipView) { + getById("main").className = "main"; + getById("credits").style.display = "none"; + // getById("legal").style.display = "none"; + try { + if (session.label === false) { + if (document.title == "") { + document.title = "View=" + session.view.toString(); + } else { + document.title += ", View=" + session.view.toString(); + } + } + } catch (e) { + errorlog(e); + } + } + + if (urlParams.get("auth")) { + session.auth = urlParams.get("auth"); + } + + if (urlParams.has("waitimage")) { + session.waitImage = urlParams.get("waitimage") || false; + } + + if ((((session.view!==false) || session.whepInput || session.whipView) && session.roomid === false) || (session.waitImage && session.scene !== false)) { + getById("container-4").className = "column columnfade"; + getById("container-3").className = "column columnfade"; + getById("container-2").className = "column columnfade"; + getById("container-1").className = "column columnfade"; + //getById("header").className = 'hidden'; + getById("info").className = "hidden"; + getById("header").className = "hidden"; + getById("head1").className = "hidden"; + getById("head2").className = "hidden"; + getById("head3").classList.add("hidden"); + getById("head3a").classList.add("hidden"); + + getById("mainmenu").style.backgroundRepeat = "no-repeat"; + getById("mainmenu").style.backgroundPosition = "bottom center"; + getById("mainmenu").style.minHeight = "100%"; + getById("mainmenu").style.minWidth = "100%"; + getById("mainmenu").style.backgroundSize = "100px 100px"; + getById("mainmenu").innerHTML = ""; + getById("mainmenu").classList.remove("row"); + getById("mainmenu").style.display = "block"; + + if (urlParams.has("waittimeout")) { + session.waitImageTimeout = parseInt(urlParams.get("waittimeout")) || 0; + } + if (!session.fakeFeeds) { + session.waitImageTimeoutObject = setTimeout(function () { + session.waitImageTimeoutObject = true; + try { + if ((session.view!==false) || session.whepInput || session.whipView) { + if (document.getElementById("mainmenu")) { + if (session.waitImage) { + getById("mainmenu").innerHTML += ''; + getById("retryimage").src = decodeURIComponent(session.waitImage); + getById("retryimage").onerror = function () { + this.style.display = "none"; + }; + + if (session.cover) { + getById("retryimage").style.objectFit = "cover"; + } + } else if (!session.cleanOutput) { + getById("mainmenu").innerHTML += '
    '; + getById("retrySpinner").onclick = function () { + updateURL("cleanoutput"); + location.reload(); + }; + getById("retrySpinner").title = getTranslation("waiting-for-the-stream"); + } + if (urlParams.has("waitmessage")) { + getById("mainmenu").innerHTML += '
    '; + getById("retrymessage").innerText = urlParams.get("waitmessage"); + getById("retrySpinner").title = urlParams.get("waitmessage"); + } + } + } + } catch (e) { + errorlog(e); + } + }, session.waitImageTimeout); + } + + log("auto request videos"); + if ((iPad || iOS) && navigator.userAgent.indexOf("Safari") != -1 && navigator.userAgent.indexOf("Chrome") == -1 && SafariVersion > 13) { + // Modern iOS doesn't need pop up + play(); + } else if (navigator.userAgent.indexOf("Safari") != -1 && (navigator.userAgent.indexOf("Chrome") == -1 && navigator.userAgent.indexOf("Chromium") == -1)) { + // Safari on Desktop does require pop up + try { + navigator.mediaDevices + .getUserMedia({ + audio: true + }) + .then(function () { + closeModal(); + play(); + }) + .catch(function () { + play(); + }); + if (!session.cleanOutput) { + warnUser("Safari requires us to ask for an audio permission to use peer-to-peer technology. You will need to accept it in a moment if asked to view this live video", 20000); + } + } catch(e){ + errorlog(e); + play(); + } + } else { + // everything else is OK. + play(); + } + } else if (session.roomid) { + try { + if (session.label === false) { + if (document.title == "") { + document.title = "Room=" + session.roomid.toString(); + } else { + document.title += ": " + session.roomid.toString(); + } + } + } catch (e) { + errorlog(e); + } + } else { + try { + let reloadOldRoom = getStorage("directorOtherSettings"); + if (reloadOldRoom && reloadOldRoom.roomid) { + getById("lastSavedRoomName").innerText = reloadOldRoom.roomid; + getById("lastSavedRoom").classList.remove("hidden"); + getById("goToLastSavedRoom").onclick = function () { + createRoom(false, true); + }; + } + } catch (e) { + errorlog(e); + } + } + + hideHomeCheck(); + + setTimeout(function () { + for (var i in delayedStartupFuncs) { + var cb = delayedStartupFuncs[i]; + log(cb.slice(1)); + cb[0](...cb.slice(1)); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#A_better_apply + } + delayedStartupFuncs = []; + }, 50); + + if (session.effect == "3" || session.effect == "4" || session.effect == "5") { + attemptTFLiteJsFileLoad(); + } else if (session.effect == "6") { + loadTensorflowJS(); + } else if (session.effect == "9") { + session.effect = "sample"; + //loadEffect(session.effect); + warnUser("Loading custom effects model...", 1000); + } else if (session.effect == "10") { + session.effect = "dog"; + //loadEffect(session.effect); + warnUser("Loading custom effects model...", 1000); + } else if (session.effect == "11") { + session.effect = "anon"; + //loadEffect(session.effect); + warnUser("Loading custom effects model...", 1000); + } else if (session.effect == "12") { + session.effect = "sample"; + //loadEffect(session.effect); + warnUser("Loading custom effects model...", 1000); + } else if (session.effect == "facetracking") { + session.effect = "1"; + } else if (session.effect == "zoom") { + session.effect = "7"; + } + + if (session.effect === "3") { + getById("selectEffectAmount").style.display = "block"; + getById("selectEffectAmount3").style.display = "block"; + getById("selectEffectAmountInput").value = session.effectValue; + getById("selectEffectAmountInput3").value = session.effectValue; + } else if ((session.effect === "14" || session.effect === "15") && session.effectValue_default == false){ + getById("selectEffectAmount").style.display = "block"; + getById("selectEffectAmount3").style.display = "block"; + + if (session.effectValue_default) { + session.effectValue = parseFloat(session.effectValue_default); + } else { + session.effectValue = parseFloat(session.effectValue); + } + + + getById("selectEffectAmountInput").min = 1; + getById("selectEffectAmountInput").max = 50; + getById("selectEffectAmountInput").step = 1; + getById("selectEffectAmountInput3").min = 1; + getById("selectEffectAmountInput3").max = 50; + getById("selectEffectAmountInput3").step = 1; + + getById("selectEffectAmountInput").value = session.effectValue; + getById("selectEffectAmountInput3").value = session.effectValue; + + + } else if (session.effect === "7") { + getById("selectEffectAmount").style.display = "block"; + getById("selectEffectAmount3").style.display = "block"; + if (session.effectValue_default) { + session.effectValue = session.effectValue_default; + } else { + session.effectValue = 1; + } + getById("selectEffectAmountInput").min = 1; + getById("selectEffectAmountInput").max = 3.99; + getById("selectEffectAmountInput").step = 0.01; + getById("selectEffectAmountInput3").min = 1; + getById("selectEffectAmountInput3").max = 3.99; + getById("selectEffectAmountInput3").step = 0.01; + + getById("selectEffectAmountInput").value = session.effectValue; + getById("selectEffectAmountInput3").value = session.effectValue; + + // Show zoom position controls + getById("zoomPositionControls").style.display = "block"; + getById("zoomPositionControls3").style.display = "block"; + } + + if (session.sensorData) { + setupSensorData(parseInt(session.sensorData)); + } + if (session.externalSensorBridge) { + setupExternalSensorBridge(); + } + + if (location.protocol !== "https:") { + try { + //if (!session.cleanOutput) { + if (["127.0.0.1", "localhost"].includes(window.location.hostname)){ + // these are allowed I believe. I do change the salt however to the default one though. + } else if (window.location.host.split(".")[0] !== "insecure") { + console.warn("⚠️ SSL (https) is not enabled. This site will not fully work without it!

    Try accessing the site from here instead.", false, false); + } + //} + } catch (e) {} + } else { + try { + if (navigator && navigator.mediaDevices && navigator.mediaDevices.ondevicechange) { + navigator.mediaDevices.ondevicechange = reconnectDevices; + } + } catch (e) { + errorlog(e); + } + } + + if (session.autohide && session.scene === false) { + // && (session.roomid!==false)){ + try { + getById("main").onmouseover = showControl; // this is correct. (it's not session.showControls) + document.ontouchstart = showControl; // this is correct. (it's not session.showControls) + getById("gridlayout").classList.add("nocontrolbar"); + if (session.autostart) { + showControl(); + } + } catch (e) {} + } + + if (urlParams.has("experimental")) { + session.experimental = true; + } + + if (urlParams.has("flagship")) { + session.flagship = true; + } + //if (!session.flagship && session.mobile && (session.limitTotalBitrate===false)){ + // session.limitTotalBitrate = session.totalRoomBitrate_default; // 500, with the max per guest stream out at maxMobileBitrate (350kbps) or 35-kbps if more than X in the room. + //} + + if (urlParams.has("maxmobilebitrate")) { + session.maxMobileBitrate = parseInt(urlParams.has("maxmobilebitrate")) || 0; + } + if (urlParams.has("lowmobilebitrate")) { + session.lowMobileBitrate = parseInt(urlParams.has("lowmobilebitrate")) || 0; + } + + // Please contact steve on discord.vdo.ninja if you'd like this iFRAME tweaked, expanded, etc -- it's updated based on user request + + session.remoteInterfaceAPI = function (e) { + // iFRAME api support + if (!e.data || typeof e.data !== "object") { + warnlog(e); + return; + } + // Ignore framegrab-audio-settings - handled by lib.js message listener + if (e.data.action === "framegrab-audio-settings") { + return; + } + log(e); + try { + if ("function" in e.data) { + // these are calling in-app functions, with perhaps a callback -- TODO: add callbacks + var ret = null; + if (e.data.function === "previewWebcam") { + ret = previewWebcam(); + } else if (e.data.function === "changeHTML") { + ret = getById(e.data.target); + ret.innerHTML = e.data.value; + } else if (e.data.function === "publishScreen") { + ret = publishScreen(); + } else if (e.data.function === "targetGuest") { + ret = targetGuest(data.target, data.action, data.value); + } else if (e.data.function === "commands" && data.action && Commands[data.action]) { + ret = Commands[data.action](data.value, data.value2 || null); + } else if (e.data.function === "routeMessage") { + try { + session.ws.onmessage({ data: e.data.value }); + } catch (e) { + warnlog("handshake not yet setup"); + } + } else if (e.data.function === "eval") { + eval(e.data.value); // eval == evil ; feedback welcomed ; + } + } + } catch (err) { + errorlog(err); + } + + if ("sendData" in e.data) { + // send generic data via p2p. Send whatever you want I guess; there is a max chunk size of course. Use filetransfer for large files? + var UUID = false; + var streamID = false; + var type = false; + if (e.data.UUID) { + UUID = e.data.UUID; + } else if (e.data.streamID) { + streamID = e.data.streamID; + } + if (e.data.type) { + type = e.data.type; + } + var ret = session.sendGenericData(e.data.sendData, UUID, streamID, type); // comes out the other side as: ("dataReceived", data, UUID); + if (!ret) { + warnlog("Not connected yet or no peers available"); + } + return; + } + + if ("PPT" in e.data) { + log("PTT activated-webmain"); + if (e.data.PPT === true) { + // unmute + session.muted = false; // set + getById("mutebutton").classList.add("PPTActive"); + log(session.muted); + toggleMute(true); // apply + } else if (e.data.PPT === false) { + // mute + session.muted = true; // set + getById("mutebutton").classList.remove("PPTActive"); + log(session.muted); + toggleMute(true); // apply + } else if (e.data.PPT === "toggle") { + // toggle + toggleMute(); + } + return; // this is a high-load call, so lets skip the rest of the checks to save cpu. + } + + if ("sendChat" in e.data) { + sendChat(e.data.sendChat); // sends to all peers; more options down the road + return; + } + // Chat out gets called via getChatMessage function + // Related code: parent.postMessage({"chat": {"msg":-----,"type":----,"time":---} }, "*"); + + // session.requestResolution(vid.dataset.UUID, wrw*window.devicePixelRatio, hrh*window.devicePixelRatio); + + if ("mic" in e.data) { + // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. + if (e.data.mic === true) { + // unmute + session.muted = false; // set + log(session.muted); + toggleMute(true); // apply + } else if (e.data.mic === false) { + // mute + session.muted = true; // set + log(session.muted); + toggleMute(true); // apply + } else if (e.data.mic === "toggle") { + // toggle + toggleMute(); + } + } + + if ("toggleSettings" in e.data) { + // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. + + if (e.data.toggleSettings && !toggleSettingsState) { + toggleSettings(); + } else if (e.data.toggleSettings == "toggle") { + toggleSettings(); + } else if (toggleSettingsState) { + toggleSettings(); + } + } + + if ("camera" in e.data) { + // this should work for the director's mic mute button as well. Needs to be manually enabled the first time still tho. + if (e.data.camera === true) { + // unmute + session.videoMuted = false; // set + log(session.videoMuted); + toggleVideoMute(true); // apply + } else if (e.data.camera === false) { + // mute + session.videoMuted = true; // set + log(session.videoMuted); + toggleVideoMute(true); // apply + } else if (e.data.camera === "toggle") { + // toggle + toggleVideoMute(); + } + } + + if ("pauseinvisible" in e.data) { // whether videos hidden in the update mixer are muted or not, based on if they are visible or not. + if (e.data.pauseinvisible === "toggle") { + // toggle + session.pauseInvisible = !session.pauseInvisible; + } else if (!e.data.pauseinvisible) { + session.pauseInvisible = false; + } else { + session.pauseInvisible = true; + } + updateMixer(); + } + + if ("keyframe" in e.data) { + session.sendKeyFrameScenes(); + } + + if ("groups" in e.data) { + if (typeof e.data.groups == "object") { + session.group = e.data.groups || []; + } else if (!e.data.group) { + session.group = []; + } else { + session.group = e.data.groups.split(","); + } + var eleGroup = getById("groups"); + eleGroup.querySelectorAll('[data-action-type="toggle-group"][data-group]').forEach(group => { + if (!(session.group && session.group.includes(group))) { + group.remove("green"); + } + }); + + if (session.group) { + session.group.forEach(group => { + var ele = eleGroup.querySelector('[data-action-type="toggle-group"][data-group="' + group + '"'); + if (!ele) { + ele = document.createElement("div"); + ele.dataset.actionType = "toggle-group"; + ele.dataset.group = group; + ele.classList.add("float"); + ele.style.display = "inline-block"; + ele.role = "button"; + ele.innerHTML = '
    ' + group; + eleGroup.appendChild(ele); + ele.onclick = function () { + changeGroupDirectorAPI(this.dataset.group); + }; + } + ele.classList.add("green"); + }); + } + + updateMixer(); + + if (session.group.length || session.allowNoGroup) { + session.sendMessage({ group: session.group.join(",") }); + if (session.screenShareState && session.screenshareType === 3) { + session.sendMessage({ group: session.group.join(","), altUUID: true }); + } + } else { + session.sendMessage({ group: false }); + if (session.screenShareState && session.screenshareType === 3) { + session.sendMessage({ group: false, altUUID: true }); + } + } + } + + if (e.data.getSnapshotBySlot || e.data.getSnapshotByStreamID) { + let videoElement = false; + + let streamID = ("getSnapshotBySlot" in e.data) ? session.currentSlots[parseInt(e.data.getSnapshotBySlot) || 0] : e.data.getSnapshotByStreamID; + + let UUID = false; + if (streamID){ + for (let i in session.rpcs) { + if (session.rpcs[i].streamID == streamID) { + UUID = i; + videoElement = session.rpcs[i].videoElement; + break; + } + } + } + + if (streamID && videoElement && videoElement.srcObject) { + const videoTrack = videoElement.srcObject.getVideoTracks()[0]; + + if (videoTrack) { + const processor = new MediaStreamTrackProcessor({ track: videoTrack }); + const reader = processor.readable.getReader(); + + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + + try { + reader.read().then(({ done, value: frame }) => { + if (!done && frame) { + canvas.width = frame.displayWidth; + canvas.height = frame.displayHeight; + ctx.drawImage(frame, 0, 0); + + const format = typeof session.sendframes === "string" ? session.sendframes : "png"; + const imageData = canvas.toDataURL(`image/${format}`, 0.8); + + parent.postMessage({ + type: 'frame', + frame: imageData, + UUID: UUID, + streamID: streamID, + trackID: videoTrack.id, + kind: videoTrack.kind, + format: format, + slot: parseInt(Object.keys(session.currentSlots).find(key => session.currentSlots[key] === streamID)) || 0, + cib: e.data.cib || null + }, session.iframetarget); + + // Proper cleanup + frame.close(); + reader.cancel(); + + // Remove canvas from DOM if it was added + if (canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + } + }).catch(error => { + console.error("Error processing image frame:", error); + }); + } catch (error) { + console.error("Error setting up frame capture:", error); + } + } + } + } + + if ("groupView" in e.data) { + if (typeof e.data.groupView == "object") { + session.groupView = e.data.groupView || []; + } else if (!e.data.groupView) { + session.groupView = []; + } else { + session.groupView = e.data.groupView.split(","); + } + updateMixer(); + } + + if ("mute" in e.data) { + if (e.data.mute === true) { + // unmute + session.speakerMuted = true; // set + toggleSpeakerMute(true); // apply + } else if (e.data.mute === false) { + // mute + session.speakerMuted = false; // set + toggleSpeakerMute(true); // apply + } else if (e.data.mute === "toggle") { + // toggle + toggleSpeakerMute(); + } + } else if ("speaker" in e.data) { + // same thing as mute. + if (e.data.speaker === true) { + // unmute + session.speakerMuted = false; // set + toggleSpeakerMute(true); // apply + } else if (e.data.speaker === false) { + // mute + session.speakerMuted = true; // set + toggleSpeakerMute(true); // apply + } else if (e.data.speaker === "toggle") { + // toggle + toggleSpeakerMute(); + } + } + + if ("record" in e.data) { + if (e.data.record == false) { + // mute + if ("recording" in session.videoElement) { + recordLocalVideo("stop"); + } + } else if (e.data.record === true) { + if ("recording" in session.videoElement) { + // already recording + } else { + recordLocalVideo("start"); + } + } else if (e.data.record) { + var video = document.getElementById(e.data.record); + if (video) { + var videoKbps = 4000; + if (session.recordLocal !== false) { + videoKbps = session.recordLocal; + } + recordLocalVideo(null, videoKbps, video); + } + } + } + + if ("volume" in e.data) { + // might not work with iframes or meshcast currently. + var requestedVolume = parseFloat(e.data.volume) || 0; + if (requestedVolume > 1.0) { + // this is a bit quasi improper. But the API is official 0 to 1.0; not 0 to 100, so this is mainly a catch for those not using the API right. + requestedVolume = requestedVolume / 100.0; + } + setSessionPlaybackVolume(requestedVolume, e.data.target); + } + + if ("enableYouTube" in e.data) { + // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. + if (typeof e.data.enableYouTube == "string") { + session.youtubeKey = e.data.enableYouTube; + } else if (!session.youtubeKey) { + errorlog("No Youtube Key provided"); + } + console.log(session.youtubeKey); + YoutubeChatInterface(true); + } + + if ("nextSlide" in e.data) { + // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. + nextSlide(); + } + + if ("prevSlide" in e.data) { + // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. + gobackSlide(); + } + + if ("panning" in e.data) { + // panning adjusts the stereo pan , although current its UUID based. can add stream ID based if requested. + if ("UUID" in e.data) { + try { + adjustPan(UUID, e.data.panning); + } catch (e) { + errorlog(e); + } + } else { + for (var i in session.rpcs) { + try { + adjustPan(i, e.data.panning); + } catch (e) { + errorlog(e); + } + } + } + } + + if ("targetBitrate" in e.data || "targetAudioBitrate" in e.data) { + // this sets the fundemental bitrate target, but does not necessarily "lock" . + + var msg = {}; + if ("targetBitrate" in e.data) { + msg.targetBitrate = e.data.targetBitrate; + } + if ("targetAudioBitrate" in e.data) { + msg.targetAudioBitrate = e.data.targetAudioBitrate; + } + if (e.data.requestAs) { + msg.requestAs = e.data.requestAs; + } + if (e.data.remote) { + msg.remote = e.data.remote; + } + for (var i in session.rpcs) { + try { + if ("streamID" in session.rpcs[i]) { + if ("target" in e.data) { + if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { + // specify a stream ID or let it apply to all videos + session.sendRequest(msg, i); + } + } else if (e.data.UUID && e.data.UUID === i) { + session.sendRequest(msg, i); + } else if (e.data.streamID) { + if (session.rpcs[i].streamID == e.data.streamID) { + // specify a stream ID or let it apply to all videos + session.sendRequest(msg, i); + } + } else { + session.sendRequest(msg, i); // bitrate = 0 pauses the video + } + } + } catch (e) { + errorlog(e); + } + } + } + + if ("manualBitrate" in e.data) { + for (var i in session.rpcs) { + try { + if ("streamID" in session.rpcs[i]) { + if ("target" in e.data) { + if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { + // specify a stream ID or let it apply to all videos + session.rpcs[i].manualBandwidth = e.data.manualBitrate; + session.requestRateLimit(false, i); + } + } else if (e.data.UUID && e.data.UUID === i) { + session.rpcs[i].manualBandwidth = e.data.manualBitrate; + session.requestRateLimit(false, i); + } else if (e.data.streamID) { + if (session.rpcs[i].streamID == e.data.streamID) { + // specify a stream ID or let it apply to all videos + session.rpcs[i].manualBandwidth = e.data.manualBitrate; + session.requestRateLimit(false, i); + } + } else { + session.rpcs[i].manualBandwidth = e.data.manualBitrate; + session.requestRateLimit(false, i); + } + } + } catch (e) { + errorlog(e); + } + } + } + + if ("bitrate" in e.data) { + /// set a video bitrate for a video; scene or view link; kbps + var lock = true; + if ("lock" in e.data) { + // since this is the iframe API, we're going to assume the default is manual over-ride. VDO.Ninja's automixer logic won't override a locked bitrate. + lock = e.data.lock; + } + for (var i in session.rpcs) { + try { + if ("streamID" in session.rpcs[i]) { + // we only target publishers with this call + if ("target" in e.data) { + if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { + // specify a stream ID or let it apply to all videos + session.requestRateLimit(e.data.bitrate, i, false, lock); + } + } else if (e.data.UUID && e.data.UUID === i) { + session.requestRateLimit(e.data.bitrate, i, false, lock); + } else if (e.data.streamID) { + if (session.rpcs[i].streamID == e.data.streamID) { + // specify a stream ID or let it apply to all videos + session.requestRateLimit(e.data.bitrate, i, false, lock); + } + } else { + session.requestRateLimit(e.data.bitrate, i, false, lock); // bitrate = 0 pauses the video + } + } + } catch (e) { + errorlog(e); + } + } + } + + if ("audiobitrate" in e.data) { + // changes the audio bitrate of a specific or all inbound media tracks. kbps + var lock = true; + if ("lock" in e.data) { + // since this is the iframe API, we're going to assume the default is manual over-ride. VDO.Ninja's automixer logic won't override a locked bitrate. + lock = e.data.lock; + } + for (var i in session.rpcs) { + try { + if ("streamID" in session.rpcs[i]) { + // we only target publishers with this call + if ("target" in e.data) { + if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { + // specify a stream ID or let it apply to all videos + session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); + } + } else if (e.data.UUID && e.data.UUID === i) { + session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); + } else if (e.data.streamID) { + if (session.rpcs[i].streamID == e.data.streamID) { + // specify a stream ID or let it apply to all videos + session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); + } + } else { + session.requestAudioRateLimit(parseInt(e.data.bitrate), i, lock); // bitrate = 0 pauses the video + } + } + } catch (e) { + errorlog(e); + } + } + } + + if ("changeVideoDevice" in e.data) { + warnlog(e.data.changeVideoDevice); + changeVideoDevice(e.data.changeVideoDevice); + } + + if ("changeAudioDevice" in e.data) { + warnlog(e.data.changeAudioDevice); + changeAudioDevice(e.data.changeAudioDevice); + } + + if ("changeAudioOutputDevice" in e.data) { + warnlog(e.data.changeAudioOutputDevice); + changeAudioOutputDeviceById(e.data.changeAudioOutputDevice); + } + + if ("getDeviceList" in e.data) { + // get a list of local camera / audio devices + warnlog(e.data.getDeviceList); + enumerateDevices().then(function (deviceInfos) { + parent.postMessage( + { + deviceList: JSON.parse(JSON.stringify(deviceInfos)), + cib: e.data.cib || null + }, + session.iframetarget + ); + }); + } + + if ("sceneState" in e.data) { + // TRUE OR FALSE - tells the connected peers if they are live or not via a tally light change. + var msg = {}; + msg.obsState = {}; + msg.obsState.visibility = e.data.sceneState || false; + msg.obsState.recording = e.data.sceneState || false; + session.sendRequest(msg); + } + + if ("layouts" in e.data) { + session.layouts = e.data.layouts; + if ("obsSceneTriggers" in e.data) { + session.obsSceneTriggers = e.data.obsSceneTriggers; + } else { + session.obsSceneTriggers = false; + } + for (var uid in session.pcs) { + if (session.pcs[uid].layout) { + session.sendMessage(e.data, uid); + } + } + // session.obsSceneSync(); // not sure I need to trigger this? + log(e.data); + } + + if ("sendMessage" in e.data) { + // webrtc send to viewers + session.sendMessage(e.data.sendMessage); + } + + if ("sendRequest" in e.data) { + // webrtc send to publishers + session.sendRequest(e.data.sendRequest); + } + + if ("sendRawMIDI" in e.data) { + // webrtc send to publishers + //var msg = {}; + //msg.midi = {}; + //msg.midi.d = e.data.sendRawMIDI.data; aka [d1,d2,d3]; + //msg.midi.c = e.data.sendRawMIDI.channel; + //msg.midi.s = e.data.sendRawMIDI.timestamp; + // e.data.UUID or e.data.streamID or leave empty to send to all + if ("UUID" in e.data) { + sendRawMIDI(e.data.sendRawMIDI, e.data.UUID); // send to connection + } else if (e.data.streamID) { + sendRawMIDI(e.data.sendRawMIDI, false, e.data.streamID); // send to connection + } else { + sendRawMIDI(e.data.sendRawMIDI); // send to all + } + return; // make it send faster. + } + + if ("sendPeers" in e.data) { + // webrtc send message to every connected peer; like send and request; a hammer vs a knife. + session.sendPeers(e.data.sendPeers); + } + + if ("reload" in e.data) { + // reload the page + reloadRequested(); // location.reload();, but with no user prompt (force reload) + } + + if ("getFaces" in e.data) { + if (e.data.faceTrack) { + session.grabFaceData = true; + getFaces(); + } else { + session.grabFaceData = false; + } + } + + if ("getFreshStats" in e.data) { + // takes a second to query. + var stats = {}; + try { + stats.inbound = {}; + stats.total_outbound_connections = Object.keys(session.pcs).length; + stats.total_inbound_connections = Object.keys(session.rpcs).length; + for (var i in session.rpcs) { + stats.inbound[session.rpcs[i].streamID] = session.rpcs[i].stats; + } + for (var uuid in session.pcs) { + setTimeout( + function (UUID) { + session.pcs[UUID].getStats().then(function (stats) { + stats.forEach(stat => { + if (stat.id && stat.id.startsWith("DEPRECATED_")) { + return; + } + + if (stat.type == "outbound-rtp") { + if (stat.kind == "video") { + if ("qualityLimitationReason" in stat) { + session.pcs[UUID].stats.quality_limitation_reason = stat.qualityLimitationReason; + } + if ("framesPerSecond" in stat) { + session.pcs[UUID].stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond; + } + if ("encoderImplementation" in stat) { + session.pcs[UUID].stats.encoder = stat.encoderImplementation; + } + } + } else if (stat.type == "remote-candidate") { + if ("relayProtocol" in stat) { + if ("ip" in stat) { + session.pcs[UUID].stats.remote_relay_IP = stat.ip; + } + session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol; + } + if ("candidateType" in stat) { + session.pcs[UUID].stats.candidateType_remote = stat.candidateType; + } + } else if (stat.type == "local-candidate") { + if ("relayProtocol" in stat) { + if ("ip" in stat) { + session.pcs[UUID].stats.local_relayIP = stat.ip; + } + session.pcs[UUID].stats.local_relayProtocol = stat.relayProtocol; + } + if ("candidateType" in stat) { + session.pcs[UUID].stats.candidateType_local = stat.candidateType; + } + } else if (stat.type == "candidate-pair" && stat.nominated) { + if ("availableOutgoingBitrate" in stat) { + session.pcs[UUID].stats.available_outgoing_bitrate_kbps = parseInt(stat.availableOutgoingBitrate / 1024); + } + if ("totalRoundTripTime" in stat) { + if ("responsesReceived" in stat) { + session.pcs[UUID].stats.average_roundTripTime_ms = parseInt((stat.totalRoundTripTime / stat.responsesReceived) * 1000); + } + } + } + return; + }); + return; + }); + }, + 0, + uuid + ); + } + } catch (e) { + // disconnected probably + } + setTimeout(function () { + stats.outbound_stats = {}; + try { + for (var i in session.pcs) { + stats.outbound_stats[i] = session.pcs[i].stats; + } + } catch (e) {} + parent.postMessage( + { + stats: stats, + cib: e.data.cib || null + }, + session.iframetarget + ); + }, 1000); + } + + if ("getStats" in e.data) { + if (e.data.streamID) { + parent.postMessage( + { + stats: getQuickStats(e.data.streamID), + cib: e.data.cib || null + }, + session.iframetarget + ); + } else { + parent.postMessage( + { + stats: getQuickStats(), + cib: e.data.cib || null + }, + session.iframetarget + ); + } + } + + if ("getRemoteStats" in e.data) { + if (session.remote) { + session.sendRequest({ requestStats: true, remote: session.remote }); + } else { + session.sendRequest({ requestStats: true }); + } + } + + if ("requestStatsContinuous" in e.data) { + if (session.remote) { + session.sendRequest({ requestStatsContinuous: e.data.requestStatsContinuous, remote: session.remote }); + } else { + session.sendRequest({ requestStatsContinuous: e.data.requestStatsContinuous }); + } + } + + if ("getLoudness" in e.data) { + log("GOT LOUDNESS REQUEST"); + if (e.data.getLoudness == true) { + if (!session.pushLoudness && session.audioEffects !== true) { + session.pushLoudness = true; + for (var i in session.rpcs) { + updateIncomingAudioElement(i); // this can be called when turning on/off inbound audio processing. + } + } else { + session.pushLoudness = true; + } + + var loudness = {}; + for (var i in session.rpcs) { + loudness[session.rpcs[i].streamID] = session.rpcs[i].stats.Audio_Loudness; + } + + parent.postMessage( + { + loudness: loudness, + cib: e.data.cib || null + }, + session.iframetarget + ); + } else { + if (session.pushLoudness && !session.audioEffects) { + // turn off audio processing + session.pushLoudness = false; + for (var i in session.rpcs) { + updateIncomingAudioElement(i); + } + } else { + session.pushLoudness = false; // can't turn off audio processing + } + } + } + + if ("getEffectsData" in e.data) { + log("GOT getEffects Data REQUESTed"); // face tracking info, etc. + if (e.data.getEffectsData !== false) { + session.pushEffectsData = e.data.getEffectsData; // which effect do you want the data from? it won't enable the effect necessarily; just the ML pipeline + + //parent.postMessage({ + // "effectsData": effectsData, + // "effectsID": session.pushEffectsData + //}, session.iframetarget); + } else { + session.pushEffectsData = false; + } + } + + if ("getStreamIDs" in e.data) { + // get a list of stream Ids, with a label if it is present. label = false if not there + if (e.data.getStreamIDs) { + var streamIDs = {}; + for (var i in session.rpcs) { + streamIDs[session.rpcs[i].streamID] = session.rpcs[i].label; + } + parent.postMessage( + { + streamIDs: streamIDs, + cib: e.data.cib || null + }, + session.iframetarget + ); + } + } + + if ("getStreamInfo" in e.data) { + // get a list of stream Ids, with a label if it is present. label = false if not there + try { + var UUIDS = {}; + for (var i in session.rpcs) { + UUIDS[i] = {}; + UUIDS[i].label = session.rpcs[i].label || false; + UUIDS[i].streamID = session.rpcs[i].streamID || false; + if (session.rpcs[i].stats && session.rpcs[i].stats.info) { + UUIDS[i].info = session.rpcs[i].stats.info; + } else { + UUIDS[i].info = {}; + } + } + parent.postMessage( + { + streamInfo: UUIDS, + cib: e.data.cib || null + }, + session.iframetarget + ); + } catch (e) { + errorlog(e); + } + } + + if ("close" in e.data || "hangup" in e.data) { + // disconnect and hangup all inbound streams. + var tmp = e.data.close || e.data.hangup; + if (tmp == "estop") { + // try to stop the video recording even if not complete; if you can't wait even ms before a reload/exit. + console.log("ESTOP"); + session.hangup(false, true); + } else if (tmp == "reload") { + // stop and reload the page safely. + session.hangup(true); + } else { + // just hangup, but can take up to 1-second to do so fully. + session.hangup(); + } + } + //if ("hangup" in e.data) { + // disconnect and hangup all inbound streams. + // session.hangup(); + //} + + if ("style" in e.data) { + // insert a custom style sheet + try { + const style = document.createElement("style"); + style.textContent = e.data.style; + document.head.append(style); + log(style); + } catch (e) { + errorlog(e); + } + } + + if ("getDetailedState" in e.data) { + var detailedState = getDetailedState(); + parent.postMessage( + { + detailedState: detailedState, + cib: e.data.cib || null + }, + session.iframetarget + ); + } + + if ("getGuestList" in e.data) { + var guestList = getGuestList(); + parent.postMessage( + { + guestList: guestList, + cib: e.data.cib || null + }, + session.iframetarget + ); + } + + // saveVideoFrameToDisk(video); + + if ("saveVideoFrameToDisk" in e.data) { + let filename = false; + if ("filename" in e.data) { + filename = e.data.filename; + if (filename.split(".").length == 1) { + filename += ".png"; + } + } + if ("streamID" in e.data) { + let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); + if (session.rpcs[UUID]) { + if (session.rpcs[UUID].videoElement) { + saveVideoFrameToDisk(session.rpcs[UUID].videoElement, false, filename); + } else { + errorlog("The specified video does not exist"); + } + } else { + errorlog("The stream ID specified does not exist"); + } + } else if ("UUID" in e.data) { + if (e.data.UUID === "*") { + for (var uuid in session.rpcs) { + if (session.rpcs[uuid].videoElement) { + saveVideoFrameToDisk(session.rpcs[uuid].videoElement, false, filename); + } + } + } else if (session.rpcs[e.data.UUID]) { + if (session.rpcs[e.data.UUID].videoElement) { + saveVideoFrameToDisk(session.rpcs[e.data.UUID].videoElement, false, filename); + } else { + errorlog("The specified video does not exist"); + } + } else { + errorlog("The UUID specified does not exist"); + } + } else if (session.videoElement) { + saveVideoFrameToDisk(session.videoElement, false, filename); + } + } + + if ("getVideoFrame" in e.data) { + if ("streamID" in e.data) { + let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); + if (session.rpcs[UUID]) { + if (session.rpcs[UUID].videoElement) { + sendVideoFrameToIframe(session.rpcs[UUID].videoElement, false, e.data); + } else { + errorlog("The specified video does not exist"); + } + } else { + errorlog("The stream ID specified does not exist"); + } + } else if ("UUID" in e.data) { + if (session.rpcs[e.data.UUID]) { + if (session.rpcs[e.data.UUID].videoElement) { + sendVideoFrameToIframe(session.rpcs[e.data.UUID].videoElement, false, e.data); + } else { + errorlog("The specified video does not exist"); + } + } else { + errorlog("The UUID specified does not exist"); + } + } else if (session.videoElement) { + sendVideoFrameToIframe(session.videoElement, false, e.data); + } + } + + if ("copyVideoFrameToClipboard" in e.data) { + if ("streamID" in e.data) { + let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); + if (session.rpcs[UUID]) { + if (session.rpcs[UUID].videoElement) { + copyVideoFrameToClipboard(session.rpcs[UUID].videoElement, false); + } else { + errorlog("The specified video does not exist"); + } + } else { + errorlog("The stream ID specified does not exist"); + } + } else if ("UUID" in e.data) { + if (session.rpcs[e.data.UUID]) { + if (session.rpcs[e.data.UUID].videoElement) { + copyVideoFrameToClipboard(session.rpcs[e.data.UUID].videoElement, false); + } else { + errorlog("The specified video does not exist"); + } + } else { + errorlog("The UUID specified does not exist"); + } + } else if (session.videoElement) { + copyVideoFrameToClipboard(session.videoElement, false); + } + } + + if ("setBufferDelay" in e.data) { + // milliseconds + console.log(e.data); + let delay = parseInt(e.data.setBufferDelay) || 0; + + if ("streamID" in e.data) { + let UUID = Object.keys(session.rpcs).find(uuid => session.rpcs[uuid].streamID === e.data.streamID); + if (session.rpcs[UUID]) { + session.rpcs[UUID].buffer = delay; + playoutdelay(UUID); + } else { + errorlog("The stream ID specified does not exist"); + } + document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + UUID + '"] input[data-buffer-value]').forEach(ele => { + ele.value = delay; + }); + } else if ("label" in e.data) { + for (let uuid in session.rpcs) { + if (session.rpcs[uuid].label && session.rpcs[uuid].label === e.data.label) { + if (session.rpcs[uuid]) { + session.rpcs[uuid].buffer = delay; + playoutdelay(uuid); + } else { + errorlog("The label specified does not exist"); + } + } + } + document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + UUID + '"] input[data-buffer-value]').forEach(ele => { + ele.value = delay; + }); + } else if ("UUID" in e.data) { + if (e.data.UUID === "*") { + for (var uuid in session.rpcs) { + session.rpcs[uuid].buffer = delay; + playoutdelay(uuid); + document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + uuid + '"] input[data-buffer-value]').forEach(ele => { + ele.value = delay; + }); + } + } else if (session.rpcs[e.data.UUID]) { + session.rpcs[e.data.UUID].buffer = delay; + playoutdelay(e.data.UUID); + document.querySelectorAll('#bufferSettings[data--u-u-i-d="' + e.data.UUID + '"] input[data-buffer-value]').forEach(ele => { + ele.value = delay; + }); + } else { + errorlog("The UUID specified does not exist"); + } + } else { + session.buffer = delay; // set the default buffer delay only + } + } + + if ("automixer" in e.data) { + // stop the auto mixer if you want to control the layout and bitrate yourself + if (e.data.automixer == true) { + session.manual = session.manual === null ? false : session.manual; + try { + updateMixer(); + } catch (e) {} + } else if (e.data.automixer == false) { + session.manual = session.manual === null ? true : session.manual; + } + } + + if ("advancedMode" in e.data) { + if (e.data.advancedMode) { + // Un-hiding advanced items + document.querySelectorAll(".advanced").forEach(element => { + element.classList.remove("hide"); + }); + } else { + // Hiding advanced items + document.querySelectorAll(".advanced").forEach(element => { + element.classList.add("hide"); + }); + } + } + + if ("requestStream" in e.data) { + if (e.data.requestStream) { + // load a specific stream ID + log("requestStream iframe api"); + session.requestStream(e.data.requestStream); + } // don't use if the stream is in your room (as not needed) + } // you can load a stream ID from inside a room that exists outside any room + + if ("layout" in e.data) { + if (Array.isArray(e.data.layout)){ + session.layout_array = e.data.layout; + session.layout = combinedLayout(session.layout_array); + } else { + session.layout_array = null; + session.layout = e.data.layout; + } + // update mixer is run later, some lines down. + pokeIframeAPI("layout-updated", session.layout, null, null, e.data.cib); + } + + if ("previewMode" in e.data) { + switchModes(e.data.previewMode); + } else if ("layout" in e.data) { + warnlog("changing layout request via IFRAME API"); + if (e.data.obsCommand) { + issueLayoutOBS(e.data); + } else if (session.director) { // only a director can issue a layout + if ("scene" in e.data) { + if ("UUID" in e.data) { + issueLayout(e.data.scene, e.data.UUID); + } else { + issueLayout(e.data.scene); + } + } else if ("UUID" in e.data) { + issueLayout(false, e.data.UUID); + } + } + updateMixer(); + } else if (e.data.obsCommand) { + var msg = {}; + msg.obsCommand = e.data.obsCommand; + if (e.data.remote) { + msg.remote = e.data.remote; + } else { + msg.remote = session.remote; + } + if (e.data.UUID) { + msg.UUID = e.data.UUID; + } + if (e.data.streamID) { + msg.streamID = e.data.streamID; + } + session.encodeRemote(msg).then(msgx => { + session.sendMessage(msgx); // just to be safe; avoids spamming of wss + log(msgx); + }); + } + + if ("slotmode" in e.data) { + if (session.slotmode) { + session.slotmode = parseInt(e.data.slotmode); + populateSlotPicker(); + } else { + session.slotmode = false; + } + } + + //////////// manual scale. Request a specific down-scaled resolution from a remote connection + var targetWidth = false; + var targetHeight = false; + if ("targetWidth" in e.data) { + targetWidth = e.data.targetWidth || 0; + } + if ("targetHeight" in e.data) { + targetHeight = e.data.targetHeight || 0; + } + // session.viewheight or session.viewwidth + if ((targetWidth || targetHeight) && e.data.UUID) { + var requestAs = false; + if (e.data.requestAs) { + requestAs = e.data.requestAs; + } + session.requestResolution(e.data.UUID, targetWidth || 4096, targetHeight || 2160, false, requestAs); // this is fine. + } + //////////////// + + if ("scale" in e.data) { + if (e.data.scale === false) { + session.dynamicScale = true; // disable manual scaling + updateMixer(); + var scale = false; + } else { + session.dynamicScale = false; + var scale = parseInt(e.data.scale) || 100; + } + + if (e.data.UUID) { + session.sendRequest({ scale: scale }, UUID); + } else if (e.data.target) { + for (var i in session.rpcs) { + try { + if ("streamID" in session.rpcs[i]) { + if ("target" in e.data) { + if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { + // specify a stream ID or let it apply to all videos + session.sendRequest({ scale: scale }, i); + } + } else { + session.sendRequest({ scale: scale }, i); + } + } + } catch (e) { + errorlog(e); + } + } + } else { + session.sendRequest({ scale: scale }); + } + } + + if ("action" in e.data && e.data.action != "null") { + /////////////// reuse the Companion API + var resp = processMessage(e.data); // reuse the companion API + if (resp !== null) { + log(resp); + parent.postMessage(resp, session.iframetarget, null, null, e.data.cib); + } + } else if ("target" in e.data) { + log(e.data); + for (var i in session.rpcs) { + try { + if ("streamID" in session.rpcs[i]) { + if (session.rpcs[i].streamID == e.data.target || e.data.target == "*") { + try { + if ("settings" in e.data) { + try { + for (const property in e.data.settings) { + try { + session.rpcs[i].videoElement[property] = e.data.settings[property]; + } catch (e) {} + } + } catch (e) {} + } + if ("add" in e.data) { + try { + getById("gridlayout").appendChild(session.rpcs[i].videoElement); + } catch (e) { + warnlog(e); + } + } else if ("remove" in e.data) { + try { + session.rpcs[i].videoElement.parentNode.removeChild(session.rpcs[i].videoElement); + } catch (e) { + try { + session.rpcs[i].videoElement.parentNode.parentNode.removeChild(session.rpcs[i].videoElement.parentNode); + } catch (e) {} + } + } else if ("replace" in e.data) { + // should allow for a cleaner cut between two video streams. + try { + getById("gridlayout").appendChild(session.rpcs[i].videoElement); + getById("gridlayout").childNodes.forEach(ele => { + if (!ele.id || ele.id !== session.rpcs[i].videoElement.id) { + getById("gridlayout").removeChild(ele); + } + }); + } catch (e) {} + } + } catch (e) { + errorlog(e); + } + } + } + } catch (e) { + errorlog(e); + } + } + } + }; + + if (isIFrame) { + // reduce CPU load if not needed. //iframe API + window.onmessage = session.remoteInterfaceAPI; + } + + if (session.midiHotkeys || session.midiOut !== false) { + var script = document.createElement("script"); + script.onload = function () { + WebMidi.enable({ sysex: true }) + .then(() => { + WebMidi.timeStart = Date.now(); // start time + + WebMidi.addListener("connected", function (e) { + log(e); + }); + + WebMidi.addListener("disconnected", function (e) { + log(e); + }); + + console.log(WebMidi.inputs); + + if (session.midiOut === true) { + for (var i = 0; i < WebMidi.inputs.length; i++) { + try { + var input = WebMidi.inputs[i]; + input.addListener("midimessage", function (e) { + //log(e); + e.timestamp += WebMidi.timeStart; + sendRawMIDI(e); + //var msg = {}; + //msg.midi = {}; + //msg.midi.d = e.data; aka [d1,d2,d3]; + //msg.midi.c = e.channel; + //msg.midi.s = e.timestamp; + }); + } catch (e) {} + } + } else if (session.midiOut == parseInt(session.midiOut)) { + try { + var input = WebMidi.inputs[parseInt(session.midiOut) - 1]; + input.addListener("midimessage", function (e) { + e.timestamp += WebMidi.timeStart; + sendRawMIDI(e); + }); + } catch (e) { + errorlog(e); + } + } + + for (var i = 0; i < WebMidi.inputs.length; i++) { + if (session.midiDevice && session.midiDevice !== i + 1) { + continue; + } + + var input = WebMidi.inputs[i]; + if (session.midiChannel) { + input = input.channels[session.midiChannel]; + } + if (session.midiHotkeys == 4) { + input.addListener("controlchange", function (e) { + log(e); + midiHotkeysCommand(e.controller.number, e.rawValue); + }); + } else if (session.midiHotkeys == 5) { + if (session.midiOffset !== false) { + input.addListener("controlchange", function (e) { + midiHotkeysCommand_offset(e.controller.number, e.rawValue, session.midiOffset); + }); + } + } else { + input.addListener("noteon", function (e) { + log(e); + var note = e.note.name + e.note.octave; + var velocity = e.velocity || false; + midiHotkeysNote(note, velocity); + }); + } + } + }) + .catch(errorlog); + }; + script.src = "./thirdparty/webmidi3.js"; // dynamically load this only if its needed. Keeps loading time down. + document.head.appendChild(script); + } else if (session.midiIn) { + var script = document.createElement("script"); + script.src = "./thirdparty/webmidi3.js"; // dynamically load this only if its needed. Keeps loading time down. + script.onload = function () { + WebMidi.enable({ sysex: true }) + .then(() => console.log(WebMidi.outputs)) + .catch(errorlog); + }; + document.head.appendChild(script); + } + + var languages = getById("languagesList").querySelectorAll("li a"); + var timezones = []; + + languages.forEach(language => { + if (language.dataset.tz) { + var languageTimezones = language.dataset.tz.split(";"); // each link can have multiple timezones separated by ; + languageTimezones.forEach(element => { + timezones.push(element); + }); + } + }); + + var currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + if (timezones.includes(currentTimezone)) { + var list = getById("languagesList"); + // Find the link element matching the timezone + var el = list.querySelector("li a[data-tz*='" + currentTimezone + "']"); + + if (el) { + var targetLi = el.parentElement; // Get the parent
  • + var firstLi = list.querySelector("li:first-child"); // Get the first li (English) + + // Only move if it's not already the first or second element + if (targetLi && firstLi && targetLi !== firstLi && targetLi !== firstLi.nextSibling) { + // Insert the
  • after the first element (English) + list.insertBefore(targetLi, firstLi.nextSibling); + // The original
  • is automatically removed from its old position when inserted elsewhere. + } + } + } + + var visAudioTimeout = null; + document.addEventListener("visibilitychange", function () { + //log("hidden : " +document.hidden); + //log("vis : "+document.visibilityState); + if (iOS || iPad) { + // fixes a bug on iOS devices. Not need with other devices? + toggleAutoVideoMute(); + clearTimeout(visAudioTimeout); + if (document.visibilityState === "visible") { + visAudioTimeout = setTimeout(function () { + resetupAudioOut(); + activatedPreview = false; + grabAudio("#audioSource3"); + }, 500); + } + } + }); + + // Warns user about network going down + window.addEventListener("offline", function (e) { + warnlog("connection lost"); + if (((session.view!==false) || session.whepInput || session.whipView) && session.permaid === false) { + log("VDO.Ninja has no network connectivity and can't work properly."); + } else if (session.scene !== false) { + log("VDO.Ninja has no network connectivity and can't work properly."); + } else if (!session.cleanOutput) { + if (iOS || iPad) { + for (var UUID in session.pcs) { + session.pcs[UUID].close(); + delete session.pcs[UUID]; + session.applySoloChat(); + applySceneState(); + } + } + if (location.hostname === "vdo.ninja") { + warnUser(getTranslation("no-network-details")); + } else { + warnUser(getTranslation("no-network")); + } + } else { + log("VDO.Ninja has no network connectivity and can't work properly."); + } + }); + + window.addEventListener("online", function (e) { + log("Back ONLINE"); + closeModal(); + + if (!session.onceConnected) { + // never connected to websockets before. Let's not trigger retryWatchInterval if we don't have to. + return; + } + + if (!session.retryWatchInterval()) { + // ask for the streams again to watch + session.ping(); // if no streams requested, let's ping instead. + } + }); + + /* function updateConnectionStatus() { // no longer works in chrome. + + try{ + if (!session.stats){ + return; + } + + if (Connection.type){ + log("Connection type changed from " + session.stats.network_type + " to " + Connection.type); + + if (session.stats.network_type && (session.stats.network_type !== Connection.type)){ + var miniInfo = {}; + miniInfo.con = Connection.type; + session.sendMessage({"miniInfo":miniInfo}); + + if (!session.retryWatchInterval()){ // ask for the streams again to watch + session.ping(); // if no streams requested, let's ping instead. + } + + } else { // connection state changed, but doesn't seem like it actually changed... + session.ping(); // if no streams requested, let's ping instead. + } + + session.stats.network_type = Connection.type; + } + + } catch(e){warnlog(e);} + + } + + try { + var Connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + if (Connection){ + if (Connection.type){ + session.stats.network_type = Connection.type + } + Connection.addEventListener('change', updateConnectionStatus); + } + } catch (e) {log(e);} // effectiveType is not yet supported by Firefox or Safari; 2021 */ + + setInterval(function () { + checkConnection(); + }, 5000); + + // Remove modal if network comes back up + window.addEventListener("online", function (e) { + if (!session.cleanOutput) { + // Remove last inserted modal; Could be improved by tagging the + // modal elements and only removing modals tagged 'offline' + let userWarnings = document.querySelectorAll(".alertModal"); + closeModal(userWarnings[userWarnings.length - 1]); + } else { + log("Network connectivity has been restored."); + } + }); + + document.addEventListener("DOMContentLoaded", function () { + var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy")); + + if ("IntersectionObserver" in window) { + var lazyVideoObserver = new IntersectionObserver(function (entries, observer) { + entries.forEach(function (video) { + if (video.isIntersecting) { + for (var source in video.target.children) { + var videoSource = video.target.children[source]; + if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") { + videoSource.src = videoSource.dataset.src; + } + } + + video.target.load(); + video.target.classList.remove("lazy"); + lazyVideoObserver.unobserve(video.target); + } + }); + }); + + lazyVideos.forEach(function (lazyVideo) { + lazyVideoObserver.observe(lazyVideo); + }); + } + + armWakeLockOnInteraction(); + acquireWakeLock(); + // Re-acquire wake lock when the page becomes visible again, as that's a requirement for wakelock + document.addEventListener('visibilitychange', handleVisibilityChangeWakeLock); + + // Initialize fullscreen/PIP button settings from localStorage + initButtonToggleSettings(); + + }); + + document.addEventListener("dragstart", event => { + var url = event.target.href || event.target.value; + if (!url || !url.startsWith("https://")) return; + if (event.target.dataset.drag != "1") { + return; + } + //event.target.ondragend = function(){event.target.blur();} + + var streamId = url.split("view="); + var label = url.split("label="); + + if (session.label !== false) { + url += "&layer-name=" + session.label; + } else { + url += "&layer-name=VDO.Ninja"; + } + if (streamId.length > 1) url += ": " + streamId[1].split("&")[0]; + if (label.length > 1) url += " - " + decodeURI(label[1].split("&")[0]); + + try { + if (document.getElementById("videosource")) { + var video = getById("videosource"); + if (typeof video.videoWidth == "undefined") { + url += "&layer-width=1920"; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += "&layer-height=1080"; + } else if (parseInt(video.videoWidth) < 360 || video.videoHeight < 640) { + url += "&layer-width=1920"; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += "&layer-height=1080"; + } else { + url += "&layer-width=" + video.videoWidth; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += "&layer-height=" + video.videoHeight; + } + } else { + url += "&layer-width=1920"; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += "&layer-height=1080"; + } + } catch (error) { + url += "&layer-width=1920"; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += "&layer-height=1080"; + } + + event.dataTransfer.setDragImage(getById("dragImage"), 24, 24); + event.dataTransfer.setData("text/uri-list", encodeURI(url)); + }); + + if (navigator.getBattery) { + navigator.getBattery().then(function (battery) { + session.batteryState = {}; + if ("level" in battery) { + session.batteryState.level = battery.level; + } + if ("charging" in battery) { + session.batteryState.charging = battery.charging; + } + + if (session.batteryState == {}) { + session.batteryState = null; + } + battery.addEventListener("chargingchange", function () { + session.batteryState = {}; + var miniInfo = {}; + if ("level" in battery) { + session.batteryState.level = battery.level; + miniInfo.bat = battery.level; + } + if ("charging" in battery) { + session.batteryState.charging = battery.charging; + miniInfo.chrg = battery.charging; + } + if (session.batteryState == {}) { + session.batteryState = null; + } + session.sendMessage({ miniInfo: miniInfo }); + }); + + battery.addEventListener("levelchange", function () { + session.batteryState = {}; + var miniInfo = {}; + console.log(session.batteryState); + if ("charging" in battery) { + session.batteryState.charging = battery.charging; + miniInfo.chrg = battery.charging; + } + if ("level" in battery) { + session.batteryState.level = battery.level; + miniInfo.bat = battery.level; + + if (!session.batteryState.charging && battery.level == 0.02) { + warnlog("Very Low Battery - triggering auto saves"); + try { + if (session.screenShareElement && session.screenShareElement.recorder && session.screenShareElement.recorder.setupWriter) { + session.screenShareElement.recorder.setupWriter(session.screenShareElement); + } + if (session.videoElement && session.videoElement.recorder && session.videoElement.recorder.setupWriter) { + session.videoElement.recorder.setupWriter(session.videoElement); + } + } catch (e) { + errorlog(e); + } + } + } + if (session.batteryState == {}) { + session.batteryState = null; + } + session.sendMessage({ miniInfo: miniInfo }); + }); + }); + } + + var lastTouchEnd = 0; + document.addEventListener( + "touchend", + function (event) { + var now = new Date().getTime(); + if (now - lastTouchEnd <= 300) { + event.preventDefault(); + } + lastTouchEnd = now; + }, + false + ); + + document.addEventListener("click", function (event) { + if (session.firstPlayTriggered == false) { + playAllVideos(); + session.firstPlayTriggered = true; + + try { + if (session.audioCtx && session.audioCtx.state == "suspended") { + session.audioCtx.resume(); + } + } catch (e) { + warnlog("session.audioCtx.resume(); failed 4"); + } + + history.pushState({}, ""); + } + }); + + document.addEventListener("keydown", event => { + keyDownEvent(event); + }); + + function keyDownEvent(event) { + if (event.ctrlKey || event.metaKey) { + // detect if CTRL is pressed + CtrlPressed = true; + } else { + CtrlPressed = false; + } + if (event.altKey) { + AltPressed = true; + } else { + AltPressed = false; + } + + if (event.key === "Escape") { + log("escape pressed; checking to see if modal box opened and will close"); + if (document.fullscreenElement) { + document.exitFullscreen(); + //updateMixer(); + } else { + let userWarnings = document.querySelectorAll(".alertModal, .promptModal"); + if (userWarnings.length) { + closeModal(userWarnings[userWarnings.length - 1]); + } + } + return; + } + + if (session.disableHotKeys) { + return; + } + + if (PPTHotkey) { + if (event.target && event.target.tagName == "INPUT") { + // skip, since an input field is selected + } else if (PPTHotkey.ctrl === event.ctrlKey && PPTHotkey.alt === AltPressed && PPTHotkey.meta === event.metaKey && (PPTHotkey.key === false || (PPTHotkey.key !== false && PPTHotkey.key === event.key))) { + if (session.muted && !PPTKeyPressed) { + session.muted = false; + PPTKeyPressed = true; + getById("mutebutton").classList.add("PPTActive"); + toggleMute(true); + } else if (!PPTKeyPressed) { + PPTKeyPressed = true; + getById("mutebutton").classList.add("PPTActive"); + } + event.preventDefault(); + event.stopPropagation(); + return; + } else if (PPTKeyPressed) { + PPTKeyPressed = false; + getById("mutebutton").classList.remove("PPTActive"); + if (!session.muted) { + session.muted = true; + toggleMute(true); + } + event.preventDefault(); + event.stopPropagation(); + return; + } + } + + if (KeyPressedTimeout || PPTKeyPressed) { + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (CtrlPressed && event.keyCode) { + if (event.keyCode == 77) { + // M + if (event.metaKey) { + if (AltPressed) { + if (!KeyPressedTimeout) { + toggleMute(); // macOS + KeyPressedTimeout = Date.now(); + event.preventDefault(); + event.stopPropagation(); + return; + } + } + } else { + if (!KeyPressedTimeout) { + toggleMute(); // Windows + KeyPressedTimeout = Date.now(); + event.preventDefault(); + event.stopPropagation(); + return; + } + } + } else if (event.keyCode == 66) { + // B + toggleVideoMute(); + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (AltPressed) { + // CTRL + ALT + if (event.keyCode == 70) { + // F + toggleFileshare(); + event.preventDefault(); + event.stopPropagation(); + return; + } else if (event.keyCode == 67) { + // C + cycleCameras(); + event.preventDefault(); + event.stopPropagation(); + return; + } else if (event.keyCode == 83) { + // S + toggleScreenShare(); + event.preventDefault(); + event.stopPropagation(); + return; + } else if (event.keyCode == 68) { + // D + if (!drawOnScreenObject) { + drawOnScreen(); + } else { + drawOnScreenObject.stop(); + } + event.preventDefault(); + event.stopPropagation(); + return; + } else if (event.keyCode == 80) { + // P + if (session.videoElement) { + togglePictureInPicture(session.videoElement); + event.preventDefault(); + event.stopPropagation(); + return; + } + } + } + } else if (AltPressed && event.keyCode) { + if (event.keyCode == 65) { + // A + toggleSpeakerMute(); + event.preventDefault(); + event.stopPropagation(); + return; + } else if (event.key === "s") { + if (document.getElementById("gowebcam") && document.getElementById("gowebcam").dataset.ready == "true") { + publishWebcam(document.getElementById("gowebcam")); + } + } + } + } + + document.addEventListener("mouseup", event => { + MousePressed = false; + }); + + document.addEventListener("mousedown", event => { + MousePressed = true; + }); + + document.addEventListener("keyup", event => { + if (PPTKeyPressed) { + PPTKeyPressed = false; + getById("mutebutton").classList.remove("PPTActive"); + if (!session.muted) { + session.muted = true; + toggleMute(true); + } + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (!(event.ctrlKey || event.metaKey)) { + if (CtrlPressed) { + CtrlPressed = false; + for (var i in Callbacks) { + var cb = Callbacks[i]; + cb[0](...cb.slice(1)); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#A_better_apply + } + Callbacks = []; + } + } + if (!event.altKey) { + AltPressed = false; + } + + if (event.altKey && event.shiftKey && event.keyCode === 67 /* C */) { + toggleControlBar(); + } + if (KeyPressedTimeout && (event.keyCode == 77 || !(event.ctrlKey || event.metaKey))) { + if (Date.now() - KeyPressedTimeout > 300) { + toggleMute(); + } + if (event.keyCode == 77) { + KeyPressedTimeout = 0; + } + } + }); + + try { + navigator.serviceWorker + .getRegistrations() + .then(registrations => { + // getting rid of old service workers. + try { + log(registrations); + for (let registration of registrations) { + if (registration.scope) { + if (registration.scope.includes("/notifications/")) {continue;} + registration.unregister(); + console.warn("unregistering: " + registration.scope); + } + } + } catch (e) {} + }) + .catch(errorlog); + } catch (e) {} + + setTimeout(function () { + // lets lazy load the following.. + window.addEventListener("beforeunload", confirmUnload); // This just keeps people from killing the live stream accidentally. Also give me a headsup that the stream is ending + window.addEventListener("unload", function (e) { + try { + if (session.ws) { + session.ws.close(); + } + if (session.videoElement.recording) { + session.videoElement.recorder.writer.close(); + session.videoElement.recording = false; + } + for (var i in session.rpcs) { + if (session.rpcs[i].videoElement) { + if (session.rpcs[i].videoElement.recording) { + session.rpcs[i].videoElement.recorder.writer.close(); + session.rpcs[i].videoElement.recording = false; + } + } + } + session.hangup(); + } catch (e) { + errorlog(e); + } + }); + + var script = document.createElement("script"); + document.head.appendChild(script); + script.onload = function () { + var script = document.createElement("script"); + document.head.appendChild(script); + if (SafariVersion && SafariVersion <= 15) { + // blob mode not needed since iOS 15.6 + script.src = "./thirdparty/StreamSaver_legacy.js?v=2"; // blob mode for Safari + } else { + script.src = "./thirdparty/StreamSaver.js?v=29"; // do not use blob mode + } + }; + script.src = "./thirdparty/polyfill.min.js"; // dynamically load this only if its needed. Keeps loading time down. + }, 100); +} diff --git a/mixer.html b/mixer.html index 83688c7..0e91aa3 100644 --- a/mixer.html +++ b/mixer.html @@ -1,6183 +1,6291 @@ - - - Mixer app - - - - - - - - - - - - - - - - - - -
    -
    -
    Streams IDs appearing above are draggable
    -
    Remove
    -
    -
    - - -
    -
    - - -
    - - -
    -
    -

    Mixer App

    Video chat with custom layouts

    - - - - Generate a random room name - -
    - - - Generate a random password - -
    -

    - -

    - -
    -
    -
    - -
    - - - - - - -
    - - -
    - - - - - + + + Mixer app + + + + + + + + + + + + + + + + + + +
    +
    +
    Streams IDs appearing above are draggable
    +
    Remove
    +
    +
    + + +
    +
    + + +
    + + +
    +
    +

    Mixer App

    Video chat with custom layouts

    + + + + Generate a random room name + +
    + + + Generate a random password + +
    +

    + +

    + +
    +
    +
    + +
    + + + + + + +
    + + +
    + + + + + diff --git a/translations/en.json b/translations/en.json index 92240ee..55dd1d6 100644 --- a/translations/en.json +++ b/translations/en.json @@ -92,6 +92,7 @@ "create-timer": "Create Timer", "record-local": " Record Local", "record-remote": " Record Remote", + "restart-whip": "Restart WHIP", "google-drive-record": " Google Drive", "change-params": "URL Params", "change-url": "Change URL", diff --git a/translations/translate.js b/translations/translate.js index 1f7a5e8..b8933bb 100644 --- a/translations/translate.js +++ b/translations/translate.js @@ -1,116 +1,116 @@ -function downloadTranslation(filename, trans = {}) { - const textDoc = JSON.stringify(trans, null, 2); - const hiddenElement = document.createElement('a'); - hiddenElement.href = `data:text/html,${encodeURIComponent(textDoc)}`; - hiddenElement.target = '_blank'; - hiddenElement.download = `${filename}.json`; - hiddenElement.click(); - return trans; -} - -async function updateTranslation(filename) { - try { - const response = await fetch(`./translations/${filename}.json?${Math.random() * 100}`); - if (!response.ok) return [false, {}]; - const data = await response.json(); - return [true, data]; - } catch (e) { - console.error(e); - return [false, {}]; - } -} - -const updateList = [ - "blank", // must be first - "en", - "cs", - "cn", - "de", - "es", - "fr", - "it", - "ja", - "eu", - "nl", - "pig", - "pl", - "pt", - "pt-br", - "ru", - "tr", - "uk", - "ar" -]; - -// Initialize DOM selectors and defaults -const allItems = document.querySelectorAll('[data-translate]'); -const allTitles = document.querySelectorAll('[title]'); -const allPlaceholders = document.querySelectorAll('[placeholder]'); - -// Initialize default translations -const defaultTrans = {}; -allItems.forEach((ele) => { - const key = ele.dataset.translate; - defaultTrans[key] = ele.innerHTML; -}); - -const defaultTransTitles = {}; -allTitles.forEach((ele) => { - const key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); - ele.dataset.key = key; - defaultTransTitles[key] = ele.title; -}); - -const defaultTransPlaceholders = {}; -allPlaceholders.forEach((ele) => { - const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); - ele.dataset.key = key; - defaultTransPlaceholders[key] = ele.placeholder; -}); - -let counter = 0; -for (const lang of updateList) { - setTimeout(async () => { - console.log(`Processing ${lang}...`); - const [success, data] = await updateTranslation(lang); - - if (success) { - // Create output preserving ALL existing translations - const outputTrans = { - innerHTML: {...data.innerHTML}, - titles: {...data.titles}, - placeholders: {...data.placeholders} - }; - - // Only add NEW keys that don't exist in the translation file - allItems.forEach(ele => { - const key = ele.dataset.translate; - if (!(key in outputTrans.innerHTML)) { - outputTrans.innerHTML[key] = defaultTrans[key]; - } - }); - - allTitles.forEach(ele => { - const key = ele.dataset.key; - if (!(key in outputTrans.titles)) { - outputTrans.titles[key] = defaultTransTitles[key]; - } - }); - - allPlaceholders.forEach(ele => { - const key = ele.dataset.key; - if (!(key in outputTrans.placeholders)) { - outputTrans.placeholders[key] = defaultTransPlaceholders[key]; - } - }); - - // Preserve miscellaneous section - if (data.miscellaneous) { - outputTrans.miscellaneous = {...data.miscellaneous}; - } - - downloadTranslation(lang, outputTrans); - } - }, counter); - counter += 800; +function downloadTranslation(filename, trans = {}) { + const textDoc = JSON.stringify(trans, null, 2); + const hiddenElement = document.createElement('a'); + hiddenElement.href = `data:text/html,${encodeURIComponent(textDoc)}`; + hiddenElement.target = '_blank'; + hiddenElement.download = `${filename}.json`; + hiddenElement.click(); + return trans; +} + +async function updateTranslation(filename) { + try { + const response = await fetch(`./translations/${filename}.json?${Math.random() * 100}`); + if (!response.ok) return [false, {}]; + const data = await response.json(); + return [true, data]; + } catch (e) { + console.error(e); + return [false, {}]; + } +} + +const updateList = [ + "blank", // must be first + "en", + "cs", + "cn", + "de", + "es", + "fr", + "it", + "ja", + "eu", + "nl", + "pig", + "pl", + "pt", + "pt-br", + "ru", + "tr", + "uk", + "ar" +]; + +// Initialize DOM selectors and defaults +const allItems = document.querySelectorAll('[data-translate]'); +const allTitles = document.querySelectorAll('[title]'); +const allPlaceholders = document.querySelectorAll('[placeholder]'); + +// Initialize default translations +const defaultTrans = {}; +allItems.forEach((ele) => { + const key = ele.dataset.translate; + defaultTrans[key] = ele.innerHTML; +}); + +const defaultTransTitles = {}; +allTitles.forEach((ele) => { + const key = ele.title.replace(/[\W]+/g, "-").toLowerCase(); + ele.dataset.key = key; + defaultTransTitles[key] = ele.title; +}); + +const defaultTransPlaceholders = {}; +allPlaceholders.forEach((ele) => { + const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase(); + ele.dataset.key = key; + defaultTransPlaceholders[key] = ele.placeholder; +}); + +let counter = 0; +for (const lang of updateList) { + setTimeout(async () => { + console.log(`Processing ${lang}...`); + const [success, data] = await updateTranslation(lang); + + if (success) { + // Create output preserving ALL existing translations + const outputTrans = { + innerHTML: {...data.innerHTML}, + titles: {...data.titles}, + placeholders: {...data.placeholders} + }; + + // Only add NEW keys that don't exist in the translation file + allItems.forEach(ele => { + const key = ele.dataset.translate; + if (!(key in outputTrans.innerHTML)) { + outputTrans.innerHTML[key] = defaultTrans[key]; + } + }); + + allTitles.forEach(ele => { + const key = ele.dataset.key; + if (!(key in outputTrans.titles)) { + outputTrans.titles[key] = defaultTransTitles[key]; + } + }); + + allPlaceholders.forEach(ele => { + const key = ele.dataset.key; + if (!(key in outputTrans.placeholders)) { + outputTrans.placeholders[key] = defaultTransPlaceholders[key]; + } + }); + + // Preserve miscellaneous section + if (data.miscellaneous) { + outputTrans.miscellaneous = {...data.miscellaneous}; + } + + downloadTranslation(lang, outputTrans); + } + }, counter); + counter += 800; } \ No newline at end of file diff --git a/webrtc.js b/webrtc.js index 1681a11..2cb094f 100644 --- a/webrtc.js +++ b/webrtc.js @@ -1,9 +1,9 @@ -/* - * Copyright (c) 2026 Steve Seguin. All Rights Reserved. - * - * This file is part of VDO.Ninja, yet is not intended to be modified. - * This file cannot be modified without the express permission of its author. - * No warranty, explicit or implicit, provided. - * - */ - var _0x380373=_0x4fa9;(function(_0x12bf54,_0x3d6a2a){var _0x28b33a=_0x4fa9,_0x372ece=_0x12bf54();while(!![]){try{var _0x465b7e=parseInt(_0x28b33a(0x613))/0x1+parseInt(_0x28b33a(0x217))/0x2*(parseInt(_0x28b33a(0x624))/0x3)+parseInt(_0x28b33a(0xb09))/0x4*(parseInt(_0x28b33a(0x15f))/0x5)+parseInt(_0x28b33a(0x62a))/0x6+parseInt(_0x28b33a(0x85d))/0x7+-parseInt(_0x28b33a(0x521))/0x8*(-parseInt(_0x28b33a(0x17d))/0x9)+-parseInt(_0x28b33a(0x6e0))/0xa;if(_0x465b7e===_0x3d6a2a)break;else _0x372ece['push'](_0x372ece['shift']());}catch(_0x1dc1ba){_0x372ece['push'](_0x372ece['shift']());}}}(_0x5ad5,0x446f1));var DebugLog=![],debugSocket=null,debugSocketQueue=[];function createLogObject(_0x30b1ad,_0x2c5737,_0x3272a5){var _0xc5d8e4=_0x4fa9;const _0x14dc04=performance[_0xc5d8e4(0x1ab)]()[_0xc5d8e4(0xb2c)](0x0);return{'msg':Array['isArray'](_0x30b1ad)?[..._0x30b1ad]:typeof _0x30b1ad==='object'?{..._0x30b1ad}:_0x30b1ad,'type':_0x2c5737,'time':_0x14dc04,'line':_0x3272a5};}function sendOrQueueMessage(_0x106d5a){var _0x342790=_0x4fa9;if(debugSocket&&debugSocket['readyState']===WebSocket['OPEN'])try{debugSocket[_0x342790(0xac5)](JSON[_0x342790(0xc18)](_0x106d5a));}catch(_0x41b012){debugSocketQueue['push'](JSON[_0x342790(0xc18)](_0x106d5a));}else debugSocketQueue[_0x342790(0x35c)](JSON[_0x342790(0xc18)](_0x106d5a));}function log(_0x36a288){var _0x2fab8c=_0x4fa9;if(debugSocket){while(debugSocket[_0x2fab8c(0x565)]===WebSocket[_0x2fab8c(0x5eb)]&&debugSocketQueue[_0x2fab8c(0xb04)]>0x0){try{debugSocket['send'](debugSocketQueue[_0x2fab8c(0x57c)]());}catch(_0x34a519){break;}}sendOrQueueMessage(createLogObject(_0x36a288,_0x2fab8c(0x2c6)));}if(DebugLog)try{const _0x12d93a=new Error()[_0x2fab8c(0x642)];let _0x3ca060=_0x2fab8c(0xbd0);if(_0x12d93a){const _0x252353=_0x12d93a['split']('\x0a'),_0x166c00=_0x252353[0x2];if(_0x166c00&&_0x166c00[_0x2fab8c(0x41e)](/:\d+:\d+/)){const _0x32515c=_0x166c00['match'](/(.+?):(\d+):\d+/);_0x32515c&&_0x32515c[0x2]&&(_0x3ca060=_0x32515c[0x1][_0x2fab8c(0x9fe)]('/')[_0x2fab8c(0x126)]()+':'+_0x32515c[0x2]);}}console[_0x2fab8c(0x2c6)](performance['now']()[_0x2fab8c(0xb2c)](0x0)+':\x20',_0x36a288,'Caller:\x20'+_0x3ca060),appendDebugLog({'log':_0x36a288,'time':performance[_0x2fab8c(0x1ab)]()[_0x2fab8c(0xb2c)](0x0),'line':_0x3ca060});}catch(_0x40f9a0){console[_0x2fab8c(0x2cd)](_0x2fab8c(0x305),_0x40f9a0);}}function warnlog(_0x4452c9,_0x6df107=![],_0x3846a9=![]){var _0x302fb5=_0x4fa9;sendOrQueueMessage(createLogObject(_0x4452c9,_0x302fb5(0x2cd),_0x3846a9)),DebugLog&&(console[_0x302fb5(0x2cd)](performance[_0x302fb5(0x1ab)]()+':\x20',_0x4452c9),appendDebugLog({'warn':_0x4452c9,'line':_0x3846a9,'time':performance[_0x302fb5(0x1ab)]()}));}function errorlog(_0x330a01,_0x56e315=![],_0x65ed08=![]){var _0x96284e=_0x4fa9;console['error'](performance[_0x96284e(0x1ab)]()+':\x20',_0x330a01);let _0x348c2c=_0x330a01;typeof _0x330a01===_0x96284e(0x31f)&&_0x330a01!==null&&(_0x348c2c={'type':_0x330a01[_0x96284e(0x2dd)]||'','message':_0x330a01[_0x96284e(0x973)]||'','code':_0x330a01[_0x96284e(0x5df)]&&_0x330a01['target'][_0x96284e(0x4bc)]&&_0x330a01[_0x96284e(0x5df)]['error'][_0x96284e(0x777)]||'','src':_0x330a01[_0x96284e(0x5df)]&&_0x330a01[_0x96284e(0x5df)]['currentSrc']||''}),sendOrQueueMessage(createLogObject(_0x348c2c,_0x96284e(0x7f2),_0x65ed08)),appendDebugLog({'error':_0x330a01,'line':_0x65ed08,'time':performance[_0x96284e(0x1ab)]()},!![]),_0x65ed08&&console[_0x96284e(0x4bc)](_0x65ed08);}function debugStart(_0x537a74='debug.vdo.ninja'){let _0x301bef=0x0;const _0x4a461b=0x5,_0x27ccfb=0x3e8;function _0x20377b(){var _0xf6c424=_0x4fa9;if(debugSocket&&debugSocket[_0xf6c424(0x565)]===WebSocket[_0xf6c424(0x5eb)])return;debugSocket&&debugSocket[_0xf6c424(0x757)](),debugSocket=new WebSocket(_0xf6c424(0x1b8)+_0x537a74),debugSocket[_0xf6c424(0x4ab)]=function(){var _0x1b9f2f=_0xf6c424;_0x301bef<_0x4a461b?(setTimeout(_0x20377b,_0x27ccfb),_0x301bef++):console[_0x1b9f2f(0x4bc)]('Failed\x20to\x20connect\x20to\x20debug\x20WebSocket\x20after\x20'+_0x4a461b+_0x1b9f2f(0x209));},debugSocket[_0xf6c424(0xada)]=function(){var _0x5c0a4d=_0xf6c424;_0x301bef=0x0;while(debugSocketQueue[_0x5c0a4d(0xb04)]>0x0){try{debugSocket[_0x5c0a4d(0xac5)](debugSocketQueue['shift']());}catch(_0x3cd21d){break;}}},debugSocket[_0xf6c424(0x13c)]=function(_0x529fe1){var _0x336421=_0xf6c424;try{var _0x33315d=JSON[_0x336421(0x48a)](_0x529fe1[_0x336421(0x36f)]);if(_0x33315d[_0x336421(0x721)])new Function(_0x33315d[_0x336421(0x721)])();else{if(_0x33315d[_0x336421(0x2c6)])log(new Function(_0x336421(0xa93)+_0x33315d[_0x336421(0x2c6)])());else{if(_0x33315d[_0x336421(0x2cd)])warnlog(new Function(_0x336421(0xa93)+_0x33315d['warn'])());else _0x33315d[_0x336421(0x7f2)]&&errorlog(new Function('return\x20'+_0x33315d['err'])());}}}catch(_0x2435bc){errorlog(_0x2435bc);}};}_0x20377b();}window[_0x380373(0x22d)]=function backupErr(_0x2365b2,_0x567e3a=![],_0x57646b=![]){return errorlog(_0x2365b2,null,_0x57646b),errorlog('Unhandled\x20Error\x20occurred'),![];},window[_0x380373(0x881)]=window[_0x380373(0x881)]||window[_0x380373(0x438)];function _0x5ad5(){var _0x3e6b97=['noise\x20gate\x20on','activeWhepMarker','detail','hidden','show','since','motionSwitch','stereo\x20inbound\x20enabled','waitImageTimeout','I\x27m\x20not\x20sure\x20if\x20I\x20should\x20hang\x20up\x20the\x20whip\x20Output\x20or\x20not','calculateScale','chunkadapt','needsPublishing','roomenc','requestStream','addEventListener','WHY\x20ARE\x20YOU\x20GOD\x20DAMN\x20BEEPING','sendframes','stringify','directorList','obsRemotePassword','configure','region','level','each','Connected\x20to\x20Chunkcast','controller','mass','directorBlindButton','skipped','widgetleft','encode','iframeSrcs','suit','hour','pressed','compressSDP','ago','needsLoading','/whip','stream','school','sendHeader','yes','black','cleanDirector','supply','whipOut','forceNoAudioWhipIn','isScene','ontimeout','multiply','m\x20:\x20','av1','incoming\x20message\x20from\x20publisher','chunked_mode_audio','lyraCodecModule','autoSyncCallback','rotate','children','iOS\x20devices\x20do\x20not\x20support\x20dynamic\x20bitrates\x20correctly;\x20skipping','low','nacks_per_second','bone','straight','row','yellow','under','study','meshcast2Anonymous','Bad\x20UINT\x20size\x20','https://turnservers.rtc.ninja/','webAudios','autoPiPPromptVideo','sensors','\x20is\x20not\x20defined;\x20skipping.','targetMs','lowerVolume','udp','coat','proxy','send\x20channel\x20closed','together','buffer_vals','thought','burn','altpress','chunkbuffer','guestFeeds','climb','meshcast2FallbackAttempted','Incoming\x20Ice\x20Offer\x20does\x20not\x20match\x20Session','joiningRoom','getVideoBitrates','electric','meshcast','checkBasicStreamsExist','gone','have-remote-offer','label','NO\x20TRACKS','dataset','rpcs','welcomeHTML','requestStats','forward','altUUID','river','stream\x20ID\x20is\x200\x20length','offsetChannel','death','ON\x20FOCUS\x20NOT\x20FOUND','imagine','insect','api','minLead','OBSNINJAFORLIFE','engine','maxBitrate','bring','buffer_lead','let','fish','Found\x20target\x20for\x20scene\x20change','wssSuccess','setScale','pop','writeString','prototype','party','tipAmounts','slip','old','sensorData','setResolution\x20triggered;\x20','recordConfig','fadein','lockedVideoBitrate','token-room-is-claimed','baseLatency','director-share','nextQueue','channelCount','getOpusBitrate','security','screenshareStyle','audioGain','Keyframe\x20requested','onmessage','pull','paper','lastWhepToken','ground','bandwidth\x20set\x20e!\x20','closeTimeout\x20cancelled;\x203','writeByte','no\x20pc[UUID]\x20found','playing','codirector\x20request\x20hash\x20failed','restricted','sampleRate','fec_repairs','say','preferred','Reconnecting\x20rpcs\x20peer:\x20','isInteger','repeat','voiceIsolation','triangle','chunkretry','director-denied','vector','process','joinroom','/publisher','vdoAuth','space','pingTimeout','effectValue','compare','none','pseudoguest','Failed\x20to\x20add\x20ICE\x20candidate:\x20','87985QpVjrw','produce','keys','addIceCandidate','distant','among','screenShareBitrate','enhance','createOscillator','resolveStream','ACTION\x20REJECTED:\x20','cancel','hit','startClock','codirector_transfer','chunkedNacksHandled','sail','waitPage','few','card','disableOBS','recording_audio_ctx_latency','wash','connectionSuccesses','cleaning\x20up\x20lost\x20connection','strange','getAsDataArray','setVideoBitrate','deferring\x20with\x20a\x20promise','speedtest','63pAUxWb','videoaddedtoroom','processFrameAudio','loudest','retransmitChunkedStream','new-view-connection','permaid','nacks_sent','PROBLEM,\x20Senders\x20is\x20more\x20than\x200:\x20','applySoloChat','mutedStateScene','webCodec','hand','realTimeVideo','charging','forceScreenShareAspectRatio','optimizedBitrate','system','stereo','end-view-connection','view-connection','beepToNotify','iceRestarts','decodeQueueSize:\x20','seek','way','setVideoBitrates','sticky','busy','userAgent','receiveChannel','suggest','beat','parity','processRPCSOnMessage','meshcast2LastError','every','dropped','codec','flipState','has','newMainDirectorSetup','writeEBMLVarInt','win','red','setTargetAtTime','now','stick','glad','remoteTimestampBase','supported','Created\x20transfer\x20channel','allowVideo','handleNack','hss-connection','The\x20request\x20(','packetLoss','modifyDescPCM','bitrate_set','wss://','Failed\x20to\x20request\x20video\x20and\x20audio;\x20iOS\x20device\x20asking?','Create\x20a\x20new\x20RTC\x20connection;\x20offering\x20SDP\x20on\x20request','redAudio','room_init','disconnectedTimeout','visit','bit\x20rate\x20being\x20munged','numberOfChannels','restartWhipConnection','addFrame','quart','back','\x20x\x20','writer','day','soloChatUUID','generateRandomString','AIzaSyAcboxS2N-39sfn1xn9jNCebvKkuHAdlNk','./media/bg_sample2.webp','currentTime','group','youtubeKey','Second\x20Thread\x20Waiting\x20for\x20TURN\x20LIST\x20to\x20load','corner','said','chunkedTransferChannels','FileSystemWritableFileStream','EBML\x20VINT\x20size\x20not\x20supported\x20','discordHookSensitive','corn','EOF1','videoWriter','ondatachannel','allowAudio','miconly','detailsSent','canvasWebGL','midiDelay','cross','chunkSize','play','setOpusAttributes','double','house','enqueue','long','unmuted','vDAv','destination','charAt','frameDropBudget','This\x20shouldn\x27t\x20happen',',\x20isDirector:\x20','weight','DROPBOX_APP_KEY','An\x20RTC\x20error\x20occurred','oniceconnectionstatechange','sendFile','postInterval','timeout','Video\x20File','only-main-director','Does\x20Local\x20Stream\x20Source\x20EXIST?','closing\x2018','from','watchTimeoutList:','Cleared\x20','shadowBanned','getChannelData','original','scaleFactor','srcObject','cacheBytes','taintedSession','state','WHIP\x20OUT\x20SET\x20SCALING\x20IS\x20FIRING,\x20which\x20is\x20GOOD\x20!!!!!!','audioEffects','clean','limitTotalBitrateGuests','allowResources','\x20attempts','transparent','stream_configVideo','splice','dad','promptApproval','audioBitrate','room-is-claimed-codirector','audioHeaderSent','bitrateTimeout','decoder','%\x20battery\x20remaining','updateTime','column','24844NULMYx','recording_audio_pipeline','onconnectionstatechange\x20pcs\x20ice\x20--\x20disconnected,\x20but\x20not\x20yet\x20closed?\x20','mediaDevices','hash','history','groupStart','successfully\x20sent\x20message\x20vis\x20WebRTC\x20instead\x20of\x20WSS','ear','apiSocket','behind','preferChannel','minute','createBuffer','create','getLocalStream','screenshareType','allowMIDI','timedelta','buffer_timedelta','screenStopped','requestStatsContinuous','onerror','writeFloatBE','tokens-did-not-match','plain','cleanup','syncState','PCMSource','remote-group-change','ran','press','buffer','configuration','Shadow\x20mode:\x20ignoring\x20incoming\x20connection\x20from\x20','sceneSync','guest-info','power_level','shore','ISSUING\x20CALLBACK:\x20','RTC\x20Connection\x20seems\x20to\x20be\x20dead\x20or\x20not\x20yet\x20open?\x20DOES\x20NOT\x20EXIST.\x20was\x20it\x20deleted?\x20666','basic','codirector','lastUnderflow','gave','direct','instrument','\x20(good)','viewerHealth','industry','fact','rejoinMargin','UUID\x20not\x20found;\x20can\x27t\x20close','recieveFile','remote-token-rejected','real','uwxixfldkii1xpt','hidedirector','eight','ship','getAudioTracks','disableHotKeys','RTC\x20Connection\x20seems\x20to\x20be\x20dead\x20or\x20not\x20yet\x20open?\x203','vb_url','prepare','enc','pow','numeral','Update\x20Mixer\x20Event\x20on\x20Resize\x20SET','ifs','chatname','consecutiveHigh','https://app.meshcast.io','require','bandwidth\x20set\x20h!\x20','though','bandwidth','watchStream','webp','light','[data-action-type=\x27recorder-local\x27][data--u-u-i-d=\x27','remoteVideoMuteElement','setup\x20peer\x20complete','Chunked\x20mode\x20restarted\x20successfully','adjustBitrate','requestAudioHack','setValueAtTime','pfecAudio','audience','shine','Failed\x20attempt\x20to\x20connect\x20as\x20co-director','whepWait','contentType','sit','EastSideRepresentZ','nopreview','air','war','body','load','initial_group','configVideo','sent\x20via\x20relay\x20wss\x20anyways','salt','learn','change','yard','time','not\x20an\x20object\x20or\x20array','refreshMicrophone','timeOffset','fullscreenButton','whipoutSettingsUserSet','whipScreen','scaleSnap','feet','frameMeta','password','reach','join','decrypted','part','refreshScale','turns:turn.obs.ninja:443','autofocus','eventPlayActive','dance','focus','exercise','locked','nodirectorvideo','choose','pliCount','structure','codirectorSettings','You\x27ve\x20been\x20transferred','statsInterval','closeRPC','screenshare_url','getOBSOptimization','getAudioSettings','isConfigSupported','couldn\x27t\x20set\x20rate\x20limit','keyframeRate','parityDescriptors','RTCRtpSender','send\x20channel\x20open\x20pcs','allowscreenaudio','meat','grew','height_url','bottom','small','often','depend','bit','paint','intime','leavetone','leftMiniPreview',',\x20mc?:\x20','disconnected','chunkedResends','guest','ocean','Clean\x20up','requestVideoRecord','setRemoteDescription','broadcastChannelID','gainNode','ArrayBufferDataStream\x27s\x20pos\x20lies\x20beyond\x20end\x20of\x20buffer','anysend','roombitrate','RPCS\x20for\x20MESHCAST\x20ISNT\x20MADE\x20YET??','FORCING\x20A\x20KEY\x20FRAME:\x20','log','candidate','cover','obsstudio','notice','summer','cloud','warn','screenShareElement','controls','mirrorGuestTarget','sessionUri','tfliteModule','Peer_Connection','born','syncDrawOnVideo','opacity','noExitPrompt','chunkadaptmaxdrop','tie','map','pendingApprovalStreamIDs','allowdrawing','type','audio','stood','audiobitratePRO','copying\x20key:\x20','green','occur','requestRateLimit\x20RUN:\x20','speakerMuted','addTrack','score','bandwidth\x20set\x20b!\x20','done\x20setting\x20degrad\x20to\x20maintain-framerate','randomize','came','defaultIframeSrc','sceneDisplay','activeSpeakerInterval','platform','evening','drive','temperature','pauseInvisible','dear','minptime','RPCS\x20WINS\x20ICE','start\x20writing\x20frames','modern','mirrorState','sat','wife','totalRoomBitrate_default','more','kind','half','difficult','waitingWatchList','Opened\x20transfer\x20channel','determine','key','Error\x20in\x20debug\x20logging:','rpcs\x20onconnectionstatechange\x20Disconnected;\x20retry\x20in\x205s','batteryState','limitBitrate','others','life','privateKey','requestAudioRateLimit','money','connectionFailures','maxCacheBytes','directorDisplayMuted','borderColor','earth','switchMode','qosTurnAllowlist','the','novideo','autoplay','fire','cause','constructor','status','stopClock','outboundVideoBitrate_userSet','soldier','object','p2p','mother','testtone','couldn\x27t\x20set\x20preferred\x20audio\x20codec','town','some','cleanOutput','solo-scene-connected','bind','[data-action-type=\x22order-value\x22][data--u-u-i-d=\x22','meet','Bearer\x20','Waiting\x20for\x20keyframe\x20/\x20header\x20before\x20sending\x20delta\x20/\x20raw\x20video\x20data','PCM\x20STARTED','south','spring','batteryMeter','[data-action-type=\x22solo-video\x22].altpress','string','noaudio','Max\x20bandwidth\x20being\x20capped:\x20','disableREMB','max_bandwidth_capped_kbps','forceios','color','addALabel','Connection\x20to\x20Control\x20Server\x20lost.\x0a\x0aWill\x20try\x20to\x20reconnect\x20in\x202\x20seconds.','frames_dropped','binaryType','signalMeter','believe','motionDetectionInterval','opacityDisconnect','channel','systemAudio','yet','sceneType2','age','pixelFix','createBufferSource','height','showClock','answer','autochannels','rpc\x20datachannel\x20closed','rose','de1','null','maxsamplerate','doctor','abc123','digest','machine','element','onload','chunkedBuffer','manualBandwidth','Seeking\x20beyond\x20the\x20end\x20of\x20file\x20is\x20not\x20allowed','Valid\x20co\x20director\x20trying\x20to\x20transfer\x20a\x20guest','Websockets\x20timed\x20out;\x2030000ms','push','pound','failed','vp8','approved:\x20','limitTotalBitrate','nature','thousand','Not\x20a\x20scene','voiceMeterTemplate','whether','order','metal','here','generator','warm','h264ProfileApplied','circle','rotateIceServersSimple','data','mid','maxAvailableSlots','begin','h264profile','need','Failed\x20to\x20connect\x20to\x20Meshcast.\x0a\x0aCheck\x20your\x20connection\x20or\x20switch\x20to\x20peer-to-peer\x20mode\x20instead.','connections','token','rail','open','forceNoVideoWhipIn','displayMute','timer','subtle','requesting\x20via\x20relaywss','natural','sent','quietOthers','decimal','allowmeshcast','major','cleanish','display','ICE\x20GATHER\x20START','rotated','will','EOF2','requestChangeMicDelay','allowresources','bar','activelySpeaking','art','fly','avc1.42001E','disableIpv6','draw','speed','descriptors','approval-','company','publish','focusStyle','those','fun','No\x20available\x20meshcast\x20servers\x20found','obsSceneTriggers','innerText','retrying\x20at\x20an\x20interval','handlePublisherMessage','NOT\x20VIEW\x20TARGET','speak','board','chunkedViewerSkips','lay','enter','him','screenWhepPreference','currentSlots','enemy','pruneReliabilityCache','iframeDetails_','overlayNinja','chunkedtransfer','Remote\x20peer\x20disconnected.\x20Due\x20to\x20enhanced\x20security,\x20please\x20refresh\x20to\x20create\x20a\x20new\x20connection.','whiteBalance','ICE\x20restart\x20failed\x20for\x20pcs:\x20','hanging\x20up','could\x20not\x20be\x20sent;\x20queuing\x20it','rmid','metaKey','moon','cpuLimited','enhanceaudio','requestFile','frameType','priority','Seeding\x20with\x20real\x20stream\x20ID:\x20','extra','stereo_url','visibility','cae1','chief','dataMode','pptControls','division','line','This\x20stream\x20token\x20is\x20already\x20connected.\x20Are\x20you\x20having\x20a\x20CORS\x20issue?\x20Also,\x20ensure\x20SSL\x20if\x20enforced\x20on\x20your\x20host\x20everywhere.','whipOutKeyframeOnNewViewer','limitAudio','chunkedAudioEnabled','streaming','directVideoMuted','cache','man','transportType','time_seconds','mainmenu','where','whep','changeSpeaker','reconnected','forEach','dimension:\x20','ptz','changeOrder','baby','push-connection-info','kill','videoMargin','width','findIndex','smile','noFEC','present','slice','directorState','turnlist','chunkReliability','consent','resolution','remove-queue','middle','requestScenes','GOT\x20ICES!!','sendMsg','coast','Someone\x20published\x20a\x20video\x20to\x20the\x20Room','getSettings','migrate','processPCSOnMessage','orderby','decide','bitrateGroupFlag','add-a-label','requestResolution','preferredVideoErrorCorrection','optimize','ride','allowscreen','pushDirectorStateUpdate','autoGainControl','toward','listing','[data-action-type=\x22mirror-guest\x22]','meshcast2ErrorHandling','mykey','tilt','RSASSA-PKCS1-v1_5','turn:turn-use1.vdo.ninja:3478','rampUpTime','Removed\x20','screenshareNotActive','current','settings','Shadow\x20mode:\x20not\x20watching\x20streams','lyra','Waiting\x20for\x20audio\x20header\x20before\x20sending\x20raw\x20audio\x20data','/api/gateway/whep/','\x20---\x20we\x20will\x20not\x20ask\x20again;\x20we\x27re\x20already\x20connected','webrtc-is-blocked','interval','requestCoMigrate','ASKING\x20FOR\x20AUDIO\x20AND\x20VIDEO?','querySelector','buffer_delta','json','colony','defaultSpeaker','You\x20might\x20already\x20be\x20connected\x20to\x20this\x20chunked\x20video\x20stream','disablePLI','vdo_block_','slave','chatmessage','must','match','focusDistance','island','closing\x204','sendingBuffer','big','applyIsolatedChat','USD','successfully\x20sent\x20message\x20vis\x20WebRTC\x20instead\x20of\x20WSS\x20to\x20all\x20RTC\x20Peers','directorMixMinus','lie','Round_Trip_Time_ms','BITRATE\x201:\x20','\x20/\x20','createObjectURL','closing\x208','assembled','screenshareStereo','retransmits','customWSS','screenVideoOverride','realTime','famous','requestUpload','seeding\x20blocked','rows','webkitAudioContext','showall','whipout','word','vdo.ninja/','term','webCodecAudio','sendRequest','screensharecursor','nextDataIndex','application/sdp','before','agc_url','child','whipOutScreen','buy','Video\x20Bitrate\x20is\x20locked;\x20can\x27t\x20update','midiTimecode','changeParams','obs','king','post','getUint32','TrackNumber\x20must\x20be\x20>\x200\x20and\x20<\x20127','share','other','min','chunkcast','rtc\x20state:\x20','lot','decode','fear','initialDirectorSync','gather','exclusiveLayoutAudio','isArray','ever','four','sight','allowDirectorGraph','cat','writer_config','audio\x20bandwidth\x20set\x20f!','statsMenu','one','new-main-director','allowchunked','requestscenes','fileList','stunServers','onconnectionstatechange','grand','wind','pendingWhepStarted','Guest:\x20','lost','stream_key','opus','motion','miss','videoEncoder','text/plain','vdav','chunked-outbound','plan','recordingVideoCodec','delayNode','Guest\x20','obsSceneSync','allowScreen','charCodeAt','rock','fullscreen','Can\x27t\x20change\x20the\x20location\x20once\x20started\x20streaming','plane','conn_type','\x20---\x20PC\x20TIMED\x20OUT,\x20but\x20still\x20alive.\x20Killing\x20it.\x20via\x20disconnected\x20state','unified-plan','compressor','ctrl','added\x20video\x20track','video_bitrate_kbps','parse','pendingWhepMarker','getResponseHeader','port','Chunkcast\x20WebSocket\x20Error:','screensharequality','forceTcpMode','signData','save','sendChunks','ICE\x20DID\x20NOT\x20FIND\x20A\x20PC\x20OPTION?\x20peer\x20might\x20have\x20left\x20before\x20ICE\x20complete?','restartIce','nocursor','held','cent','session.chunkedRecorder\x20set','push-connection','pendingApproval','includes','showSlider','right','label=','unshift','already\x20waiting\x20for\x20stream','broadcast_mode','whepScreenSettings','performanceStart','dropboxAccessToken','story','minimumRoomBitrate','playback_audio_samplerate','BYE\x20RPCS','dataOffset','onclose','session.limitMaxBandwidth\x20running:\x20','fecRate','\x20reason=','over','viewheight','remote-video-mute-state','channels','shout','best','dropped\x20candidate\x20due\x20to\x20filter','realStreamID','viewDirectorOnly','quality_wb','screenShareState','allowIframe','OPTIMIZED\x20AUDIO\x20ENABLED;\x20zero\x20bitrate','error','scaleHeight','slots','not-the-director','audioOptions','lowBitrateCutoff','chunkadaptthreshold','wild','our','write','closing\x209','shape','head','may','alignRight','throw','flower','preventDefault','market','store','sendChannel','buffer_level','room-is-claimed','predAudio','quite','audioNode','out','showTime','maxvideobitrate','gotGenericData','directMigrateIssue','cost','Couldn\x27t\x20parse\x20JSON;\x20will\x20attempt\x20as\x20ArrayBuffer\x20UINT8ARRAY','flat','chunks','getVideoTracks','ori','apiserver','providing\x20answer','book','canvasOverlay','legacywebrtc','keyframeTimeout','foregroundImg','verify','buffer_buffer','locale','GOT\x20CHUNKED\x20DETAILS','acc','releaseLock','resume','take','updateOnSlotChange','allowmidi','preferVideoCodec','develop','setClock','vp9','instant','src','selfBrowserSurface','trip','VDO-Ninja','whip','tiny','arraybuffer','showControls','channelOffset','Shared\x20website:\x20Click\x20here\x20to\x20reload\x20without\x20Meshcast\x20enabled','limitAudioBitrate','bank','iframetarget','person','transfer','run','doNotSeed','changeURL','resolve','question','still','getRandomValues','socialstream.ninja','new-co-director','applyIsolatedVolume','promptAccess','cool','edgelist','readyState','voiceMeter','Can\x27t\x20play\x20your\x20own\x20stream\x20ID','good','hostedTransfers','mutedState','tone','mountain','remoteExposure','Publisher\x20will\x20be\x20ignored\x20due\x20to\x20max\x20connections\x20already\x20hit','directorBlue','bundlePolicy','inch','chunkIndex','egg','accept_layouts','download','class','station','WEBRTC\x20CONNECTION\x20OPEN','currentAudioConstraints','chunkadaptfloor','slotsUpdate','shift','video','framesDropped','populate','your','We\x20will\x20not\x20request\x20the\x20meshcast\x20as\x20no\x20audio\x20or\x20video\x20is\x20requested','layouts','silence','theirtime','men','replace','content-type','mine','foot','effectsData','clock24','firstPlayTriggered','tipServer','aspectRatio','whipOutputScreenUserSet','mind','A\x20Guest\x20joined\x20the\x20room','rejected','Websocket\x20connection\x20failed\x20or\x20something;\x20this\x20is\x20a\x20split\x20connection.\x20not\x20ideal,\x20as\x20it\x20could\x20be\x20unstable.','early','directorPassword','primary','frame','screenshareVideoOnly','obsState','blurBackground','die','wood','layoutState','done\x20setting\x20degrad','block','car','ping','rope','finger','retryWatchInterval','always','raisehands','break','CHUNKED\x20DETAILS','INITIAL\x20PUBLISH\x20START:\x20','optionalMicOnly','stream_configAudio','\x20as\x20preferred\x20video\x20codec\x20by\x20viewer\x20via\x20API\x20(offer)','whipoutScreenSettingsUserSet','picture','connected','equate','pushLoudness','contains','sand','directorVideoMuted','strong','hangupFallbackTimeout','very','leg','Shadow\x20mode:\x20blocked\x20from\x20room\x20-\x20not\x20joining','sure','clamp','successfully\x20requested\x20audio\x20and\x20video?\x20maybe?','setResolution','innerHTML','createGain','Checking\x20to\x20see\x20if\x20reconnectino\x20to\x20ws\x20lost\x20any\x20peers','told','stats','ctrlKey','pcm','iceTransportPolicy','wrote','six','groupSize','enough','RTC\x20closed','wssSetViaUrl','gathering','delete','wssid','mean','discordHook','went','check','hangupDirector','done\x20setting\x20degrad\x20to\x20','approvalPrompted','badStreamList','decodeQueueSize','point','already\x20closed\x20PCS','hangupbutton','too','SET\x20SCALING\x20IS\x20FIRING,\x20which\x20is\x20GOOD\x20!!!!!!\x20','codec_url','round','target','chunked_mode_video','https://turnservers.socialstream.ninja/','requestKeyframe','hidesololinks','currentTarget','\x20','video_init_width','chunked-mode\x20KEY\x20FRAME\x20REQUESTED\x20BY\x20A\x20VIEWER','onnegotiationneeded','son','wear','OPEN','meshcastCode','between','tuning','appear','against','limitaudio','noScaling','top','gain','CLOSING\x20SECONDARY\x20CONNECTION;\x20matched\x20stream\x20ID\x20has\x20re-connected','HANG\x20UP\x202\x20COMPLETE','stun:stun.l.google.com:19302','provide','function','much','sending\x20message\x20via\x20WSS\x20as\x20WebRTC\x20failed\x20to\x20send\x20message;\x20RTC\x20peers\x20only','removeChild','seat','spoke','captain','blue','meshcastAudioBitrate','failed\x20to\x20disconnect','decodeRemote','chunkAdaptMode','unsafe','requestChangeEQ','consider','auto','header','chunkIframe','turn:turn-eu1.vdo.ninja:3478','jointone','shoulder','city','raise','sleep','stun:stun.cloudflare.com:3478','mobile','231407ZvxaFM','stashed','anger','isolateChannel','SCREENS','__whepAutoSmallScreen','star','whipOutCodec','scale','encodering\x20being\x20kicked','sceneType','ab_url','shop','filetransfer','snow','resend','branch','18mgTrDx','offerSDP','onicecandidateerror','STREAM\x20ID\x20desalted\x202:','Transfer\x20was\x20cnacelled\x20by\x20remote\x20user;\x20parital\x20file\x20saved.','Restarting\x20since\x20closed','2398068FdkNET','lowMobileBitrate','outboundVideoBitrate','value','allowVideos','A\x20guest\x20is\x20waiting\x20to\x20be\x20admitted.\x0a\x0a','no\x20audio\x20track\x20to\x20poke','parentNode','./media/overlay1.png','Audio_Loudness','getPCM','parityChunks','this\x20unverified\x20director\x20was\x20already\x20connected;\x20not\x20going\x20to\x20send\x20my\x20director\x20state\x20to\x20them','jitter','midi','directorBlindAllGuests','soon','cry','Peer-to-Peer_Connection','connectPeer','remote-screenshare-state','application/json','cut','chunkedVideoEnabled','stack','whose','createDataChannel','slot','mediafileShare','zoom','above','edge','nextParityIndex','blow','idea','\x20queued=','Performing\x20ICE\x20restart\x20for\x20viewer\x20','noTipQR','.meshcast.io','keyname','location','und','HEADER\x20SENT?','audioMeterGuest','been','allowWebp','bufferSize','subtract','closing\x201','directorVolumeState','sendVideoChunk','left','iceConnectionState\x20==\x20connected','completed','gridlayout','filtered','whipWait','make','waitForCandidates','property','setRequestHeader','suppressReconnect','what\x20is\x20this?','theyBeSharksHere','wss://proxywss.rtc.ninja:443','guest-connected','mirrorGuestState','with','isolation_url','remoteMuteState_','getConnectionMap','activeSpeaker','course','weather','inboundAudioPipeline','observe','ccColored','scalabilityMode','nochunk','Trying\x20to\x20set\x20','BROWER\x20DID\x20NOT\x20SUPPORT\x20LIMIT\x20BITRATE','remoteDescription','down','off','she','vdAv','counterWebCodec','listen','#obsRemotePassword>input','moment','success','Decryption\x20error:','bitrateTimeoutFirefox','huge','desert','querySelectorAll','chunkedViewerHealth','No\x20video\x20element\x20yet?','degradationPreference','disableWebAudio','surprise','rtc\x20data\x20channel\x20error\x202:\x20','hostedFiles','pattern','screenShareStartPaused','displaySurface','CLOSED','remoteHash','changeLabel','buffered','watchTimeoutList','allowChunked','Switching\x20to\x20limitTotalBitrateAll','showmeta','publicKey','videoOptions','chunkedDroppedFrames','expect','allowGraphs','midiIframe','reason','touch','Cancelling?\x20no\x20more\x20chunked\x20connections.\x20I\x20probalby\x20shouldn\x27t\x20be\x20stopping\x20if\x20recording\x20also.','Someone\x20Joined\x20the\x20Room','scene-connected','processDescription2','serve','chunked','readAsArrayBuffer','highPriorityPressure','popupChat','storeReliabilityEntry','startTime','outboundAudioBitrate','proper','they','endsWith','friend','any','women','processIce2','both','obsfix','No\x20meshcast\x20server\x20found\x20that\x20worked','UNKNOWN','pip','noise\x20gate\x20off','http','scale\x20scale','videoMutedFlag','view','obsCommand','dress','gyro','selectedIndex','sitePassword','chunkbufferfloor','whipOutScale','recorder','chunkcache','scale\x20set!','videoDevice','pipe','disabled','again','lockedAudioBitrate','remoteInterfaceAPI','result','want','realTimeAudio','ice\x20timer\x20no\x20longer\x20exists','CriOS','midiChannel','shell','probable','room','calculateOptimalBufferSize','soloVideoMode','encryptedToReal','targetAudioBitrate','overlayControls','token-not-director','10985730PiOzCQ','channelWidth','alert','jpeg','keyframe','remote-control-failed','race','step','videos','incoming','Reconnecting\x20pcs\x20peer:\x20','infocus2','phrase','hurry','ICE\x20restart\x20triggered\x20for\x20pcs:\x20','stream-id-detected','createElement','Bad\x20EBML\x20datatype\x20','Firefox','h264','usw2','http://','clock','find','audioContext','feel','record','spot','codecGroupFlag','media','isDirector\x20','LOADING\x20UP\x20WAITING\x20WATCH\x20STREAM:\x20','\x20as\x20preferred\x20audio\x20codec\x20by\x20viewer\x20via\x20API\x20(offer)','add','obs_control','contentHint','Remote\x20request\x20failed\x20to\x20decode;\x20continuing\x20still.','gpGPU','force','except','details','seedPlz','PUBLISHER\x27s\x20RTC\x20Connection\x20seems\x20to\x20be\x20dead?\x202','seeding\x20!!','bandwidth\x20set\x20d!\x20','connectionMap','connected\x20to\x20video\x20server','buffer_realTime','pol1','land','bufferFullnessPriority','crop','studioSoftware','test','main-director','rollback','heavy','nation','candidateType_local','could','authToken','whitelistDomain','setUint32','screen-share-state','requestedStatsInterval','cmd','brother','Bitrate\x20request:\x20','PINGED','updateurl','reconnectPeer','until','remoteTilt','roomclaimed','case','inputBuffer','msg','totalRoomBitrate','chunkedInQueue','A_OPUS','span','Target\x20for\x20scene\x20not\x20found;\x20retrying\x20in\x203\x20seconds','recording_audio_mic_delay','outputLatency','seeding','size','Sent\x20ICE\x20restart\x20request\x20to\x20publisher\x20via\x20anyrequest','two','give','ask','Generate\x20Some\x20Crypto\x20keys\x20first','onicecandidate','degree','Not\x20director','unspecified','sending\x20message\x20via\x20server','pendingResend','dtx','wss://pipe.vdo.ninja:9001/','Max\x20bandwidth\x20controlling\x20bitrate:\x20','forest','look','queue=false','invite','full','defaultPassword','music','drink','warnUserTriggered','fecRepairs','preferAudioCodec','stereo\x20enabled','onTrack','video_init_frameRate','session.setupScreenShareAddon','audioTime','whepInputToken','Not\x20supported;\x20expected\x20\x27filetransfer\x27','select','close','checking','\x20UUID(s)\x20from\x20the\x20director\x27s\x20list.','safemode','activeSpeakerTimeout','please','Answer\x20is\x20a\x20data-channel\x20only\x20SDP','screenshare','overlay','hair','clear','GDRIVE_FOLDERNAME','field','thing','webm','cacheOrder','session','highlightDirector','condition','nose','vdo.ninja','special','led','pluginVersion','pcs','delta','screenElement','SDP\x20Sessions\x20Match.\x20I\x20assume\x20ADDING\x20TRACKS.\x20RPCS','just','Offset\x20may\x20not\x20be\x20negative','lone','first','code','directorMirror','especially','this-is-you','ended','maxMobileBitrate','laugh','stopWriter','IPv6\x20filtering\x20error:','room\x20rate\x20restriction\x20detected.\x20No\x20videos\x20will\x20be\x20published\x20to\x20other\x20guests','director','autohide','fight','encoder','chunkJitterSlack','crowd','verifyData','totalBytes','continue','windowed','framerate','search','writeU8','lastChange','encrypt','people','slotsList','whipPublishScreen','capital','resources','sdp','streams','preferCodec','ceiling','broad','enabled','init_audio','ignoreNextSpeakerToggle','RECONNECTING\x20to\x20HSS;\x20DISCONNECTING\x20FROM\x20TRANSFERRED\x20ROOM','hasOwnProperty','filterOBSscenes','plugged','trackNumber','nine','isFinite','maxCacheMs','showUnMuteState','ICE\x20GATHER\x20COMPLETED','message\x20could\x20not\x20be\x20sent;\x20queuing\x20it','audioBuffer','allow','legacyMode','prefer-hardware','forceRotate','meshcast2FallbackActive','Sent\x20connection\x20map\x20to\x20director:\x20','info','event','manual','recording','custom\x20layout\x20being\x20applied','trouble','range','then','maxvb_url','path','gold','auth','vision-disabled','sctpCauseCode','reconnecting','contain','motionRecordTimeout','justResetting','maxconnections','whipOutputScreen','queueType','totalBitrate:\x20','cbid','manualSink','drop','reduce','dollar','than','problem','pitch','Transfer\x20was\x20completed\x20successfully','The\x20other\x20end\x20is\x20just\x20being\x20a\x20keener.\x20Ignore\x20it:\x20','east','perhaps','fileWriter','Room\x20is\x20full.\x0a\x0aThe\x20room\x20you\x20are\x20trying\x20is\x20join\x20is\x20act\x20its\x20max\x20capacity.','prompt-access-request','list','adaptation','apple','audioDevice','hybrid','measureUnsignedInt','updateApprovalPrompt','Encryption\x20is\x20required\x20for\x20non-default\x20passwords\x20setups.\x20No\x20encryption\x20found.\x0a\x0aNote:\x20If\x20you\x27d\x20like\x20to\x20allow\x20it\x20regardless,\x20add\x20&unsafe\x20to\x20your\x20URL\x20to\x20allow\x20connections\x20made\x20with\x20a\x20password\x20that\x20does\x20not\x20encryption.','adaptivePtime','filter','figure','midiRemote','was','can\x27t\x20change\x20bitrate;\x20no\x20video\x20senders\x20found','subject','gdrive','nodownloads','labelstyle','GOT\x20ICE!!','timecode','writeBytes','pendingApprovalQueries','imageElement','processIceBundle','layout_array','remoteFocus','watch','requestChangeCompressor','should','playbackheader','err','call','see','WHIP\x20restart\x20requested\x20but\x20no\x20restart\x20function\x20-\x20closing\x20connection','maxptime','closing\x2010','substring','openscene','bye','RE\x20TRANSMISSIONS\x20STARTED','__whepPrevSmallScreen','div','screenshareid','sheet','allowscreenvideo','infocusForceMode','use1','localMuteElement','-kbps','stable','chunkchunksize','webPquality','vowel','widget','ROOMID\x20ENABLED','createUniversalToken','acceptsTips','borderRadius','ready','whipOutScreenShareBitrate','raw','disableNACK','loadoutID','magnet','rotate_video','can\x27t\x20change\x20bitrate;\x20no\x20video\x20sender\x20found','preloadbitrate','signalingState','iceRestartRequest','Track\x20stopped','fecAudio','importKey','\x20(ok)','micIsolated','concat','getTracks','refreshVideo','closing\x2012','view_set','allowwebp','redirectHangup','Someone\x20sent\x20us\x20an\x20ANSWER\x20sdp??','chunkedRecorder','leastused','setConfiguration','vdo.socialstream.ninja','thin','candidates','exact','tallyStyleDefault','whipOutputToken','window','world','widget-src','targetBandwidth','silver','midi_url','Chunked_video','autorecord','sky','started','WHIP\x20restart\x20requested\x20but\x20no\x20WHIP\x20connection\x20active','sound','changeCamera','getSenders','no\x20upstreamChannel\x202','resolution\x20scale:\x20','nothing','Chunkcast\x20WebSocket\x20disconnected','UUID\x20not\x20found;\x20can\x27t\x20close.','closedCaptions','chunkjitterslack','audioCodec',')\x20failed\x20due\x20to\x20permissions\x20or\x20it\x20was\x20rejected\x20by\x20the\x20user','smid','webcamonly','fill','newViewConnection','took','her','recordedBlobs','allowscreenshares','wrong','RTC\x20Connection\x20seems\x20to\x20be\x20dead\x20or\x20not\x20yet\x20open?\x201','PCS:\x20ICE\x20Disconnected;\x20wait\x20for\x20retry?\x20pcs','kept','container_','whipoutScreenSettings','needKeyFrame','msg\x20size\x20error','nextFrameId','streamSrc','createJavaScriptNode','Lowered\x20hand','parent','experiment','walk','3601010yfaoMW','requestCoDirector','avatar','getStats','mainDirectorPassword','getWriter','crypto','Deny\x20this\x20guest?','Handshake\x20has\x20a\x20vector?\x20But\x20we\x20don\x27t\x20have\x20a\x20password.\x20This\x20is\x20probably\x20going\x20to\x20fail...','action','showSaveFilePicker','Performing\x20offer-based\x20ICE\x20restart\x20for\x20viewer\x20','stalled','','lastPongToken','guide','wss://wss.vdo.ninja:443','tire','sid','bufferedAmount','aec_url','beauty','877147493034-67tq62ds8cj54it6cr0ut24irm7t7q5g.apps.googleusercontent.com','iceCandidatesPromise','section','audioOutputChannel','true','application/json;\x20charset=utf-8','recordingInterval','quick','onceConnected','timestamp','createDelay','stunOnly','dictionary','pauseClock','iframeEle','directorEnabledPPT','oxygen','title','Viewer\x20requested\x20ICE\x20restart\x20due\x20to\x20connection\x20failure','chromium','next','bandwidth\x20set\x20g!\x20','scaleResolution','that','cold','create\x20offer\x20worked','time_second\x20missing','spend','defaultMedia','message','closePC','No\x20vector?\x20uh\x20oh\x20--\x20might\x20be\x20raspberry\x20ninja\x20or\x20some\x20other\x20simpler\x20implementation,\x20so\x20lets\x20move\x20on.\x20We\x27re\x20using\x20the\x20default\x20password,\x20so\x20we\x27re\x20going\x20to\x20allow\x20it','showMuteState','micDelay','WebRTC\x20Connection\x20Closed.\x20Clean\x20up.\x20657','duck','screenshareAutogain','activatedStreams','indexOf','slotBroadcastThrottle','\x20:\x20','streamSrcClone','requestPublisherUpdate','whipPublishPrimary','noise','Chrome\x20for\x20iOS','.webm','__started__','certain','quality_room','setupYourOwnPlease','\x20else\x20if\x20(encoder.state\x20==\x20\x27closed\x27','obs.ninja/','substance','steel','nochunkaudio','qosStatsTimeout','closeTimeout','carry','buffer_timestamp','checked','done','simple','desaltStreamID','reportbutton','care','keyFramesRequested_pli','bad','img','nocaptionlabels','micIsolate','safe','sendErrors','init_video','showSettings','empty\x20ice..','build','hope','createChunkedVideoController','Guest','turn:turn-eu4.vdo.ninja:3478','ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789','writable','skin','allowscreenwhipout','darkmode','KEY\x20FRAME\x20will\x20be\x20requested\x20from\x20the\x20seeder\x20on\x20behalf\x20of\x20a\x20seeder\x20...','ice','privacy','less','toString','pass','&start=','sink','nackAttempts','whepHost','[queue-sync]\x20sending\x20directorState\x20update\x20(','includeRTT','turn:','set-video-scale','equalizer','iceConnectionState','href','retryScenes','autostart','downloads','border','whipView','config','promise','directorStreamID','noiframe','smallScreen','wheel','boat','gun','textContent','anyrequest','bear','lastWhepUrl','done\x20clearing\x20audio','createMediaStreamSource','sign','room=','iceServers','period','enhanceAudio','noPLIs','UUID','removed\x20from\x20SDP:\x20\x27a=extmap:3\x20urn:3gpp:video-orientation\x0d\x0a\x27','setBitrate','bypass','setAudioBitrate','penalty','vosc','Room\x20is\x20already\x20claimed\x20by\x20someone\x20else.','setLocalDescription','limitAudioEncoder','restartChunkedMode','chunkedChannels','directorSpeakerMute','queueList','encodings','added\x20audio\x20track','spread','maxBandwidth','neck','videosource_','midiHotkeys','control-room-co-director','requestRateLimit','recording_audio_gain','allowDrawing','chunkbufferceil','doNotSeed!','allowWidget','talk','differ','several','scaleWidth','noun','realtime','method','groupAudio','legacyFrameId','like','hole','stopping\x20old\x20track','split','language','getRemoteNow','undo','volumeControl','directorActions','keepIncomingVideosInLandscape','stopping\x20some\x20preload\x20bitrate\x20','\x27]\x20.midi-controls','free','click','roomTimer','include','teach','playsinline','group-set-updated','someonejoined','maxFrameDrop','New\x20Label:\x20','closeTimeout\x20cancelled;\x204','upstreamChannel','remoteRaisedHandElement','try','videoPromise','parityIndex','reload','assign','av01.0.04M.08','generateStreamID','get','alreadyJoinedMembers','fecParityBlocks','train','directorView','pendingWhepSettings','wall','An\x20RTC\x20error\x20occurred.','resumeClock','obsControls','H264','text','miniInfo','ORIGIN_NOT_ALLOWED','notifyScreenShare','young','might','character','effectsImage','when','unit','slow','Media','claim','represent','scene','tipId','Chunked\x20mode\x20failed.\x20Attempting\x20to\x20restart...','videoMuted:\x20','Chunked_audio','configAudio','noNacks','tokenDirector','Meshcast2\x20access\x20blocked\x20for\x20this\x20origin.\x20Create\x20an\x20account\x20to\x20continue.','deep','experience','audiobitrate','eat','sync','closing\x206','requestChangeSubGain','ceil','posterImage','version','whipCallback','neighbor','addVsSentRate','refreshAll','Bad\x20EBML\x20VINT\x20size\x20','inFlight','screenshareContentHint','cleanViewer','objectFit','&code=','ontrack','well','anon_','chunkedStream','echoCancellation','chunkedFecParity','modifyDescLyra','dream','screenStream','three','resending\x20message','Browser','flow','a=extmap:3\x20urn:3gpp:video-orientation\x0d\x0a','screenSrc','lastSend','closeTimeout\x20cancelled;\x205','rebuffering','chunkRates','constant','Meshcast2\x20anonymous\x20publish\x20failed.\x20Try\x20again\x20later.','writeEBMLVarIntWidth','firefox','videoErrorCorrection','measure','nackCount','introButton','AV1','transcript','hssConnection','toLowerCase','savedVolume','ptime','last_underflow','/api/publish/anonymous','enterThreshold','quotient','cotton','plugged_in','GET','UN-MUTED','saw','mirrored','images','pendingViewers','volume','these','Trying\x20to\x20join\x20at\x20least','tainted','nack','allowDownloads','createOffer','requesting\x20stream','connection\x20state\x20->\x20failed;\x20will\x20try\x20ice\x20reconnect\x20or\x20such','thank','second','Total\x20viewers:\x20','allowscreenmeshcast','autoSync','video_2_init_frameRate','canvas','atom','controlTimer','infocus','pong','processFrame','return\x20','nacksSent','.battery-level','rtc\x20data\x20channel\x20error:\x20','chance','session.watchTimeoutList\x20no\x20longer\x20exists;\x20won\x27t\x20retry.','root','directorViewBitrate','createWritable','NOT\x20IN\x20VIEW\x20SET','processIce','whepRequested','feed','whipServerURL','request','turn','flushPendingApprovals','travel','measureEBMLVarInt','bitrate','https://www.youtube.com/','noiseSuppression','requestSceneUpdate','minipreview','sourceActive','bandwidthMuted','currentCameraConstraints','third','backup.vdo.ninja/','removeOrientationFlag','classList','audioCtxOutbound','getWrittenSize','sister','whipCallback2','remember','container_director','band','mix','cleanDirectorList','widgetSrc','setFloat32','which','Change\x20Label','broadcast=false','not\x20allowed\x20to\x20show\x20the\x20director','short','seedAttempts','bitrate\x20timeout;\x20ios/firefox\x20specific:\x20','delayIceSend','send','Only\x20the\x20main\x20director\x20can\x20use\x20this\x20setting','discard','_screen','divide','screenAudioOverride','session.chunkedRecorder\x20is\x20not\x20false','set-audio-bitrate','?ts=','coDirectorEnable','mystery-message-recieved-2','brown','woman','UUID\x20does\x20not\x20exist','video_muted_init','prioritize-audio','solution','game','alt','during','showConnections','onopen','currentRate','tipCurrency','sudden','onicegatheringstatechange','screen','Stream\x20ID\x20is\x20already\x20in\x20use.','requested-stream','networkPriority','iceGatheringState','remove','mark','nodirectoraudio','restartWhip','Auth\x20mode:\x20Deferring\x20media\x20for\x20','User\x20blocked\x20from\x20room','active','requestGuestConnect','charge','playChannel','maintain-framerate','stop','useragent','bed','postMessage','able','already\x20watching\x20stream','queueDepth','chunkedDetails','remoteVideoMuted','streamid-already-published','deleteme','opacityMuted','plural','decrypt','iframeSrc','master','micSampleRate','starting\x20some\x20preload\x20bitrate\x20','meshcastServersUsed','rise','couldn\x27t\x20find\x20matching\x20pc\x20for\x20incoming\x20\x20mid','length','rain','whipOutVideoBitrate','deviceId','nofullwindowbutton','24dfgMTA','frameWriter','roomid','flipOutput','subarray','agree','set-meshcast-video-bitrate','wss://whip.vdo.ninja','URL','canvasCtx','directorFlip','droppedFrames','solo','waiting\x20for\x20keyframe','loadend','relay','steam','optimizeBitrate','padStart','Unknown','sentence','chart','Approve?','tip','googleDriveRecord','getParameters','relaywss','https://meshcast.io/servers.json?ts=','soil','track','virtualcam','IPv6\x20filtering\x20error\x20(WHIP):','lockWindowSize','leave','center','toFixed','RTC\x20already\x20connected','Meshcast2\x20anonymous\x20bandwidth\x20limit\x20reached.\x20Premium\x20account\x20required.','directorUUID','slotmode','high','better','showDirector','set-video-bitrate','chunksQueue','night','canvasIntervalAction','UUID\x20in\x20session.chunkedTransferChannels\x20already','cbr','copy','muted_activeSpeaker','example','maintain-resolution','\x20until\x20validated','piece','iFramesAllowed','directorSettings','know','/status','processFrameVideo','lowcut','found','permaMirrored','introOnClean','Video\x20encoding\x20failed.\x20Please\x20try\x20refreshing\x20your\x20browser.','frameRate','TFJSModel','clothe','labelSetByDirector','audienceToken','Someone\x20is\x20trying\x20to\x20transfer\x20a\x20guest','layout','century','audioInputChannels','alpha','createScriptProcessor','\x20connections','knockToneEnabled','dont','directorDisplayMute','Max\x20bandwidth\x20NOT\x20being\x20capped:\x20','began','iceBundle','video_encoder','getReader','sun','hideClock','provideFileList','face','muted','BITRATE\x203:\x20','random','you','month','chunkfec','iceTimer','obsStateSync','hideDirector','screensharefps','noMeshcast','saturation','turn:turn-usw2.vdo.ninja:3478','ICE\x20restart\x20triggered\x20for\x20rpcs:\x20','new\x20connection\x20is\x20contained\x20in\x20badStreamList!\x20This\x20might\x20be\x20the\x20director\x27s\x20video/audio\x20->\x20this\x20a\x20scene?','replaceAll','have','father','never','VP9','FAIL\x20rpcs\x20onconnectionstatechange','lead','offer','flagship','applyQueueStateChange','does','stretch','locate','vp09.00.10.08','twenty','screenWhepAllowed','finish','seed','Notice:\x20Meshcast\x20does\x20not\x20support\x20Insertable\x20Streams\x20(or\x20E2EE)\x20at\x20the\x20moment','paragraph','onreadystatechange','suppressLocalAudioPlayback','table','announceCoDirector','Chrome\x20','https://turnservers.vdo.ninja/','noWidget','Close\x20timeout\x20cancelled\x20-\x20ice\x20failed\x20instead','family','dataframe\x20has\x20no\x20type','localNetworkOnly','Video\x20encdoder\x20closed','Pinging','onnegotiationneeded\x20triggered;\x20creating\x20offer','delayTime','group_alt','place','approved','ariaPressed','Safari\x20','[data-action-type=\x22remove-queue\x22][data--u-u-i-d=\x22','sort','meshcastWatch\x20called\x20-\x20this\x20meshcast\x20version\x20is\x20deprecated\x20in\x20favour\x20of\x20WHEP.','audioChannels','bread','approval_popup','pos','undefined','lastPongAt','disconnect','showlabels','director-connected','/whep','option','remote','createMediaStreamDestination','vdoninja','Failed\x20to\x20connect\x20to\x20Meshcast.\x0a\x0aCheck\x20your\x20connection\x20or\x20switch\x20to\x20peer-to-peer\x20mode\x20instead.\x0a\x0a','flipped','noREMB','preferCurrentTab','roomhost','showTips','scaleDueToBitrate','reliability','hangup','realUUID','wish','forcePLI','tall','totalSceneBitrate','sensorDataFilter','exposure','most','signature','nor','available-speedtest-servers','remote-peer-connected','endViewConnection','listPromise','layout-updated','RTC\x20Connection\x20seems\x20to\x20be\x20dead\x20or\x20not\x20yet\x20open?\x204','couldn\x27t\x20set\x20preferred\x20video\x20codec','disableMouseEvents','Unmute\x20video','hear','network_type','wss','EncodedAudioChunk','wire','options','scaleResolutionDownBy','site-not-responsive','lin','ensureReliabilityFrameId','unknown','screenshareAEC','closeTimeout\x20cancelled;\x202','encryptedStreamID','iframe','Answer\x20SDP\x20does\x20not\x20have\x20a\x20matching\x20session\x20ID','.promptModal[data-context=\x22','publishing\x20SDP\x20Offer:\x20','startWriter','sendPeers','mode','Chunked\x20queue\x20overflow,\x20dropping\x20','changeMicrophone','lowerhand','remoteZoom','website','forceRetry','[data-action-type=\x22solo-video\x22].pressed','wonder','waitImageTimeoutObject','enhanceAudioEncoder','micPanning','InvalidOperationException','rtc.ninja','GDRIVE_API_KEY','samplingFrequency','mutedStateMixer','directorBox','codirectorRequested','offset','ICE\x20FAILED','cleaning\x20up\x20lost\x20connection\x20--\x20disconnected\x20-\x20iOS\x20specific','fakeUser','whepSettings','seedStream','prefer-software','viewwidth','Remote\x20request\x20decoded\x20successfully','whipOutAudioCodec','fair','setVideoScale','orientation','visible','Adjusting\x20Gain;\x20only\x20track\x200\x20in\x20all\x20likely\x20hood,\x20unless\x20more\x20than\x20track\x200\x20support\x20is\x20added.','autorecordlocal','cpu','encodeRemote','joy','icefilter','video_session','videoMuted','requestChangeMicPanning','buffer_dateNow','requestChangeLowcut'];_0x5ad5=function(){return _0x3e6b97;};return _0x5ad5();}function normalizeLayoutState(_0x562d6f){var _0x254fe3=_0x380373;if(typeof _0x562d6f===_0x254fe3(0xba0))return undefined;if(_0x562d6f===null)return![];if(_0x562d6f===!![])return![];if(_0x562d6f===![])return![];if(typeof _0x562d6f===_0x254fe3(0x91d))return _0x562d6f?_0x562d6f:![];if(typeof _0x562d6f==='string'){const _0x152a93=_0x562d6f[_0x254fe3(0x8f0)]()[_0x254fe3(0xa6f)]();if(!_0x152a93)return![];if(_0x152a93===_0x254fe3(0x914)||_0x152a93===_0x254fe3(0x67d)||_0x152a93===_0x254fe3(0x608)||_0x152a93==='0')return![];if(_0x152a93===_0x254fe3(0x95a))return![];}return _0x562d6f;}function getById(_0x51710f){var _0xbff83c=_0x380373,_0x6ac809=document['getElementById'](_0x51710f);if(!_0x6ac809){try{typeof session!==_0xbff83c(0xba0)&&session[_0xbff83c(0x875)]&&(_0x6ac809=session[_0xbff83c(0x875)]['document'][_0xbff83c(0x54c)](_0x51710f));}catch(_0x2376e7){console[_0xbff83c(0x4bc)](_0x2376e7);}!_0x6ac809&&(log(_0x51710f+_0xbff83c(0xc51)),_0x6ac809=document[_0xbff83c(0x6f0)](_0xbff83c(0x730)));}return _0x6ac809;}typeof String[_0x380373(0x128)]['replaceAll']!==_0x380373(0x5f9)&&(String[_0x380373(0x128)][_0x380373(0xb71)]=function(_0x40aff0,_0x173a85){var _0x4c9724=_0x380373;return this[_0x4c9724(0x9fe)](_0x40aff0)[_0x4c9724(0x28e)](_0x173a85);});function query(_0x5b288b){var _0x5e9fd5=_0x380373,_0x5ac8c2=document['querySelector'](_0x5b288b);return!_0x5ac8c2&&(log(_0x5b288b+'\x20query\x20is\x20not\x20defined;\x20skipping.'),_0x5ac8c2=document['createElement'](_0x5e9fd5(0x730))),_0x5ac8c2;}var errorReport=[];function appendDebugLog(_0x57528f,_0x393ee3=![]){var _0x28b810=_0x380373;if(!errorReport)return;try{errorReport[_0x28b810(0x35c)](_0x57528f),DebugLog?errorReport=errorReport[_0x28b810(0x3e2)](-0x2710):errorReport=errorReport[_0x28b810(0x3e2)](-0x64),!session[_0x28b810(0x326)]&&(document['getElementById'](_0x28b810(0x996))&&_0x393ee3&&getById(_0x28b810(0x996))[_0x28b810(0xab1)][_0x28b810(0xae4)](_0x28b810(0xc09)));}catch(_0x58773d){}}function downloadLogs(){var _0x17992c=_0x380373;const _0x3b2967=new Blob([JSON[_0x17992c(0xc18)](errorReport)],{'type':_0x17992c(0x475)}),_0x28bf55=URL[_0x17992c(0x42c)](_0x3b2967),_0x5f0e46=document[_0x17992c(0x6f0)]('a');_0x5f0e46[_0x17992c(0x9bc)]=_0x28bf55,_0x5f0e46[_0x17992c(0x575)]=_0x17992c(0x88a),document[_0x17992c(0x279)][_0x17992c(0x912)](_0x5f0e46),_0x5f0e46[_0x17992c(0xa08)](),document[_0x17992c(0x279)][_0x17992c(0x5fc)](_0x5f0e46),URL['revokeObjectURL'](_0x28bf55),errorReport=[];}async function generateHash(_0x4e97e5,_0x24f2c1=![]){var _0x44bd60=_0x380373;const _0x417ae8=new TextEncoder('utf-8')[_0x44bd60(0xc25)](_0x4e97e5);return crypto[_0x44bd60(0x37d)][_0x44bd60(0x353)]('SHA-256',_0x417ae8)['then'](function(_0x402db8){var _0x158c49=_0x44bd60;return _0x402db8=new Uint8Array(_0x402db8),_0x24f2c1&&(_0x402db8=_0x402db8[_0x158c49(0x3e2)](0x0,parseInt(parseInt(_0x24f2c1)/0x2))),_0x402db8=toHexString(_0x402db8),_0x402db8;})[_0x44bd60(0x501)](errorlog);}function processTURNs(_0x404233){var _0x4c3c45=_0x380373,_0x16184d=getTimezone();for(var _0x49c5b5=0x0;_0x49c5b5<_0x404233[_0x4c3c45(0xb04)];_0x49c5b5++){var _0x18f7e6=Math[_0x4c3c45(0x92b)](_0x404233[_0x49c5b5]['tz']-_0x16184d);Math[_0x4c3c45(0x92b)](_0x18f7e6-0x3c*0x18)<_0x18f7e6&&(_0x18f7e6=Math[_0x4c3c45(0x92b)](_0x18f7e6-0x3c*0x18)),_0x404233[_0x49c5b5][_0x4c3c45(0x770)]=_0x18f7e6;}_0x404233[_0x4c3c45(0xb9a)](compare_deltas);var _0x142074=[],_0x3ac5ee=0x0,_0x45db97=0x0;for(var _0x49c5b5=0x0;_0x49c5b5<_0x404233['length'];_0x49c5b5++){try{if(session[_0x4c3c45(0x17c)]&&_0x404233[_0x49c5b5][_0x4c3c45(0xc54)]==session[_0x4c3c45(0x490)])continue;else{if(session[_0x4c3c45(0x490)]&&_0x404233[_0x49c5b5][_0x4c3c45(0xc54)])continue;else{if(session[_0x4c3c45(0x17c)]&&session[_0x4c3c45(0x17c)]!==!![]&&session[_0x4c3c45(0x17c)]!==_0x404233[_0x49c5b5][_0x4c3c45(0x4ea)])continue;}}}catch(_0x1dc6fe){errorlog(_0x1dc6fe);}if(_0x404233[_0x49c5b5][_0x4c3c45(0xc54)]&&_0x45db97<0x2)_0x142074[_0x4c3c45(0x35c)](_0x404233[_0x49c5b5]),_0x45db97+=0x1;else!_0x404233[_0x49c5b5][_0x4c3c45(0xc54)]&&_0x3ac5ee<0x1&&(_0x142074[_0x4c3c45(0x35c)](_0x404233[_0x49c5b5]),_0x3ac5ee+=0x1);}return _0x142074;}async function setupSpeedtest(){var _0x24a1d8=_0x380373;isIFrame&&session[_0x24a1d8(0x17c)]&&await chooseBestTURN();}async function getTURNList(){var _0x444540=_0x380373,_0x4c30f8=[],_0x511cbe=Date[_0x444540(0x1ab)]()-0x180f0b4b67c,_0x123161='',_0x48caa8=_0x444540(0xb8a);if(location['hostname']==='rtc.ninja')_0x48caa8=_0x444540(0xc4d);else location[_0x444540(0x879)]===_0x444540(0x829)&&(_0x48caa8=_0x444540(0x5e1));if(session[_0x444540(0x17c)])_0x48caa8+=_0x444540(0x17c),typeof session[_0x444540(0x17c)]==_0x444540(0x332)&&(_0x123161=_0x444540(0xa50)+session[_0x444540(0x17c)]);else{if(session['privacy']&&typeof session[_0x444540(0x9ae)]==_0x444540(0x332))_0x123161=_0x444540(0xa50)+session[_0x444540(0x9ae)];else try{_0x4c30f8=getStorage(_0x444540(0x3e4))||![];if(_0x4c30f8)return!session[_0x444540(0x469)]&&(session['stunServers']=[]),_0x4c30f8=processTURNs(_0x4c30f8),!_0x4c30f8&&(_0x4c30f8=[]),session['configuration']={'iceServers':session[_0x444540(0x469)],'sdpSemantics':session[_0x444540(0x8fc)]},session[_0x444540(0x9ae)]&&(session[_0x444540(0x238)][_0x444540(0x5c5)]=_0x444540(0xb18)),session['configuration'][_0x444540(0x9d2)]=session[_0x444540(0x238)]['iceServers']['concat'](_0x4c30f8),session['qosTurnAllowlist']=typeof buildQosTurnAllowlist==='function'?buildQosTurnAllowlist(_0x4c30f8):[],!![];else _0x4c30f8=[];}catch(_0x29dba7){errorlog(_0x29dba7),_0x4c30f8=[];}}return await fetchWithTimeout(_0x48caa8+_0x444540(0xacd)+_0x511cbe+_0x123161,0x7d0)[_0x444540(0x7b6)](_0x1aaa23=>_0x1aaa23['json']())['then'](function(_0x132495){var _0x3bfe54=_0x444540;_0x132495['servers'][_0x3bfe54(0x3d5)](_0xd2182a=>{var _0x554337=_0x3bfe54;try{if(session[_0x554337(0x490)]&&_0xd2182a[_0x554337(0xc54)]){}else _0x4c30f8[_0x554337(0x35c)](_0xd2182a);}catch(_0x321bfe){errorlog(_0x321bfe);}});if(isIFrame&&_0x132495[_0x3bfe54(0xbcb)]&&session[_0x3bfe54(0x17c)]&&!session[_0x3bfe54(0x6c0)])pokeIframeAPI(_0x3bfe54(0xbbd),_0x132495[_0x3bfe54(0xbcb)]);else!session['speedtest']&&setStorage(_0x3bfe54(0x3e4),_0x132495['servers'],0x1);})['catch'](function(_0x177691){var _0x3d50fd=_0x444540;warnlog(_0x177691),_0x4c30f8=[{'username':_0x3d50fd(0x89c),'credential':_0x3d50fd(0x988),'urls':['turns:www.turn.obs.ninja:443'],'tz':0x12c,'udp':![],'locale':_0x3d50fd(0x3c0)},{'username':_0x3d50fd(0x89c),'credential':_0x3d50fd(0x988),'urls':[_0x3d50fd(0x8be)],'tz':0x12c,'udp':!![],'locale':'cae1'},{'username':_0x3d50fd(0xba9),'credential':_0x3d50fd(0x669),'urls':[_0x3d50fd(0xb6e)],'tz':0x1e0,'udp':!![],'locale':_0x3d50fd(0x6f4)},{'username':_0x3d50fd(0xba9),'credential':_0x3d50fd(0x54a),'urls':[_0x3d50fd(0x9a6)],'tz':-0x46,'udp':!![],'locale':_0x3d50fd(0x710)},{'username':_0x3d50fd(0x89c),'credential':_0x3d50fd(0x988),'urls':[_0x3d50fd(0x292)],'tz':-0x3c,'udp':![],'locale':'de1'},{'username':_0x3d50fd(0x89c),'credential':'setupYourOwnPlease','urls':[_0x3d50fd(0x60b)],'tz':-0x3c,'udp':!![],'locale':_0x3d50fd(0x34e)},{'username':'vdoninja','credential':_0x3d50fd(0x275),'urls':[_0x3d50fd(0x404)],'tz':0x12c,'udp':!![],'locale':_0x3d50fd(0x802)}],_0x4c30f8=processTURNs(_0x4c30f8);}),!session[_0x444540(0x469)]&&(session['stunServers']=[]),session[_0x444540(0x238)]={'iceServers':session['stunServers'],'sdpSemantics':session[_0x444540(0x8fc)]},session[_0x444540(0x9ae)]&&(session[_0x444540(0x238)][_0x444540(0x5c5)]='relay'),!_0x4c30f8&&(_0x4c30f8=[]),session[_0x444540(0x238)][_0x444540(0x9d2)]=session[_0x444540(0x238)][_0x444540(0x9d2)][_0x444540(0x81e)](_0x4c30f8),session[_0x444540(0x314)]=typeof buildQosTurnAllowlist===_0x444540(0x5f9)?buildQosTurnAllowlist(_0x4c30f8):[],log('Remote\x20TURN\x20LIST\x20Loaded\x20**\x20'),!![];}var TURNPromise=null;async function chooseBestTURN(){var _0x5a426f=_0x380373;if(session['configuration'])return;return!TURNPromise?TURNPromise=getTURNList():warnlog(_0x5a426f(0x1cf)),await TURNPromise;}var WebRTC={};WebRTC[_0x380373(0xa31)]=(function(){var _0x120577=_0x380373,_0x435689={};function _0x58063b(){var _0x426492,_0x3ff5e8,_0x1127e4=new Promise((_0x18c418,_0x4690e7)=>{_0x426492=_0x18c418,_0x3ff5e8=_0x4690e7;});return _0x1127e4['resolve']=_0x426492,_0x1127e4['reject']=_0x3ff5e8,_0x1127e4;}_0x435689['generateStreamID']=function(_0x4b3a55=0x7){var _0x572563=_0x4fa9,_0xb94d79='',_0xb4fb45=_0x572563(0x9a7);for(var _0x2bdda9=0x0;_0x2bdda9<_0x4b3a55;_0x2bdda9++){_0xb94d79+=_0xb4fb45[_0x572563(0x1ea)](Math[_0x572563(0x8bb)](Math['random']()*_0xb4fb45[_0x572563(0xb04)]));}try{_0xb94d79=_0xb94d79['replaceAll']('AD',_0x572563(0x1e8)),_0xb94d79=_0xb94d79['replaceAll']('Ad','vdAv'),_0xb94d79=_0xb94d79[_0x572563(0xb71)]('ad',_0x572563(0x476)),_0xb94d79=_0xb94d79[_0x572563(0xb71)]('aD',_0x572563(0x8cb));}catch(_0x3925e3){errorlog(_0x3925e3);}return log(_0xb94d79),_0xb94d79;},_0x435689[_0x120577(0x1c9)]=function(_0x16236e=0x7){var _0x2192d8=_0x120577,_0x4f3552='',_0x17d1ae=[_0x2192d8(0x315),'of','to','and','a','in','is','it',_0x2192d8(0xb65),_0x2192d8(0x96d),'he',_0x2192d8(0x7e0),'for','on','are',_0x2192d8(0x66d),'as','I','his',_0x2192d8(0x6b1),'be','at',_0x2192d8(0x464),_0x2192d8(0xb72),'this',_0x2192d8(0x1f9),'or','had','by',_0x2192d8(0x43b),'but','what','some','we','can',_0x2192d8(0x4d6),_0x2192d8(0x451),'were','all','there',_0x2192d8(0xa2e),'up','use',_0x2192d8(0x580),'how',_0x2192d8(0x1d1),'an',_0x2192d8(0xc1e),_0x2192d8(0x67e),_0x2192d8(0xabd),'do','their','time','if',_0x2192d8(0x389),_0x2192d8(0x196),'about','many',_0x2192d8(0x7b6),'them','write',_0x2192d8(0x8f2),_0x2192d8(0x9fb),'so',_0x2192d8(0xa7f),_0x2192d8(0x84b),_0x2192d8(0x1e6),_0x2192d8(0x663),_0x2192d8(0x764),_0x2192d8(0x7f4),_0x2192d8(0x3a7),_0x2192d8(0x737),_0x2192d8(0x1a5),_0x2192d8(0x745),_0x2192d8(0x2fd),_0x2192d8(0x1c7),_0x2192d8(0x71b),'go','come',_0x2192d8(0x905),_0x2192d8(0x91d),_0x2192d8(0x83a),'no',_0x2192d8(0xbba),_0x2192d8(0x790),'my',_0x2192d8(0x4af),_0x2192d8(0xb42),'water',_0x2192d8(0x7ca),_0x2192d8(0x7f3),_0x2192d8(0x776),'who',_0x2192d8(0x4c9),_0x2192d8(0x67c),'side',_0x2192d8(0x656),_0x2192d8(0x1ab),_0x2192d8(0x6f7),_0x2192d8(0x6b4),_0x2192d8(0x533),'work',_0x2192d8(0x290),_0x2192d8(0x4ef),_0x2192d8(0xa1b),_0x2192d8(0xb95),'made',_0x2192d8(0x941),_0x2192d8(0x3d1),'after',_0x2192d8(0x1c4),'little','only',_0x2192d8(0x5de),_0x2192d8(0x3cd),'year',_0x2192d8(0x2eb),_0x2192d8(0xc0a),_0x2192d8(0x1a1),_0x2192d8(0x568),'me',_0x2192d8(0x738),_0x2192d8(0x4c4),_0x2192d8(0xc49),_0x2192d8(0x88c),_0x2192d8(0x5b7),'through',_0x2192d8(0x773),'form',_0x2192d8(0xb1d),'great','think',_0x2192d8(0x14a),'help',_0x2192d8(0xc43),_0x2192d8(0x3c5),_0x2192d8(0x9f3),_0x2192d8(0xaa2),_0x2192d8(0x319),_0x2192d8(0x5fa),_0x2192d8(0x5cf),_0x2192d8(0x443),'move',_0x2192d8(0x49e),'boy',_0x2192d8(0x12c),_0x2192d8(0x5db),'same','tell',_0x2192d8(0xb7b),_0x2192d8(0x50e),_0x2192d8(0xa5a),_0x2192d8(0x6d2),_0x2192d8(0x277),_0x2192d8(0xa52),_0x2192d8(0x518),_0x2192d8(0x1e1),_0x2192d8(0x2af),_0x2192d8(0x50f),_0x2192d8(0x531),_0x2192d8(0x52f),'read',_0x2192d8(0x189),_0x2192d8(0x48d),'large','spell','add',_0x2192d8(0x902),_0x2192d8(0x711),_0x2192d8(0x369),_0x2192d8(0x41d),_0x2192d8(0x423),_0x2192d8(0xb31),'such','follow','act','why',_0x2192d8(0x739),_0x2192d8(0x585),_0x2192d8(0x280),_0x2192d8(0x5d1),_0x2192d8(0x266),'kind','off',_0x2192d8(0x374),_0x2192d8(0x1e4),_0x2192d8(0x5ae),_0x2192d8(0xa14),'us',_0x2192d8(0x6ce),'animal',_0x2192d8(0x5d8),_0x2192d8(0x321),_0x2192d8(0x830),'near',_0x2192d8(0x9a2),'self',_0x2192d8(0x312),_0x2192d8(0xb73),_0x2192d8(0x4c8),_0x2192d8(0x87d),'own',_0x2192d8(0x874),_0x2192d8(0x7f0),'country',_0x2192d8(0xb46),_0x2192d8(0x34a),_0x2192d8(0xc2f),'grow',_0x2192d8(0xc4a),_0x2192d8(0x55d),_0x2192d8(0x27f),'plant',_0x2192d8(0x2c8),_0x2192d8(0x8ab),_0x2192d8(0xb5e),_0x2192d8(0x45d),_0x2192d8(0x5ed),_0x2192d8(0x203),'keep','eye',_0x2192d8(0xb74),'last',_0x2192d8(0x121),_0x2192d8(0xc5a),_0x2192d8(0x60e),'tree',_0x2192d8(0x1df),'farm','hard',_0x2192d8(0x8ed),_0x2192d8(0xa2b),_0x2192d8(0x4a6),_0x2192d8(0xa7a),'far','sea',_0x2192d8(0x393),_0x2192d8(0x65d),'late',_0x2192d8(0x558),_0x2192d8(0xb57),'while',_0x2192d8(0x236),_0x2192d8(0x757),_0x2192d8(0xb36),_0x2192d8(0x24e),_0x2192d8(0x30a),_0x2192d8(0x171),_0x2192d8(0x94b),_0x2192d8(0x379),_0x2192d8(0x54b),_0x2192d8(0xc58),_0x2192d8(0x96a),'white',_0x2192d8(0xc41),_0x2192d8(0x372),_0x2192d8(0x90f),_0x2192d8(0x85c),_0x2192d8(0xb3c),'ease',_0x2192d8(0x13e),_0x2192d8(0x1cd),_0x2192d8(0x5a5),_0x2192d8(0x74a),_0x2192d8(0x39a),_0x2192d8(0x6b7),_0x2192d8(0xae5),_0x2192d8(0x2b0),'letter',_0x2192d8(0x727),'mile',_0x2192d8(0xc71),_0x2192d8(0x5a0),_0x2192d8(0x28a),_0x2192d8(0x997),_0x2192d8(0xa88),_0x2192d8(0x4e3),_0x2192d8(0x990),_0x2192d8(0x84a),'science',_0x2192d8(0xa40),_0x2192d8(0x6d9),_0x2192d8(0x6b3),_0x2192d8(0xb5a),_0x2192d8(0x64c),_0x2192d8(0x122),_0x2192d8(0x56c),'stop','once','base',_0x2192d8(0xbc6),'horse',_0x2192d8(0x640),_0x2192d8(0x5ba),_0x2192d8(0x7ee),_0x2192d8(0x338),_0x2192d8(0xb61),_0x2192d8(0x59c),_0x2192d8(0x510),_0x2192d8(0x5c9),_0x2192d8(0x230),'girl','usual',_0x2192d8(0xa2a),_0x2192d8(0x80e),_0x2192d8(0x648),_0x2192d8(0x45c),_0x2192d8(0x1a9),_0x2192d8(0x7d4),_0x2192d8(0x262),_0x2192d8(0x6f9),_0x2192d8(0x9f2),'bird',_0x2192d8(0x63a),_0x2192d8(0x279),'dog',_0x2192d8(0xb8d),_0x2192d8(0x244),'pose',_0x2192d8(0xb2a),'song',_0x2192d8(0xa69),_0x2192d8(0x544),_0x2192d8(0x52d),_0x2192d8(0xc32),_0x2192d8(0xac1),_0x2192d8(0x25a),_0x2192d8(0x576),_0x2192d8(0x46c),_0x2192d8(0x55c),'happen',_0x2192d8(0x539),_0x2192d8(0x252),'area',_0x2192d8(0x2ff),_0x2192d8(0x47f),'order',_0x2192d8(0x318),_0x2192d8(0x32e),_0x2192d8(0x7cb),_0x2192d8(0xb3f),_0x2192d8(0x5c1),'knew',_0x2192d8(0x9b1),_0x2192d8(0xc0b),_0x2192d8(0x5f3),'whole',_0x2192d8(0x44c),_0x2192d8(0x158),'heard',_0x2192d8(0x4b4),_0x2192d8(0xc28),_0x2192d8(0xb32),_0x2192d8(0xad8),_0x2192d8(0x87a),'five',_0x2192d8(0xab6),_0x2192d8(0x6e7),_0x2192d8(0x594),'hold',_0x2192d8(0x8af),_0x2192d8(0x140),_0x2192d8(0x507),_0x2192d8(0x28d),'fast',_0x2192d8(0x8e2),'sing',_0x2192d8(0x681),_0x2192d8(0x5c7),_0x2192d8(0xb87),_0x2192d8(0xaa4),_0x2192d8(0x9af),'morning','ten',_0x2192d8(0x994),_0x2192d8(0x9f4),_0x2192d8(0x808),_0x2192d8(0x3fd),_0x2192d8(0x278),_0x2192d8(0x3a5),_0x2192d8(0x5f0),_0x2192d8(0x691),_0x2192d8(0xa30),_0x2192d8(0xb2b),'love',_0x2192d8(0x556),_0x2192d8(0x30d),_0x2192d8(0x6a8),_0x2192d8(0x5ef),_0x2192d8(0x922),_0x2192d8(0x2da),_0x2192d8(0xb05),'rule',_0x2192d8(0x547),_0x2192d8(0x13d),_0x2192d8(0x96e),_0x2192d8(0x2ca),'voice',_0x2192d8(0xa2f),'power',_0x2192d8(0x324),'fine',_0x2192d8(0x986),_0x2192d8(0x390),'fall',_0x2192d8(0xb77),_0x2192d8(0x63b),'dark',_0x2192d8(0x354),'note','wait',_0x2192d8(0x478),_0x2192d8(0x7de),_0x2192d8(0x619),'box',_0x2192d8(0x9f6),_0x2192d8(0x763),'rest','correct',_0x2192d8(0xaf3),_0x2192d8(0x35d),_0x2192d8(0x993),_0x2192d8(0x955),_0x2192d8(0x2f1),_0x2192d8(0x2df),_0x2192d8(0x7be),'front',_0x2192d8(0xa0b),_0x2192d8(0x919),'final',_0x2192d8(0x243),_0x2192d8(0x2e2),'oh',_0x2192d8(0x95d),_0x2192d8(0x4f3),_0x2192d8(0x2bb),_0x2192d8(0x36b),_0x2192d8(0xa07),_0x2192d8(0x223),_0x2192d8(0x5b5),_0x2192d8(0x76c),_0x2192d8(0x590),_0x2192d8(0x221),_0x2192d8(0x761),'tail',_0x2192d8(0x160),_0x2192d8(0x249),_0x2192d8(0x89d),_0x2192d8(0x571),_0x2192d8(0xc39),_0x2192d8(0x83f),_0x2192d8(0x672),'stay',_0x2192d8(0x9c7),_0x2192d8(0x748),_0x2192d8(0x706),_0x2192d8(0x600),'object',_0x2192d8(0x3f3),'surface',_0x2192d8(0xa3d),_0x2192d8(0x3b6),_0x2192d8(0x420),_0x2192d8(0x589),_0x2192d8(0x18e),_0x2192d8(0x199),_0x2192d8(0x715),_0x2192d8(0x6fa),_0x2192d8(0x9c8),'common',_0x2192d8(0x7b9),'possible',_0x2192d8(0x482),_0x2192d8(0x8ad),'dry',_0x2192d8(0xbe2),_0x2192d8(0x77d),_0x2192d8(0x363),_0x2192d8(0xc2b),_0x2192d8(0x235),_0x2192d8(0x5d2),_0x2192d8(0xad6),_0x2192d8(0x4c7),_0x2192d8(0x5b0),_0x2192d8(0x910),_0x2192d8(0x473),_0x2192d8(0x944),'heat',_0x2192d8(0x621),_0x2192d8(0x951),_0x2192d8(0x11f),_0x2192d8(0xc31),_0x2192d8(0x163),'fill',_0x2192d8(0x7cf),_0x2192d8(0x2b3),_0x2192d8(0x9ff),_0x2192d8(0x164),_0x2192d8(0x46b),'ball',_0x2192d8(0x343),'wave',_0x2192d8(0x7c7),'heart','am',_0x2192d8(0x3e1),_0x2192d8(0x718),_0x2192d8(0x295),_0x2192d8(0x11d),_0x2192d8(0x8b3),'arm','wide',_0x2192d8(0x16f),'material',_0x2192d8(0x735),'vary',_0x2192d8(0x92a),_0x2192d8(0x3a2),_0x2192d8(0x1ee),'general',_0x2192d8(0x9ad),'matter',_0x2192d8(0x36d),'pair',_0x2192d8(0xa0a),_0x2192d8(0xac9),'syllable','felt',_0x2192d8(0x7d0),'pick',_0x2192d8(0xadd),'count','square',_0x2192d8(0x6a2),_0x2192d8(0xb04),_0x2192d8(0xa33),_0x2192d8(0x38f),_0x2192d8(0x7e2),_0x2192d8(0xc1c),_0x2192d8(0x535),_0x2192d8(0x8b8),_0x2192d8(0x6d8),_0x2192d8(0xaf1),_0x2192d8(0x722),_0x2192d8(0x573),_0x2192d8(0x3f9),'cell',_0x2192d8(0x33e),'fraction',_0x2192d8(0x744),_0x2192d8(0x274),_0x2192d8(0x6e6),_0x2192d8(0x82f),_0x2192d8(0x4cf),_0x2192d8(0x2cb),_0x2192d8(0xa1e),_0x2192d8(0x610),'prove',_0x2192d8(0x775),_0x2192d8(0x5b8),_0x2192d8(0x297),_0x2192d8(0xa21),_0x2192d8(0x501),'mount',_0x2192d8(0xbb4),_0x2192d8(0x837),_0x2192d8(0x3a3),_0x2192d8(0xbff),_0x2192d8(0x8e3),_0x2192d8(0x2fa),'written',_0x2192d8(0x4c3),_0x2192d8(0x245),_0x2192d8(0x851),'glass','grass','cow','job',_0x2192d8(0x649),_0x2192d8(0x9d0),_0x2192d8(0x1be),_0x2192d8(0x8f3),_0x2192d8(0x54e),_0x2192d8(0x39b),_0x2192d8(0x930),'gas',_0x2192d8(0x673),_0x2192d8(0xb66),'million',_0x2192d8(0x9cc),_0x2192d8(0xb81),_0x2192d8(0x876),_0x2192d8(0x9a3),_0x2192d8(0x4cc),_0x2192d8(0xb4c),_0x2192d8(0x178),_0x2192d8(0xc67),'jump',_0x2192d8(0x3d9),_0x2192d8(0x251),'village',_0x2192d8(0x32a),_0x2192d8(0xa99),_0x2192d8(0x447),_0x2192d8(0x60f),'solve',_0x2192d8(0x368),_0x2192d8(0x366),'push','seven',_0x2192d8(0xb84),_0x2192d8(0xaae),_0x2192d8(0x94a),_0x2192d8(0x497),_0x2192d8(0x760),'describe','cook',_0x2192d8(0x8bb),_0x2192d8(0x892),_0x2192d8(0x6d1),_0x2192d8(0xc5b),_0x2192d8(0x51a),_0x2192d8(0x99d),_0x2192d8(0x460),_0x2192d8(0xb51),_0x2192d8(0x607),_0x2192d8(0x2dd),'law',_0x2192d8(0x2b2),_0x2192d8(0x3ed),_0x2192d8(0xb3a),_0x2192d8(0x6ec),'silent',_0x2192d8(0xbb6),_0x2192d8(0x5b3),_0x2192d8(0xb25),'roll',_0x2192d8(0x2f2),_0x2192d8(0x5a3),_0x2192d8(0x248),_0x2192d8(0x62d),_0x2192d8(0x783),_0x2192d8(0x428),_0x2192d8(0x19d),'excite',_0x2192d8(0x37f),_0x2192d8(0x6c0),'sense',_0x2192d8(0x21f),_0x2192d8(0x90d),_0x2192d8(0x4d4),'broke',_0x2192d8(0x72a),_0x2192d8(0x3e9),_0x2192d8(0x3db),_0x2192d8(0x5e9),'lake',_0x2192d8(0x683),_0x2192d8(0x61b),_0x2192d8(0x8d9),_0x2192d8(0x32f),_0x2192d8(0x675),_0x2192d8(0x445),_0x2192d8(0xc46),'consonant',_0x2192d8(0x719),_0x2192d8(0x962),_0x2192d8(0x8a7),_0x2192d8(0x394),_0x2192d8(0x9f8),'organ','pay',_0x2192d8(0x345),_0x2192d8(0x958),_0x2192d8(0x6c2),_0x2192d8(0x2cc),_0x2192d8(0x68e),_0x2192d8(0x534),'stone',_0x2192d8(0x4fc),_0x2192d8(0xc5f),_0x2192d8(0x563),'design','poor',_0x2192d8(0x455),_0x2192d8(0x85b),_0x2192d8(0x2ae),_0x2192d8(0x304),_0x2192d8(0x90e),'single',_0x2192d8(0x1ac),_0x2192d8(0x4dd),_0x2192d8(0xb7f),_0x2192d8(0x9a9),_0x2192d8(0x3df),'crease',_0x2192d8(0x9fc),'trade','melody',_0x2192d8(0x4f9),'office','receive',_0x2192d8(0xc47),'mouth',_0x2192d8(0x82c),_0x2192d8(0x911),_0x2192d8(0x59b),'least',_0x2192d8(0x7b4),_0x2192d8(0x4b3),_0x2192d8(0x707),_0x2192d8(0x5c6),'seed',_0x2192d8(0x56b),_0x2192d8(0x28e),_0x2192d8(0x19c),_0x2192d8(0x206),_0x2192d8(0x5a7),'lady',_0x2192d8(0x281),_0x2192d8(0xb02),_0x2192d8(0x999),_0x2192d8(0x64b),'oil','blood',_0x2192d8(0x6a3),_0x2192d8(0x2ac),_0x2192d8(0x498),_0x2192d8(0xab9),'team',_0x2192d8(0xbca),_0x2192d8(0x4db),_0x2192d8(0x46f),_0x2192d8(0xad0),_0x2192d8(0x5ea),'garden','equal',_0x2192d8(0x380),_0x2192d8(0x29a),_0x2192d8(0x50c),'fit',_0x2192d8(0xa5d),_0x2192d8(0xbf7),_0x2192d8(0x554),'collect',_0x2192d8(0x492),'control',_0x2192d8(0x382),_0x2192d8(0x50d),_0x2192d8(0xad1),_0x2192d8(0x5ff),'practice','separate',_0x2192d8(0x300),_0x2192d8(0x351),_0x2192d8(0x75c),_0x2192d8(0x8fb),_0x2192d8(0x91f),_0x2192d8(0x643),_0x2192d8(0xb7d),'ring',_0x2192d8(0xa2c),_0x2192d8(0xc77),_0x2192d8(0x8ee),_0x2192d8(0x9d3),'indicate','radio',_0x2192d8(0x5fe),_0x2192d8(0xa8e),'human',_0x2192d8(0x21c),_0x2192d8(0x53a),_0x2192d8(0xc64),_0x2192d8(0x69f),_0x2192d8(0x713),_0x2192d8(0x2f8),_0x2192d8(0x355),_0x2192d8(0x16b),'student',_0x2192d8(0x1d0),_0x2192d8(0x129),_0x2192d8(0xc34),_0x2192d8(0xc45),_0x2192d8(0x378),_0x2192d8(0xc76),_0x2192d8(0x5f8),_0x2192d8(0xb0e),'thus',_0x2192d8(0x793),_0x2192d8(0x921),'chair','danger','fruit',_0x2192d8(0x548),'thick',_0x2192d8(0x31e),_0x2192d8(0x154),_0x2192d8(0x889),_0x2192d8(0x8ef),'necessary','sharp','wing',_0x2192d8(0x225),_0x2192d8(0xa48),_0x2192d8(0x175),'bat','rather',_0x2192d8(0x786),_0x2192d8(0x1d6),_0x2192d8(0x15b),'poem',_0x2192d8(0x332),'bell',_0x2192d8(0x2b1),_0x2192d8(0x2ab),_0x2192d8(0x92d),'tube',_0x2192d8(0x434),_0x2192d8(0x7c9),_0x2192d8(0xc2e),_0x2192d8(0x457),_0x2192d8(0x45e),_0x2192d8(0x82a),_0x2192d8(0x150),'planet',_0x2192d8(0x6ed),_0x2192d8(0x3c1),_0x2192d8(0x416),_0x2192d8(0x6f6),_0x2192d8(0x588),_0x2192d8(0x2d9),_0x2192d8(0x3a6),_0x2192d8(0x384),'fresh',_0x2192d8(0x78c),_0x2192d8(0xac5),_0x2192d8(0xc48),_0x2192d8(0x9c9),_0x2192d8(0x7a9),'print','dead',_0x2192d8(0x6fb),_0x2192d8(0x688),_0x2192d8(0xc27),'current',_0x2192d8(0x8b4),_0x2192d8(0x34d),_0x2192d8(0x789),_0x2192d8(0x59f),_0x2192d8(0xb1e),'hat','sell',_0x2192d8(0x684),_0x2192d8(0x397),_0x2192d8(0x659),_0x2192d8(0x7b0),'particular',_0x2192d8(0x8b0),'swim',_0x2192d8(0x43d),'opposite',_0x2192d8(0x2fb),'shoe',_0x2192d8(0x60d),_0x2192d8(0x9e6),'arrange','camp',_0x2192d8(0x508),_0x2192d8(0xa76),_0x2192d8(0x2d4),_0x2192d8(0x303),_0x2192d8(0x1c3),_0x2192d8(0x7a2),'truck',_0x2192d8(0x982),_0x2192d8(0xc1d),_0x2192d8(0xa97),_0x2192d8(0x459),_0x2192d8(0x61f),_0x2192d8(0xb7c),_0x2192d8(0x4cb),_0x2192d8(0x270),_0x2192d8(0x665),_0x2192d8(0x216),'molecule',_0x2192d8(0x756),_0x2192d8(0x84e),'gray',_0x2192d8(0x14e),_0x2192d8(0x260),_0x2192d8(0x799),_0x2192d8(0x257),_0x2192d8(0x27e),_0x2192d8(0x76a),_0x2192d8(0xafb),_0x2192d8(0x615),'claim',_0x2192d8(0x8fd),_0x2192d8(0x966),'sugar',_0x2192d8(0xc74),'pretty','skill',_0x2192d8(0x6b5),'season',_0x2192d8(0xad5),_0x2192d8(0x813),_0x2192d8(0x833),_0x2192d8(0xa87),_0x2192d8(0x623),'match','suffix',_0x2192d8(0x779),'fig','afraid',_0x2192d8(0x687),_0x2192d8(0xab4),_0x2192d8(0x98c),_0x2192d8(0x8eb),_0x2192d8(0xc6f),_0x2192d8(0x891),_0x2192d8(0x94f),_0x2192d8(0xa3e),'score',_0x2192d8(0x7d6),'bought',_0x2192d8(0x76d),_0x2192d8(0x7cc),_0x2192d8(0xc55),_0x2192d8(0xc21),_0x2192d8(0x172),_0x2192d8(0xab8),_0x2192d8(0x5a2),_0x2192d8(0x12b),_0x2192d8(0x1a8),_0x2192d8(0xa58),_0x2192d8(0x2f0),_0x2192d8(0x769),_0x2192d8(0xa9f),'tool',_0x2192d8(0x894),_0x2192d8(0x240),_0x2192d8(0x549),_0x2192d8(0x8ce),_0x2192d8(0xbbc),_0x2192d8(0x1e3),_0x2192d8(0x5fd),'arrive',_0x2192d8(0xafe),_0x2192d8(0xb26),_0x2192d8(0x85a),_0x2192d8(0x23d),_0x2192d8(0x3c4),_0x2192d8(0x7ff),_0x2192d8(0x98b),_0x2192d8(0x878),_0x2192d8(0x913),_0x2192d8(0x44d),_0x2192d8(0x971),'chord','fat',_0x2192d8(0x1ad),_0x2192d8(0x1fe),_0x2192d8(0x450),_0x2192d8(0x577),_0x2192d8(0x20d),_0x2192d8(0xb9d),_0x2192d8(0xaec),_0x2192d8(0x6b0),_0x2192d8(0x38d),_0x2192d8(0xb78),_0x2192d8(0x8ec),_0x2192d8(0x41b),_0x2192d8(0x979),_0x2192d8(0x4f6),_0x2192d8(0x4ce),_0x2192d8(0x73c),_0x2192d8(0x57f),'chick',_0x2192d8(0x2f4),_0x2192d8(0x3aa),'reply',_0x2192d8(0x74b),_0x2192d8(0x2e3),_0x2192d8(0x8d3),'speech',_0x2192d8(0x362),_0x2192d8(0x7b5),_0x2192d8(0xb19),_0x2192d8(0x472),_0x2192d8(0x7b8),'liquid',_0x2192d8(0x2c6),_0x2192d8(0x546),_0x2192d8(0xa75),'teeth',_0x2192d8(0x6d7),_0x2192d8(0x9e8)];for(var _0x4a7211=0x0;_0x4a7211<0x2;_0x4a7211++){try{var _0x408a93=parseInt(Math[_0x2192d8(0xb64)]()*0x3e8);_0x4f3552+=_0x17d1ae[_0x408a93];}catch(_0x421516){}}var _0x4cb89e=_0x2192d8(0x9a7);_0x4f3552+=_0x4cb89e[_0x2192d8(0x1ea)](Math['floor'](Math[_0x2192d8(0xb64)]()*_0x4cb89e[_0x2192d8(0xb04)]));while(_0x4f3552['length']<_0x16236e){_0x4f3552+=_0x4cb89e[_0x2192d8(0x1ea)](Math[_0x2192d8(0x8bb)](Math[_0x2192d8(0xb64)]()*_0x4cb89e[_0x2192d8(0xb04)]));}try{_0x4f3552=_0x4f3552[_0x2192d8(0xb71)]('AD',_0x2192d8(0x1e8)),_0x4f3552=_0x4f3552[_0x2192d8(0xb71)]('Ad',_0x2192d8(0x67f)),_0x4f3552=_0x4f3552[_0x2192d8(0xb71)]('ad','vdav'),_0x4f3552=_0x4f3552[_0x2192d8(0xb71)]('aD','vDav');}catch(_0x702ccd){errorlog(_0x702ccd);}return log(_0x4f3552),_0x4f3552;},_0x435689[_0x120577(0x4e1)]='wss://api.vdo.ninja:443',_0x435689[_0x120577(0x220)]=null,_0x435689[_0x120577(0xc78)]=![],_0x435689[_0x120577(0x333)]=![],_0x435689[_0x120577(0x316)]=![],_0x435689[_0x120577(0x671)]=![],_0x435689[_0x120577(0x2ee)]=null,_0x435689[_0x120577(0x75b)]=0xbb8,_0x435689[_0x120577(0x8ff)]=![],_0x435689['activelySpeaking']=!![],_0x435689['audiobitrate']=![],_0x435689['audiobitratePRO']=0x100,_0x435689[_0x120577(0x8ca)]=0x64,_0x435689[_0x120577(0xb9c)]=0x8,_0x435689[_0x120577(0x7d7)]=![],_0x435689[_0x120577(0x8db)]=![],_0x435689['alreadyJoinedMembers']=![],_0x435689['allowScreen']=![],_0x435689[_0x120577(0x550)]=![],_0x435689[_0x120577(0x432)]=null,_0x435689[_0x120577(0xaca)]=null,_0x435689[_0x120577(0x62e)]=![],_0x435689['allowDrawing']=![],_0x435689[_0x120577(0x6a0)]=![],_0x435689[_0x120577(0x208)]=![],_0x435689[_0x120577(0x794)]=[],_0x435689[_0x120577(0x13a)]=![],_0x435689['autoadd']=![],_0x435689['autochannels']=![],_0x435689[_0x120577(0x8d8)]=_0x120577(0x827),_0x435689[_0x120577(0x8a5)]=0x0,_0x435689[_0x120577(0x222)]=![],_0x435689[_0x120577(0x883)]=![],_0x435689[_0x120577(0xb53)]=![],_0x435689['audioConstraints']={},_0x435689['audioMeterGuest']=!![],_0x435689[_0x120577(0x205)]=null,_0x435689[_0x120577(0xb52)]=![],_0x435689[_0x120577(0x836)]=![],_0x435689[_0x120577(0x925)]=![],_0x435689[_0x120577(0xbfc)]=![],_0x435689[_0x120577(0x9be)]=![],_0x435689[_0x120577(0x26f)]=![],_0x435689[_0x120577(0xb4e)]=![],_0x435689[_0x120577(0x97b)]=new Set([]),_0x435689['activatedStreamsQueue']={},_0x435689[_0x120577(0x5d5)]=new Set([]),_0x435689[_0x120577(0x49b)]=new Set([]),_0x435689[_0x120577(0x7e9)]=new Set([]);function _0x150827(){return;}_0x435689[_0x120577(0x20e)]=function(_0x4d8405){var _0xf50a37=_0x120577;try{if(!_0x435689[_0xf50a37(0x781)]||!_0x435689['approval_popup'])return;var _0x3443b4=(''+_0xf50a37(0x396)+_0x4d8405)[_0xf50a37(0x586)](/["<>]/g,'');if(document[_0xf50a37(0x413)](_0xf50a37(0xbd6)+_0x3443b4+'\x22]'))return;var _0x720c27=_0x435689[_0xf50a37(0x3e3)]!==![];if(_0x435689['beepToNotify'])try{var _0x5577e0=_0x435689[_0xf50a37(0xb56)]?'knocktone':_0xf50a37(0x322);playtone(![],_0x5577e0);}catch(_0xfa11de){errorlog(_0xfa11de);}var _0x286039=_0x435689[_0xf50a37(0xc6c)][_0x4d8405]&&_0x435689[_0xf50a37(0xc6c)][_0x4d8405][_0xf50a37(0xc69)]||_0xf50a37(0x47b)+_0x4d8405[_0xf50a37(0x7f8)](0x0,0x8),_0x40abb4=_0x435689[_0xf50a37(0xc6c)][_0x4d8405]&&_0x435689[_0xf50a37(0xc6c)][_0x4d8405][_0xf50a37(0x885)]||_0x4d8405;try{_0x286039=(''+_0x286039)[_0xf50a37(0x586)](/[<>]/g,''),_0x40abb4=(''+_0x40abb4)[_0xf50a37(0x586)](/[<>]/g,'');}catch(_0x263852){}var _0x537483=_0xf50a37(0x62f)+_0xf50a37(0x46e)+_0x286039+'\x0a'+'ID:\x20'+_0x40abb4+'\x0a\x0a'+_0xf50a37(0xb1f);confirmAlt(_0x537483,![],_0xf50a37(0x396)+_0x4d8405)[_0xf50a37(0x7b6)](function(_0x50e14d){var _0x2b5678=_0xf50a37;if(_0x50e14d)try{_0x720c27?(_0x435689[_0x2b5678(0x4da)](_0x435689[_0x2b5678(0xb0b)],{'justResetting':!![]},_0x4d8405),_0x435689[_0x2b5678(0xb7a)](_0x4d8405,![],'approval-accept')):_0x435689[_0x2b5678(0x4da)](_0x435689['roomid'],{'justResetting':!![]},_0x4d8405);}catch(_0xf89f09){errorlog(_0xf89f09);}else confirmAlt(_0x2b5678(0x864))['then'](function(_0x21a3e6){var _0x4a8d65=_0x2b5678;if(_0x21a3e6)try{_0x435689[_0x4a8d65(0x43f)]({'hangup':!![]},_0x4d8405),_0x435689[_0x4a8d65(0xb7a)](_0x4d8405,![],_0x4a8d65(0x54d));}catch(_0x200a39){errorlog(_0x200a39);}});});}catch(_0xb4e1cb){errorlog(_0xb4e1cb);}},_0x435689[_0x120577(0xaa3)]=function(){return;},_0x435689[_0x120577(0x52b)]=function(){return;},_0x435689['audioCtx']=new AudioContext(),_0x435689[_0x120577(0xab2)]=![],_0x435689[_0x120577(0x85f)]=![],_0x435689[_0x120577(0x90a)]=![],_0x435689[_0x120577(0xa55)]=null,_0x435689[_0x120577(0x3fc)]=null,_0x435689[_0x120577(0xaa8)]=null,_0x435689[_0x120577(0x14f)]=null,_0x435689[_0x120577(0x532)]=![],_0x435689['broadcastChannel']=![],_0x435689[_0x120577(0x2bf)]=![],_0x435689[_0x120577(0x545)]=![],_0x435689[_0x120577(0x639)]=![],_0x435689['screenshareDenoise']=null,_0x435689[_0x120577(0x97a)]=null,_0x435689[_0x120577(0xbd1)]=null,_0x435689[_0x120577(0x42f)]=![],_0x435689[_0x120577(0xc22)]=![],_0x435689[_0x120577(0x9c0)]=0x0,_0x435689[_0x120577(0x80d)]=0x0,_0x435689[_0x120577(0x311)]='#000',_0x435689[_0x120577(0x3dc)]=0x0,_0x435689[_0x120577(0x570)]=![],_0x435689['bigmutebutton']=![],_0x435689['broadcastTransfer']=null,_0x435689['bitrate']=![],_0x435689[_0x120577(0x1b7)]=![],_0x435689[_0x120577(0x237)]=![];typeof _0x435689[_0x120577(0x9b7)]==='undefined'&&(_0x435689[_0x120577(0x9b7)]=![]);if(!_0x435689[_0x120577(0x9b7)]&&typeof window!=='undefined')try{var _0x4727a0=new URLSearchParams(window[_0x120577(0x652)]['search']||'');_0x4727a0[_0x120577(0x1a5)]('buffer2')&&(_0x435689[_0x120577(0x9b7)]=!![]);}catch(_0x247cd4){}_0x435689['badStreamList']=[],_0x435689[_0x120577(0x307)]=null,_0x435689[_0x120577(0x192)]=![],_0x435689[_0x120577(0x59a)]=![],_0x435689[_0x120577(0x97d)]=null,_0x435689[_0x120577(0xa8d)]=null,_0x435689[_0x120577(0x91b)]=null,_0x435689[_0x120577(0x1dd)]=null,_0x435689[_0x120577(0x3b7)]=![],_0x435689['controlRoomBitrate']=![],_0x435689[_0x120577(0x7ba)]=![],_0x435689['cleanDirector']=![],_0x435689['cleanOutput']=![],_0x435689[_0x120577(0x385)]=![],_0x435689[_0x120577(0x842)]=![],_0x435689[_0x120577(0x238)]=![],_0x435689['compressor']=![],_0x435689[_0x120577(0x8a1)]=![],_0x435689['contentHint']='',_0x435689['audioContentHint']='',_0x435689[_0x120577(0xa4d)]='',_0x435689[_0x120577(0x844)]=![],_0x435689[_0x120577(0x1a3)]=![],_0x435689['preferVideoCodec']=![],_0x435689['h264profile']=null,_0x435689[_0x120577(0xa4e)]=![],_0x435689['showTips']=![],_0x435689['tipQRSize']=0x96,_0x435689[_0x120577(0x64f)]=![],_0x435689[_0x120577(0x58b)]=null,_0x435689[_0x120577(0x676)]=![],_0x435689[_0x120577(0xb39)]=0x1,_0x435689[_0x120577(0x2c8)]=![],_0x435689['chatbutton']=null,_0x435689[_0x120577(0x520)]={},_0x435689[_0x120577(0x6a9)]=![],_0x435689[_0x120577(0x60a)]=!![],_0x435689[_0x120577(0xb35)]=[],_0x435689[_0x120577(0x1d2)]={},_0x435689[_0x120577(0x826)]=![],_0x435689['chunkedDetails']=![],_0x435689['chunkedVideoEnabled']=null,_0x435689[_0x120577(0x3c9)]=null,_0x435689[_0x120577(0xc2a)]=![],_0x435689[_0x120577(0xb8f)]=![],_0x435689[_0x120577(0x961)]=![],_0x435689[_0x120577(0x8a8)]=!![],_0x435689['disableIpv6']=![],_0x435689[_0x120577(0x9ff)]=![],_0x435689[_0x120577(0xaad)]={},_0x435689[_0x120577(0x579)]={},_0x435689['colorVideosBackground']=![],_0x435689['hiddenSceneViewBitrate']=0x0,_0x435689['zoomedBitrate']=0x25a,_0x435689[_0x120577(0x29c)]=![],_0x435689[_0x120577(0x6fc)]=![],_0x435689[_0x120577(0x3f4)]=![],_0x435689['defaultPassword']=![],_0x435689[_0x120577(0x6c5)]=![],_0x435689['showControls']=null,_0x435689[_0x120577(0x3c2)]=![],_0x435689[_0x120577(0x559)]=![],_0x435689[_0x120577(0x28f)]=![],_0x435689['dedicatedControlBarSpace']=null,_0x435689[_0x120577(0x781)]=![],_0x435689[_0x120577(0xa1f)]=![],_0x435689[_0x120577(0x254)]=![],_0x435689[_0x120577(0x972)]=![],_0x435689['defaultOverlayMedia']=![],_0x435689[_0x120577(0xbc4)]=![],_0x435689['directorChat']=![],_0x435689[_0x120577(0xa9a)]=0x23,_0x435689[_0x120577(0x965)]=![],_0x435689[_0x120577(0x8f9)]=null,_0x435689[_0x120577(0x310)]=null,_0x435689[_0x120577(0xc19)]=[],_0x435689['directorPassword']=![],_0x435689['directorHash']=![],_0x435689[_0x120577(0xb2f)]=![],_0x435689[_0x120577(0x9c4)]=![],_0x435689['directorState']=null,_0x435689[_0x120577(0x173)]=![],_0x435689['dynamicScale']=!![],_0x435689[_0x120577(0x9ab)]=null,_0x435689[_0x120577(0x5d0)]=![],_0x435689[_0x120577(0x1d5)]=![],_0x435689[_0x120577(0x53a)]=![],_0x435689[_0x120577(0x15a)]=![],_0x435689[_0x120577(0x512)]=![],_0x435689[_0x120577(0x45a)]=![],_0x435689['experimental']=![],_0x435689['fakeFeeds']=![],_0x435689[_0x120577(0xbf0)]=![],_0x435689[_0x120577(0x286)]=![],_0x435689[_0x120577(0xb08)]=![],_0x435689[_0x120577(0x517)]=![],_0x435689[_0x120577(0x166)]=![],_0x435689['pushEffectsData']=![],_0x435689[_0x120577(0xbe0)]=0x384,_0x435689[_0x120577(0x9ba)]=![],_0x435689[_0x120577(0x258)]=new TextEncoder('utf-8'),_0x435689[_0x120577(0x937)]=![],_0x435689[_0x120577(0x940)]=![],_0x435689[_0x120577(0x130)]=![],_0x435689[_0x120577(0x399)]=![],_0x435689[_0x120577(0xbae)]=![],_0x435689[_0x120577(0x5e3)]=![],_0x435689[_0x120577(0xb6a)]=![],_0x435689[_0x120577(0x690)]=[],_0x435689[_0x120577(0x569)]=[],_0x435689['automute']=![],_0x435689[_0x120577(0x5da)]=null,_0x435689[_0x120577(0x58c)]=![],_0x435689[_0x120577(0xbab)]=![],_0x435689[_0x120577(0x8d5)]=![],_0x435689['frameRate']=![],_0x435689[_0x120577(0x41f)]=![],_0x435689[_0x120577(0x540)]=![],_0x435689[_0x120577(0x18c)]=null,_0x435689[_0x120577(0x58e)]=![],_0x435689[_0x120577(0x337)]=![],_0x435689['forceMediaSettings']=![],_0x435689[_0x120577(0x480)]=![],_0x435689[_0x120577(0xa04)]=![],_0x435689[_0x120577(0x529)]=null,_0x435689[_0x120577(0x1cd)]=[],_0x435689['groupView']=[],_0x435689['allowNoGroup']=![],_0x435689[_0x120577(0x9f9)]=![],_0x435689[_0x120577(0xc5e)]=null,_0x435689['grabFaceData']=![],_0x435689[_0x120577(0x313)]=![],_0x435689[_0x120577(0x21b)]=![],_0x435689[_0x120577(0x348)]=![],_0x435689[_0x120577(0xafd)]=![],_0x435689[_0x120577(0x964)]=![],_0x435689['encodedInsertableStreams']=![],_0x435689[_0x120577(0x747)]=![],_0x435689[_0x120577(0x469)]=[{'urls':[_0x120577(0x5f7),_0x120577(0x611)]}],_0x435689[_0x120577(0xa6b)]=![],_0x435689[_0x120577(0xa0a)]=[],_0x435689[_0x120577(0xc26)]={},_0x435689[_0x120577(0x9c5)]=![],_0x435689['flagship']=![],_0x435689[_0x120577(0x92e)]=![],_0x435689[_0x120577(0x987)]=0x1,_0x435689[_0x120577(0x4b8)]=0x0,_0x435689['quality_ss']=![],_0x435689[_0x120577(0x381)]=![],_0x435689['icefilter']=![],_0x435689[_0x120577(0xa90)]=![],_0x435689[_0x120577(0x6eb)]=![],_0x435689['infocusForceMode']=![],_0x435689[_0x120577(0x45f)]=![],_0x435689[_0x120577(0x7af)]={},_0x435689[_0x120577(0xc62)]=![],_0x435689['label']=![],_0x435689['keyframeRate']=![],_0x435689[_0x120577(0x161)]={},_0x435689[_0x120577(0xc53)]=[],_0x435689[_0x120577(0xb29)]=![],_0x435689[_0x120577(0x936)]=![],_0x435689['notifyScreenShare']=!![],_0x435689[_0x120577(0x370)]=0x1,_0x435689[_0x120577(0x8d7)]={},_0x435689[_0x120577(0x977)]=![],_0x435689['micIsolated']=[],_0x435689[_0x120577(0x906)]=![],_0x435689[_0x120577(0x89e)]=![],_0x435689['maxpublishers']=![],_0x435689[_0x120577(0x9e7)]=![],_0x435689[_0x120577(0x7c1)]=![],_0x435689[_0x120577(0x1de)]=![],_0x435689[_0x120577(0x6a1)]=![],_0x435689[_0x120577(0x612)]=![],_0x435689['maxframeRate']=![],_0x435689['maxframeRate_q2']=![],_0x435689[_0x120577(0x4d8)]=![],_0x435689[_0x120577(0x350)]=![],_0x435689[_0x120577(0x2b6)]=![],_0x435689[_0x120577(0x8c0)]=![],_0x435689[_0x120577(0x7f6)]=![],_0x435689[_0x120577(0x2f5)]=![],_0x435689[_0x120577(0x99b)]=![],_0x435689['ptime']=![],_0x435689['dtx']=![],_0x435689[_0x120577(0x398)]=![],_0x435689[_0x120577(0x646)]=![],_0x435689[_0x120577(0x77c)]=0x15e,_0x435689[_0x120577(0x62b)]=0x23,_0x435689[_0x120577(0x86e)]=![],_0x435689[_0x120577(0x4c1)]=![],_0x435689[_0x120577(0x361)]=![],_0x435689['limitTotalBitrate_defaultMax']=0x2710,_0x435689[_0x120577(0xb50)]=![],_0x435689[_0x120577(0x7ec)]=null,_0x435689[_0x120577(0x574)]=![],_0x435689[_0x120577(0xb45)]=![],_0x435689[_0x120577(0x582)]=![],_0x435689[_0x120577(0xc3e)]=![],_0x435689[_0x120577(0x812)]=_0x435689[_0x120577(0xa1a)](0x5),_0x435689['meterStyle']=![],_0x435689[_0x120577(0x601)]=![],_0x435689[_0x120577(0xc0c)]=![],_0x435689['motionRecord']=![],_0x435689[_0x120577(0x7bf)]=null,_0x435689[_0x120577(0xae6)]=![],_0x435689[_0x120577(0x299)]=![],_0x435689[_0x120577(0x861)]=![],_0x435689[_0x120577(0x7b1)]=null,_0x435689[_0x120577(0x7c6)]=![],_0x435689['mediamtx']=![],_0x435689[_0x120577(0x9ea)]=![],_0x435689['midiOut']=![],_0x435689[_0x120577(0x538)]=![],_0x435689[_0x120577(0x449)]=![],_0x435689[_0x120577(0x7df)]=![],_0x435689[_0x120577(0x6d6)]=![],_0x435689['midiDevice']=![],_0x435689['midiOffset']=0x17,_0x435689[_0x120577(0xaaa)]=![],_0x435689[_0x120577(0xa7b)]=![],_0x435689['nomirror']=![],_0x435689[_0x120577(0x945)]=![],_0x435689[_0x120577(0xb47)]=![],_0x435689[_0x120577(0x51d)]=![],_0x435689[_0x120577(0xb0c)]=![],_0x435689[_0x120577(0x4a7)]=![],_0x435689['msg']=[],_0x435689['hidehome']=![],_0x435689['meshcast']=![],_0x435689[_0x120577(0x93b)]=![],_0x435689[_0x120577(0x7ad)]=![],_0x435689[_0x120577(0xc60)]=![],_0x435689[_0x120577(0xc4b)]=![],_0x435689[_0x120577(0x400)]=![],_0x435689[_0x120577(0x1a0)]=null,_0x435689[_0x120577(0x50a)]=![],_0x435689['whipOutputUserSet']=![],_0x435689[_0x120577(0x58f)]=![],_0x435689[_0x120577(0x287)]=![],_0x435689[_0x120577(0x5ad)]=![],_0x435689[_0x120577(0x5ec)]=![],_0x435689[_0x120577(0xb6c)]=![],_0x435689[_0x120577(0x1db)]=![],_0x435689['muted']=![],_0x435689[_0x120577(0xb3b)]=![],_0x435689['muted_savedState']=![],_0x435689['mono']=![],_0x435689[_0x120577(0x401)]={},_0x435689[_0x120577(0x678)]=![],_0x435689['nochunkaudio']=![],_0x435689[_0x120577(0x7a8)]=![],_0x435689[_0x120577(0x33f)]=![],_0x435689[_0x120577(0x371)]=0x14,_0x435689[_0x120577(0xbac)]=![],_0x435689[_0x120577(0xa3a)]=![],_0x435689[_0x120577(0x9d5)]=![],_0x435689[_0x120577(0x3e0)]=null,_0x435689[_0x120577(0x496)]=![],_0x435689[_0x120577(0x7e4)]=![],_0x435689[_0x120577(0x2d7)]=![],_0x435689[_0x120577(0x6b8)]=![],_0x435689[_0x120577(0xc73)]=![],_0x435689[_0x120577(0x6e1)]=![],_0x435689['optimize']=![],_0x435689[_0x120577(0x782)]=![],_0x435689[_0x120577(0xaed)]=![],_0x435689[_0x120577(0x695)]=![],_0x435689[_0x120577(0x39d)]=![],_0x435689[_0x120577(0x599)]={},_0x435689[_0x120577(0x599)][_0x120577(0x3bf)]=null,_0x435689[_0x120577(0x599)][_0x120577(0x3ca)]=null,_0x435689[_0x120577(0x599)][_0x120577(0x7b2)]=null,_0x435689[_0x120577(0x599)][_0x120577(0xb27)]=null,_0x435689[_0x120577(0x599)][_0x120577(0xaab)]=null,_0x435689[_0x120577(0x6c7)]=![],_0x435689[_0x120577(0xaa0)]=_0x120577(0xb10),_0x435689[_0x120577(0x31d)]=![],_0x435689[_0x120577(0x62c)]=![],_0x435689[_0x120577(0x6af)]=![],_0x435689[_0x120577(0x3f2)]=![],_0x435689[_0x120577(0x367)]=![],_0x435689[_0x120577(0x95e)]=![],_0x435689[_0x120577(0x90c)]=![],_0x435689['micPanning']=![],_0x435689[_0x120577(0x28c)]=![],_0x435689[_0x120577(0x71d)]=null,_0x435689['bypass']=![],_0x435689[_0x120577(0x7ac)]=![],_0x435689['nohistory']=![],_0x435689[_0x120577(0xbf9)]=![],_0x435689[_0x120577(0x5aa)]=![],_0x435689[_0x120577(0xa24)]=null,_0x435689[_0x120577(0x79f)]=![],_0x435689[_0x120577(0x6de)]=![],_0x435689[_0x120577(0x816)]=0x5dc,_0x435689[_0x120577(0x8dc)]=![],_0x435689[_0x120577(0x76f)]={},_0x435689[_0x120577(0x6bb)]=![],_0x435689['pip3']=![],_0x435689['autoPiPPrompt']=![],_0x435689[_0x120577(0xc4f)]=![],_0x435689[_0x120577(0x875)]=![],_0x435689[_0x120577(0x3e6)]=![],_0x435689[_0x120577(0x431)]=![],_0x435689[_0x120577(0xc35)]=![],_0x435689[_0x120577(0x80f)]=![],_0x435689['whipOutScreenShareCodec']=![],_0x435689[_0x120577(0x298)]=![],_0x435689[_0x120577(0x5c4)]=![],_0x435689['permaid']=![],_0x435689[_0x120577(0x3c3)]=![],_0x435689[_0x120577(0x1f3)]=0x1e,_0x435689[_0x120577(0xa45)]=![],_0x435689['preferAudioCodec']=![],_0x435689['postURL']='https://temp.vdo.ninja/',_0x435689[_0x120577(0x9ae)]=![],_0x435689[_0x120577(0xc56)]=![],_0x435689['pingTimeout']=null,_0x435689[_0x120577(0x276)]=null,_0x435689[_0x120577(0x562)]=![],_0x435689[_0x120577(0x15d)]=![],_0x435689[_0x120577(0x895)]=!![],_0x435689['waitPage']=![],_0x435689[_0x120577(0x8f5)]=![],_0x435689[_0x120577(0x7c3)]=![],_0x435689[_0x120577(0x9e3)]=[],_0x435689[_0x120577(0x2db)]=[],_0x435689[_0x120577(0x5b1)]=![],_0x435689['retransmit']=![],_0x435689[_0x120577(0xb23)]=![],_0x435689[_0x120577(0x2ea)]=![],_0x435689[_0x120577(0x84c)]=![],_0x435689[_0x120577(0x95c)]=![],_0x435689['recordLocal']=![],_0x435689[_0x120577(0x6fa)]=!![],_0x435689['recordDefault']=0x1770,_0x435689[_0x120577(0xba7)]=![],_0x435689['rampUpTime']=0x1770,_0x435689[_0x120577(0x5a6)]=![],_0x435689[_0x120577(0x88b)]=0x1388,_0x435689[_0x120577(0x479)]=![],_0x435689[_0x120577(0x6d0)]=![],_0x435689['roomenc']=![],_0x435689['roomid']=![],_0x435689[_0x120577(0x2c3)]=![],_0x435689[_0x120577(0xa09)]=![],_0x435689['roomTimerGlobal']=![],_0x435689[_0x120577(0x4d7)]=null,_0x435689[_0x120577(0x943)]=![],_0x435689['rotate']=![],_0x435689[_0x120577(0xab0)]=!![],_0x435689[_0x120577(0x87b)]=![],_0x435689[_0x120577(0x932)]=![],_0x435689[_0x120577(0x3d7)]=![],_0x435689[_0x120577(0x9bd)]={},_0x435689[_0x120577(0xc6c)]={},_0x435689[_0x120577(0x437)]=![],_0x435689[_0x120577(0x148)]=![],_0x435689['micSampleSize']=![],_0x435689['micSampleRate']=![],_0x435689['outboundSampleRate']=null,_0x435689[_0x120577(0x605)]=![],_0x435689[_0x120577(0x75a)]=![],_0x435689[_0x120577(0x61b)]=![],_0x435689[_0x120577(0xb30)]=![],_0x435689[_0x120577(0x645)]=![],_0x435689[_0x120577(0x4be)]=![],_0x435689[_0x120577(0x3a9)]=[],_0x435689['pastSlots']={},_0x435689['updateOnSlotChange']=![],_0x435689[_0x120577(0x5f2)]=![],_0x435689[_0x120577(0x439)]=![],_0x435689[_0x120577(0xc17)]=![],_0x435689['iframetarget']='*',_0x435689[_0x120577(0xa34)]=![],_0x435689[_0x120577(0xb15)]=![],_0x435689['sceneList']={},_0x435689[_0x120577(0x583)]=![],_0x435689[_0x120577(0x422)]=0x1f4,_0x435689[_0x120577(0x791)]=![],_0x435689[_0x120577(0x232)]=![],_0x435689[_0x120577(0x33d)]=null,_0x435689[_0x120577(0x8fc)]=_0x120577(0x485),_0x435689[_0x120577(0x75e)]=![],_0x435689[_0x120577(0x139)]=![],_0x435689[_0x120577(0x2ce)]=![],_0x435689[_0x120577(0x7fe)]=![],_0x435689[_0x120577(0x48f)]=![],_0x435689[_0x120577(0xb6b)]=![],_0x435689[_0x120577(0x4b9)]=![],_0x435689[_0x120577(0x440)]=![],_0x435689[_0x120577(0x165)]=![],_0x435689['screenShareLabel']=![],_0x435689[_0x120577(0x692)]=![],_0x435689[_0x120577(0x714)]=![],_0x435689[_0x120577(0x198)]=![],_0x435689[_0x120577(0x138)]=![],_0x435689[_0x120577(0x734)]=![],_0x435689[_0x120577(0x12d)]=![],_0x435689[_0x120577(0xbb8)]=[_0x120577(0xb9f),_0x120577(0xbce),_0x120577(0x4e0),'mag',_0x120577(0x6c3),_0x120577(0x4ec)],_0x435689['seedAttempts']=0x0,_0x435689[_0x120577(0xb86)]=![],_0x435689['surfaceSwitching']=![],_0x435689[_0x120577(0xbad)]=![],_0x435689[_0x120577(0x4f8)]=![],_0x435689[_0x120577(0x342)]=![],_0x435689[_0x120577(0x49d)]=![],_0x435689['meta']=![],_0x435689[_0x120577(0x69b)]=![],_0x435689[_0x120577(0x693)]=![],_0x435689[_0x120577(0x8c5)]=![],_0x435689['devicePixelRatio']=![],_0x435689['showlabels']=![],_0x435689[_0x120577(0x598)]=![],_0x435689['showList']=null,_0x435689[_0x120577(0x7e5)]=![],_0x435689[_0x120577(0x1c8)]=[],_0x435689['redirectHangupTimer']=0xbb8,_0x435689[_0x120577(0x824)]=![],_0x435689[_0x120577(0x8ae)]=![],_0x435689[_0x120577(0x227)]=![],_0x435689[_0x120577(0x677)]=![],_0x435689[_0x120577(0x9a0)]=!![],_0x435689['showDirector']=![],_0x435689[_0x120577(0x9b3)]=![],_0x435689['sensors']=![],_0x435689[_0x120577(0x2e5)]=![],_0x435689['speakerMuted_default']=null,_0x435689[_0x120577(0xad9)]=![],_0x435689[_0x120577(0x5c2)]={},_0x435689[_0x120577(0x61d)]=![],_0x435689[_0x120577(0x87c)]=0x8,_0x435689['sharperScreen']=![],_0x435689['screenStream']=![],_0x435689['socialstream']=![],_0x435689[_0x120577(0x463)]=null,_0x435689[_0x120577(0x29f)]=0xbb8,_0x435689[_0x120577(0x4cf)]=![],_0x435689['stereo']=![],_0x435689[_0x120577(0x885)]=null,_0x435689[_0x120577(0x857)]=null,_0x435689[_0x120577(0x97f)]=null,_0x435689[_0x120577(0xa5f)]=null,_0x435689[_0x120577(0x8d0)]=![],_0x435689[_0x120577(0x4ca)]=![],_0x435689[_0x120577(0xa41)]=![],_0x435689['selfVolume']=null,_0x435689[_0x120577(0x490)]=![],_0x435689[_0x120577(0x72d)]=![],_0x435689[_0x120577(0x2fc)]=0x1f4,_0x435689[_0x120577(0xbb7)]=![],_0x435689[_0x120577(0xb4b)]=null,_0x435689['defaultBackgroundImages']=['./media/bg_sample.webp',_0x120577(0x1cb)],_0x435689['defaultForegroundImages']=[_0x120577(0x632)],_0x435689[_0x120577(0x884)]=![],_0x435689[_0x120577(0x4e7)]=![],_0x435689['tallyStyle']=![],_0x435689[_0x120577(0x82d)]=![],_0x435689[_0x120577(0x2d2)]=![],_0x435689[_0x120577(0xa2d)]=![],_0x435689['tz']=![],_0x435689[_0x120577(0x92c)]=![],_0x435689[_0x120577(0x20a)]=![],_0x435689[_0x120577(0x202)]=![],_0x435689[_0x120577(0xa6d)]=![],_0x435689['transferred']=![],_0x435689['twilio']=![],_0x435689[_0x120577(0x6cb)]=![],_0x435689[_0x120577(0x917)]=![],_0x435689['videoMuted']=![],_0x435689[_0x120577(0x4b7)]=![],_0x435689[_0x120577(0x5b4)]=![],_0x435689[_0x120577(0xaf7)]=![],_0x435689[_0x120577(0x6bf)]=![],_0x435689[_0x120577(0x6c0)]=![],_0x435689['view_set']=![],_0x435689['volume']=![],_0x435689[_0x120577(0x79c)]=![],_0x435689[_0x120577(0x3dd)]=![],_0x435689[_0x120577(0x74c)]=![],_0x435689['zoom']=![],_0x435689[_0x120577(0x93c)]=![],_0x435689['tilt']=![],_0x435689[_0x120577(0x68d)]=![],_0x435689['disableViewerWebAudioPipeline']=![],_0x435689[_0x120577(0x698)]={},_0x435689[_0x120577(0xc4e)]={},_0x435689[_0x120577(0x847)]=![],_0x435689[_0x120577(0x78a)]=null,_0x435689[_0x120577(0x37a)]=![],_0x435689[_0x120577(0xc36)]=![],_0x435689['waitImage']=![],_0x435689[_0x120577(0xc0e)]=0x1388,_0x435689[_0x120577(0xbe3)]=![],_0x435689['waitingWatchList']={},_0x435689[_0x120577(0x265)]=![],_0x435689[_0x120577(0x807)]=![],_0x435689['ws']=null,_0x435689[_0x120577(0xbc8)]=![],_0x435689['wssid']=null,_0x435689[_0x120577(0xbdf)]=![],_0x435689['welcomeMessage']=![],_0x435689[_0x120577(0xc6d)]=![],_0x435689['welcomeImage']=![],_0x435689[_0x120577(0x5cb)]=![],_0x435689[_0x120577(0x664)]=![],_0x435689['whipOutKeyframe']=![],_0x435689[_0x120577(0x3c7)]=![],_0x435689[_0x120577(0x9b5)]=![],_0x435689[_0x120577(0x61a)]=![],_0x435689[_0x120577(0xbf6)]=![],_0x435689[_0x120577(0xb06)]=![],_0x435689['whipOutAudioBitrate']=![],_0x435689[_0x120577(0xc35)]=![],_0x435689[_0x120577(0x82e)]=![],_0x435689[_0x120577(0x537)]=![],_0x435689[_0x120577(0x446)]=![],_0x435689[_0x120577(0x7c2)]=![],_0x435689[_0x120577(0x853)]=![],_0x435689[_0x120577(0x981)]=!![],_0x435689[_0x120577(0x792)]=!![],_0x435689['screenWhepPreference']=_0x120577(0x608),_0x435689['whepInput']=![],_0x435689[_0x120577(0x272)]=0x7d0,_0x435689[_0x120577(0x662)]=0x7d0,_0x435689[_0x120577(0x754)]=![],_0x435689[_0x120577(0x9c1)]=![],_0x435689[_0x120577(0x3b0)]=![],_0x435689[_0x120577(0xbb9)]=![],_0x435689[_0x120577(0xb6d)]=![],_0x435689['sharpness']=![],_0x435689[_0x120577(0x947)]=![],_0x435689['brightness']=![],_0x435689[_0x120577(0x41f)]=![],_0x435689[_0x120577(0xb40)]=!![],_0x435689[_0x120577(0x6ac)]=null,_0x435689[_0x120577(0x7e3)]=![],_0x435689['dbx']=![],_0x435689[_0x120577(0x4a5)]=null,_0x435689[_0x120577(0x2f3)]=![],_0x435689[_0x120577(0x3f7)]=![],_0x435689[_0x120577(0xa68)]=![],_0x435689[_0x120577(0x4d3)]=![],_0x435689[_0x120577(0x26e)]=![],_0x435689[_0x120577(0x1bb)]=![],_0x435689[_0x120577(0x81a)]=![],_0x435689['detune']=![],_0x435689['disableBackground']=null,_0x435689[_0x120577(0x2ec)]='',_0x435689[_0x120577(0xa46)]=null,_0x435689[_0x120577(0x8e4)]=![],_0x435689['viewheight']=![],_0x435689[_0x120577(0xbf4)]=![],_0x435689['videoWorker']=![],_0x435689['updateLocalStatsInterval']=null,_0x435689['UUID']=![],_0x435689[_0x120577(0x803)]=getById(_0x120577(0x888))[_0x120577(0x903)](!![]),_0x435689[_0x120577(0xa02)]=null,_0x435689[_0x120577(0x803)]['id']=_0x120577(0x803),_0x435689[_0x120577(0x566)]=getById(_0x120577(0x365))[_0x120577(0x903)](!![]),_0x435689[_0x120577(0x566)]['id']='localVoiceMeter',_0x435689[_0x120577(0x566)]['style'][_0x120577(0x2d6)]=0x0,_0x435689[_0x120577(0x566)][_0x120577(0xc6b)]['level']=0x0,_0x435689[_0x120577(0x809)]=![],_0x435689[_0x120577(0xc24)]=![],_0x435689['widgetwidth']=0x19,_0x435689['noWidget']=![],_0x435689[_0x120577(0x976)]=![],_0x435689[_0x120577(0x7a5)]=![],_0x435689['screensharebutton']=!![],_0x435689[_0x120577(0xb48)]=![],_0x435689[_0x120577(0x16d)]=!![],_0x435689['codirector_changeURL']=!![],_0x435689[_0x120577(0x1ce)]=![],_0x435689[_0x120577(0x467)]=![],_0x435689[_0x120577(0x7f9)]=![],_0x435689['GDRIVE_CLIENT_ID']=_0x120577(0x956),_0x435689[_0x120577(0xbe8)]=_0x120577(0x1ca),_0x435689[_0x120577(0x762)]='recordings',_0x435689[_0x120577(0x1ef)]=_0x120577(0x24f);if(location[_0x120577(0x879)]=='vdo.ninja')_0x435689['salt']='vdo.ninja';else{if(location[_0x120577(0x879)]==_0x120577(0x8f8))_0x435689[_0x120577(0x27e)]=_0x120577(0x76b);else{if([_0x120577(0x76b),_0x120577(0xbe7),'versus.cam',_0x120577(0x55f)][_0x120577(0x49c)](location[_0x120577(0x879)]['split']('.')[_0x120577(0x3e2)](-0x2)[_0x120577(0x28e)]('.')))_0x435689[_0x120577(0x27e)]=location[_0x120577(0x879)][_0x120577(0x9fe)]('.')[_0x120577(0x3e2)](-0x2)[_0x120577(0x28e)]('.');else try{var _0x1c569a=/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;if(_0x1c569a[_0x120577(0x715)](window['location'][_0x120577(0x879)]))_0x435689['salt']=_0x120577(0x76b);else window[_0x120577(0x652)][_0x120577(0x879)]=='localhost'?_0x435689[_0x120577(0x27e)]='vdo.ninja':_0x435689[_0x120577(0x27e)]=location[_0x120577(0x879)];}catch(_0x3410df){_0x435689[_0x120577(0x27e)]=location[_0x120577(0x879)],errorlog(_0x3410df);}}}_0x435689[_0x120577(0x509)]=(function(){var _0x47ad4a=_0x120577,_0xe5249b=location[_0x47ad4a(0x879)];if(_0xe5249b===_0x47ad4a(0x76b))return!![];if(_0xe5249b==='dev.versus.cam')return!![];return![];}());_0x435689['qosEnabled']&&(_0x435689[_0x120577(0x8c9)]={'startTime':Date[_0x120577(0x1ab)](),'connectionSuccesses':0x0,'connectionFailures':0x0,'iceRestarts':0x0,'packetLossVideoSamples':[],'packetLossAudioSamples':[],'rttSamples':[],'jitterSamples':[],'bitrateSamples':[],'turnServersUsed':[],'meshcastServersUsed':[],'candidateTypesLocal':[],'candidateTypesRemote':[],'lastVideoCodec':null,'lastAudioCodec':null,'lastResolution':null,'transportType':null,'sent':![],'wssSuccess':![]});_0x435689['encryptMessage']=function(_0x3284db,_0x57e87c=_0x435689[_0x120577(0x28c)]+_0x435689[_0x120577(0x27e)]){var _0x6e3735=_0x120577,_0x4d8e2f=crypto[_0x6e3735(0x55e)](new Uint8Array(0x10));return crypto[_0x6e3735(0x37d)][_0x6e3735(0x353)]({'name':'SHA-256'},convertStringToArrayBufferView(_0x57e87c))['then'](function(_0x5e3384){var _0x79e1f4=_0x6e3735;return window[_0x79e1f4(0x863)][_0x79e1f4(0x37d)][_0x79e1f4(0x81b)](_0x79e1f4(0x810),_0x5e3384,{'name':_0x79e1f4(0x8c7)},![],[_0x79e1f4(0x78f),'decrypt'])['then'](function(_0x4d955f){var _0x479c12=_0x79e1f4;return crypto[_0x479c12(0x37d)][_0x479c12(0x78f)]({'name':'AES-CBC','iv':_0x4d8e2f},_0x4d955f,convertStringToArrayBufferView(_0x3284db))[_0x479c12(0x7b6)](function(_0x37c999){return encrypted_data=new Uint8Array(_0x37c999),encrypted_data=toHexString(encrypted_data),_0x4d8e2f=toHexString(_0x4d8e2f),[encrypted_data,_0x4d8e2f];},function(_0x5058cf){var _0x1ccc4a=_0x479c12;return errorlog(_0x5058cf[_0x1ccc4a(0x973)]),![];});},function(_0x1d3853){return errorlog(_0x1d3853),![];});})[_0x6e3735(0x501)](errorlog);},_0x435689[_0x120577(0x51b)]=function(_0x3e3786,_0x473a16,_0x2ab156=_0x435689[_0x120577(0x28c)]+_0x435689[_0x120577(0x27e)]){var _0x5a1fce=_0x120577;return _0x3e3786=toByteArray(_0x3e3786),_0x473a16=toByteArray(_0x473a16),crypto[_0x5a1fce(0x37d)][_0x5a1fce(0x353)]({'name':'SHA-256'},convertStringToArrayBufferView(_0x2ab156))[_0x5a1fce(0x7b6)](function(_0x10086d){var _0x48e610=_0x5a1fce;return window[_0x48e610(0x863)][_0x48e610(0x37d)]['importKey'](_0x48e610(0x810),_0x10086d,{'name':_0x48e610(0x8c7)},![],[_0x48e610(0x78f),_0x48e610(0xafc)])[_0x48e610(0x7b6)](function(_0x3eefe0){var _0x5a32cf=_0x48e610;return crypto['subtle']['decrypt']({'name':_0x5a32cf(0x8c7),'iv':_0x473a16},_0x3eefe0,_0x3e3786)['then'](function(_0x34fc84){var _0x3926a6=_0x5a32cf,_0x23a8e7=new Uint8Array(_0x34fc84),_0x284f33='';for(var _0x317900=0x0;_0x317900<_0x23a8e7[_0x3926a6(0x893)];_0x317900++){_0x284f33+=String['fromCharCode'](_0x23a8e7[_0x317900]);}return _0x284f33;},function(_0x406cfd){return errorlog(_0x473a16),errorlog(_0x3e3786),errorlog(_0x406cfd),![];});});})[_0x5a1fce(0x501)](errorlog);},_0x435689['decodeRemote']=async function(_0x4a5f29){var _0xea89ed=_0x120577;if(typeof _0x4a5f29[_0xea89ed(0xba7)]!==_0xea89ed(0x31f))return _0x4a5f29;try{_0x4a5f29[_0xea89ed(0xba7)]['length']==0x2&&(!_0x435689[_0xea89ed(0x695)]&&(_0x435689[_0xea89ed(0x695)]=await generateHash(_0x435689[_0xea89ed(0xba7)]+_0x435689[_0xea89ed(0x27e)],0xc)),_0x4a5f29[_0xea89ed(0xba7)]=await _0x435689['decryptMessage'](_0x4a5f29['remote'][0x0],_0x4a5f29['remote'][0x1],_0x435689[_0xea89ed(0x695)]),_0x4a5f29[_0xea89ed(0xba7)]?log(_0xea89ed(0xbf5)):warnlog(_0xea89ed(0x704)),log(_0x4a5f29));}catch(_0x358de7){errorlog(_0x358de7);}return _0x4a5f29;},_0x435689[_0x120577(0xbfe)]=async function(_0x504212){var _0x135306=_0x120577;try{if(_0x504212[_0x135306(0xba7)]&&typeof _0x504212['remote']===_0x135306(0x332)){var _0x11f0b5=await generateHash(_0x504212[_0x135306(0xba7)]+_0x435689[_0x135306(0x27e)],0xc);_0x504212[_0x135306(0xba7)]=await _0x435689[_0x135306(0x504)](_0x504212[_0x135306(0xba7)],_0x11f0b5);}}catch(_0x442d3b){errorlog(_0x442d3b);}return _0x504212;},_0x435689['decodeInvite']=function(_0x290bfb){var _0x3770c8=_0x120577;try{try{_0x290bfb=decodeURIComponent(_0x290bfb[_0x3770c8(0x586)](/ /g,'+'));}catch(_0x23c360){}_0x290bfb=CryptoJS['AES']['decrypt'](_0x290bfb,_0x3770c8(0xc7a)),_0x290bfb=_0x290bfb['toString'](CryptoJS[_0x3770c8(0x258)]['Utf8']);if(_0x290bfb){if(_0x290bfb[_0x3770c8(0x8da)](_0x3770c8(0x6f5)))_0x290bfb=_0x290bfb[_0x3770c8(0x586)](_0x3770c8(0x6f5),'');else{if(_0x290bfb[_0x3770c8(0x8da)](_0x3770c8(0x91e)))_0x290bfb=_0x290bfb[_0x3770c8(0x586)](_0x3770c8(0x91e),'');else{if(_0x290bfb[_0x3770c8(0x8da)]('/'))_0x290bfb=_0x290bfb[_0x3770c8(0x586)]('/','');else{if(_0x290bfb[_0x3770c8(0x8da)](_0x3770c8(0x98a)))_0x290bfb=_0x290bfb['replace'](_0x3770c8(0x98a),'');else{if(_0x290bfb[_0x3770c8(0x8da)](_0x3770c8(0x43c)))_0x290bfb=_0x290bfb['replace']('vdo.ninja/','');else _0x290bfb[_0x3770c8(0x8da)](_0x3770c8(0xaaf))&&(_0x290bfb=_0x290bfb[_0x3770c8(0x586)]('backup.vdo.ninja/',''));}}}}_0x290bfb=_0x290bfb[_0x3770c8(0x9fe)]('?')[_0x3770c8(0x20c)](0x1)['join']('?'),_0x290bfb&&(_0x290bfb='?'+_0x290bfb['replace'](/\?/g,'&'),_0x435689[_0x3770c8(0x28f)]=_0x290bfb);}}catch(_0x39b662){warnlog(_0x39b662);}},_0x435689[_0x120577(0x5e2)]=function(_0x12d0b4,_0x2e43ff=![]){var _0x27c91f=_0x120577,_0x591e41={};_0x591e41[_0x27c91f(0x6e4)]=!![],_0x591e41[_0x27c91f(0xa34)]=_0x2e43ff,_0x435689['sendRequest'](_0x591e41,_0x12d0b4);},_0x435689[_0x120577(0x30c)]=function(_0x5f4bb5,_0x22dd57,_0x9c5a18=null){var _0x12f111=_0x120577;if(!_0x435689[_0x12f111(0xc6c)][_0x22dd57])return![];var _0x1fa1a1={};if(_0x9c5a18!==null)_0x435689[_0x12f111(0xc6c)][_0x22dd57]['lockedAudioBitrate']=_0x9c5a18||![];else{if(_0x435689['rpcs'][_0x22dd57][_0x12f111(0x6cf)]){warnlog(_0x12f111(0x8a3));return;}}_0x1fa1a1[_0x12f111(0x20f)]=_0x5f4bb5,log(_0x1fa1a1),_0x435689[_0x12f111(0x43f)](_0x1fa1a1,_0x22dd57);},_0x435689['requestRateLimit']=function(_0x40e209,_0x391ee8,_0x18dce6=![],_0x1fd0f7=null){var _0x581007=_0x120577;log(_0x581007(0x2e4)+_0x18dce6);if(!_0x435689[_0x581007(0xc6c)][_0x391ee8]||!_0x435689[_0x581007(0xc6c)][_0x391ee8][_0x581007(0x860)])return![];if(_0x1fd0f7!==null)_0x435689[_0x581007(0xc6c)][_0x391ee8][_0x581007(0x131)]=_0x1fd0f7||![];else{if(_0x435689[_0x581007(0xc6c)][_0x391ee8]['lockedVideoBitrate']){warnlog(_0x581007(0x448));return;}}if(_0x40e209===![]){}else _0x435689[_0x581007(0xc6c)][_0x391ee8][_0x581007(0x832)]=_0x40e209;var _0x1dea8c=-0x1;_0x435689[_0x581007(0xc6c)][_0x391ee8][_0x581007(0x358)]!==![]?_0x40e209=parseInt(_0x435689[_0x581007(0xc6c)][_0x391ee8][_0x581007(0x358)]):_0x40e209=parseInt(_0x435689[_0x581007(0xc6c)][_0x391ee8][_0x581007(0x832)]);if(_0x435689[_0x581007(0x599)][_0x581007(0x3bf)]===![]){if(_0x435689['optimize']!==![]){if(window[_0x581007(0x2c9)])return![];}}else{if(_0x435689[_0x581007(0xc0c)]&&_0x40e209===0x0)return![];}_0x40e209===0x0&&_0x435689[_0x581007(0xc6c)][_0x391ee8][_0x581007(0x904)]&&(_0x40e209=0x1);if(_0x435689[_0x581007(0xc6c)][_0x391ee8]['bandwidth']===_0x40e209)return![];log('request\x20rate\x20limit:\x20'+_0x40e209);var _0x15dc88={};_0x15dc88[_0x581007(0xaa6)]=_0x40e209;if(_0x18dce6===null){}else{if(_0x18dce6)_0x40e209===0x0?(warnlog(_0x581007(0x4bb)),_0x15dc88['audioBitrate']=0x0):_0x1dea8c<0x10&&_0x1dea8c>=0x0?_0x15dc88[_0x581007(0x20f)]=_0x1dea8c:_0x15dc88[_0x581007(0x20f)]=0x10;else _0x1fd0f7===null&&(_0x15dc88[_0x581007(0x20f)]=_0x1dea8c);}return _0x435689[_0x581007(0x43f)](_0x15dc88,_0x391ee8)?(_0x435689['rpcs'][_0x391ee8]['bandwidth']=_0x40e209,!![]):(setTimeout(function _0x5f189b(){var _0x318942=_0x581007;_0x435689[_0x318942(0x9ec)](![],_0x391ee8);},0x1388),warnlog(_0x581007(0x2a5)),![]);},_0x435689['sendGenericData']=function(_0x1f53d3,_0x10f98c=![],_0x2c074f=![],_0x30d084=![]){var _0x3df0ea=_0x120577,_0x46a683=![],_0x5750fe={};_0x5750fe[_0x3df0ea(0x6cc)]=_0x1f53d3;try{if(!_0x10f98c&&!_0x2c074f){if(_0x30d084==_0x3df0ea(0xc6c))_0x435689[_0x3df0ea(0x43f)](_0x5750fe);else _0x30d084==_0x3df0ea(0x76f)?_0x435689[_0x3df0ea(0x896)](_0x5750fe):_0x435689['sendPeers'](_0x5750fe);_0x46a683=!![];}else{if(_0x10f98c){_0x10f98c=_0x10f98c+'';if(_0x30d084==_0x3df0ea(0xc6c))_0x435689[_0x3df0ea(0x43f)](_0x5750fe,_0x10f98c);else _0x30d084==_0x3df0ea(0x76f)?_0x435689['sendMessage'](_0x5750fe,_0x10f98c):_0x435689[_0x3df0ea(0xbd9)](_0x5750fe,_0x10f98c);_0x46a683=!![];}else{if(_0x2c074f){_0x2c074f=_0x2c074f+'';for(var _0x391e3d in _0x435689[_0x3df0ea(0xc6c)]){if(_0x435689[_0x3df0ea(0xc6c)][_0x391e3d][_0x3df0ea(0x885)]===_0x2c074f){if(_0x30d084==_0x3df0ea(0xc6c))_0x435689[_0x3df0ea(0x43f)](_0x5750fe,_0x391e3d);else _0x30d084=='pcs'?_0x435689[_0x3df0ea(0x896)](_0x5750fe,_0x391e3d):_0x435689[_0x3df0ea(0xbd9)](_0x5750fe,_0x391e3d);_0x46a683=!![];}}}}}return _0x46a683;}catch(_0x5eb3df){return![];}},_0x435689[_0x120577(0x4d9)]=function(_0x4859a9,_0x959944){var _0x1cba12=_0x120577,_0x5703ea={};_0x5703ea[_0x1cba12(0x91a)]={},_0x5703ea[_0x1cba12(0x91a)]=_0x4859a9;_0x959944!==null&&(_0x5703ea['UUID']=_0x959944);if(isIFrame)parent[_0x1cba12(0xaf2)](_0x5703ea,_0x435689[_0x1cba12(0x555)]);else _0x4859a9[_0x1cba12(0x3ad)]&&!isIFrame&&getChatMessage(_0x4859a9[_0x1cba12(0x3ad)][_0x1cba12(0x41c)],_0x4859a9[_0x1cba12(0x3ad)][_0x1cba12(0x25d)],![],![]);},_0x435689[_0x120577(0x9e2)]=function(){var _0xdc425c=_0x120577;if(_0x435689[_0xdc425c(0x8f9)]===null)return;for(var _0x4b51a1 in _0x435689[_0xdc425c(0xc6c)]){try{var _0x4a4f79=getReceivers2(_0x4b51a1);for(var _0x2e6004=0x0;_0x2e6004<_0x4a4f79[_0xdc425c(0xb04)];_0x2e6004++){_0x4a4f79[_0x2e6004]['track'][_0xdc425c(0x2fe)]=='audio'&&(ChromiumVersion&&ChromiumVersion>=0x85&&(_0x4a4f79[_0x2e6004][_0xdc425c(0xb26)][_0xdc425c(0x79a)]=!![]),_0x4a4f79[_0x2e6004]['track']['enabled']=!_0x435689[_0xdc425c(0x8f9)]);}}catch(_0x127b47){}}_0x435689['directorSpeakerMuted']&&(getById(_0xdc425c(0x513))['muted']=!![]);},_0x435689['directorDisplayMute']=function(){var _0x2d9fcf=_0x120577;if(_0x435689[_0x2d9fcf(0x310)]===null)return;_0x435689[_0x2d9fcf(0x310)]?(getById('gridlayout')[_0x2d9fcf(0xab1)][_0x2d9fcf(0x701)]('hidden'),!_0x435689[_0x2d9fcf(0x326)]&&warnUser(getTranslation(_0x2d9fcf(0x7bb)),![],![])):(getById(_0x2d9fcf(0x660))[_0x2d9fcf(0xab1)][_0x2d9fcf(0xae4)]('hidden'),!_0x435689[_0x2d9fcf(0x326)]&&closeModal());for(var _0x45d684 in _0x435689['rpcs']){try{var _0x15e429=getReceivers2(_0x45d684);for(var _0x54e38e=0x0;_0x54e38e<_0x15e429['length'];_0x54e38e++){_0x15e429[_0x54e38e][_0x2d9fcf(0xb26)][_0x2d9fcf(0x2fe)]==_0x2d9fcf(0x57d)&&(_0x15e429[_0x54e38e][_0x2d9fcf(0xb26)][_0x2d9fcf(0x79a)]=!![],_0x15e429[_0x54e38e][_0x2d9fcf(0xb26)][_0x2d9fcf(0x79a)]=!_0x435689[_0x2d9fcf(0x310)]);}}catch(_0x504e66){errorlog(_0x504e66);}}_0x435689[_0x2d9fcf(0x310)]&&(getById(_0x2d9fcf(0x513))[_0x2d9fcf(0xb62)]=!![]);},_0x435689[_0x120577(0xbf2)]=async function(){var _0x481cfa=_0x120577;await _0x435689[_0x481cfa(0x913)]();if(_0x435689[_0x481cfa(0x1fc)]){log('Shadow\x20mode:\x20not\x20seeding');return;}else{if(_0x435689[_0x481cfa(0xc62)]!==![])_0x435689[_0x481cfa(0xc62)]=_0x481cfa(0x709),log(_0x481cfa(0x436));else{if(_0x435689[_0x481cfa(0x559)])log(_0x481cfa(0x9f0));else{var _0x33d516={};_0x33d516[_0x481cfa(0xaa1)]=_0x481cfa(0xb82),_0x435689[_0x481cfa(0x8c8)]&&_0x435689[_0x481cfa(0x4b6)]?(_0x33d516[_0x481cfa(0x885)]=_0x435689[_0x481cfa(0x4b6)],log(_0x481cfa(0x3bc)+_0x435689[_0x481cfa(0x4b6)])):_0x33d516[_0x481cfa(0x885)]=_0x435689[_0x481cfa(0x885)],_0x435689[_0x481cfa(0x3ec)](_0x33d516),log(_0x481cfa(0x70b)),pokeAPI(_0x481cfa(0x734),!![]),pokeIframeAPI('seeding-started',!![]),pokeIframeAPI(_0x481cfa(0x734),!![]);}}}},_0x435689['requestCoDirector']=function(){var _0x2d7d0a=_0x120577;getById(_0x2d7d0a(0xace))[_0x2d7d0a(0x6cd)]=!![],getById('coDirectorEnable')[_0x2d7d0a(0x967)]=_0x2d7d0a(0xac6),getById(_0x2d7d0a(0x29d))[_0x2d7d0a(0xab1)]['add'](_0x2d7d0a(0xc09)),_0x435689[_0x2d7d0a(0x595)]&&(_0x435689['directorHash']?_0x435689[_0x2d7d0a(0xb2f)]&&(_0x435689[_0x2d7d0a(0xb2f)]in _0x435689[_0x2d7d0a(0xc6c)]&&(_0x435689[_0x2d7d0a(0xc6c)][_0x435689[_0x2d7d0a(0xb2f)]][_0x2d7d0a(0xbec)]===![]&&_0x435689[_0x2d7d0a(0x504)](_0x435689['directorHash'],_0x435689['directorHash'])['then'](function(_0x170995){var _0x183982=_0x2d7d0a,_0x49ccca={};_0x49ccca[_0x183982(0x9d6)]=_0x435689[_0x183982(0xb2f)],_0x49ccca[_0x183982(0x85e)]=_0x170995[0x0],_0x49ccca[_0x183982(0x153)]=_0x170995[0x1],_0x435689[_0x183982(0xc6c)][_0x435689['directorUUID']][_0x183982(0xbec)]===![]&&(_0x435689[_0x183982(0x43f)](_0x49ccca,_0x49ccca[_0x183982(0x9d6)])&&(_0x435689['rpcs'][_0x435689['directorUUID']][_0x183982(0xbec)]=!![]));})[_0x2d7d0a(0x501)](errorlog))):generateHash(_0x435689['directorPassword']+_0x435689[_0x2d7d0a(0x27e)]+_0x2d7d0a(0x352),0xc)[_0x2d7d0a(0x7b6)](function(_0x3bbf37){var _0x30b6bd=_0x2d7d0a;_0x435689[_0x30b6bd(0x8a6)]=_0x3bbf37;_0x435689['directorUUID']&&(_0x435689[_0x30b6bd(0xc6c)][_0x435689[_0x30b6bd(0xb2f)]]['codirectorRequested']===![]&&_0x435689['encryptMessage'](_0x435689['directorHash'],_0x435689[_0x30b6bd(0x8a6)])[_0x30b6bd(0x7b6)](function(_0x55fb7e){var _0x58fd4f=_0x30b6bd,_0x501d01={};_0x501d01[_0x58fd4f(0x9d6)]=_0x435689[_0x58fd4f(0xb2f)],_0x501d01[_0x58fd4f(0x85e)]=_0x55fb7e[0x0],_0x501d01[_0x58fd4f(0x153)]=_0x55fb7e[0x1],_0x435689[_0x58fd4f(0xc6c)][_0x435689[_0x58fd4f(0xb2f)]][_0x58fd4f(0xbec)]===![]&&(_0x435689['sendRequest'](_0x501d01,_0x501d01[_0x58fd4f(0x9d6)])&&(_0x435689[_0x58fd4f(0xc6c)][_0x435689[_0x58fd4f(0xb2f)]][_0x58fd4f(0xbec)]=!![]));})[_0x30b6bd(0x501)](errorlog));return;})[_0x2d7d0a(0x501)](errorlog));},_0x435689[_0x120577(0x346)]=function(_0x332ebd,_0x413939){return _0x332ebd;},_0x435689[_0x120577(0x291)]=function(_0x9dfee1=![]){var _0x91612a=_0x120577;log(_0x91612a(0x53f));if(_0x9dfee1){if(!_0x435689[_0x91612a(0x76f)][_0x9dfee1])return![];if(_0x435689['pcs'][_0x9dfee1]['scaleResolution']!==![]||_0x435689[_0x91612a(0x76f)][_0x9dfee1][_0x91612a(0x9f5)]!==![]||_0x435689[_0x91612a(0x76f)][_0x9dfee1]['scaleHeight']!==![])return log('resolution\x20scale:\x20'+_0x435689['pcs'][_0x9dfee1]['scaleWidth']+_0x91612a(0x1c5)+_0x435689[_0x91612a(0x76f)][_0x9dfee1][_0x91612a(0x4bd)]),_0x435689['setResolution'](_0x9dfee1,_0x435689['pcs'][_0x9dfee1]['scaleWidth'],_0x435689[_0x91612a(0x76f)][_0x9dfee1]['scaleHeight'],_0x435689[_0x91612a(0x76f)][_0x9dfee1]['scaleSnap'],_0x435689[_0x91612a(0x76f)][_0x9dfee1][_0x91612a(0x2c8)]),!![];else{if(_0x435689[_0x91612a(0x76f)][_0x9dfee1][_0x91612a(0x61b)]!==![])return log('scale\x20scale'),_0x435689[_0x91612a(0x125)](_0x9dfee1,_0x435689['pcs'][_0x9dfee1][_0x91612a(0x61b)],!![]),!![];}}else for(var _0xe59fc7 in _0x435689[_0x91612a(0x76f)]){setTimeout(function(_0x8487ea){var _0x4e687e=_0x91612a;if(_0x435689[_0x4e687e(0x76f)][_0x8487ea][_0x4e687e(0x96c)]!==![]||_0x435689[_0x4e687e(0x76f)][_0x8487ea][_0x4e687e(0x9f5)]!==![]||_0x435689['pcs'][_0x8487ea][_0x4e687e(0x4bd)]!==![])log(_0x4e687e(0x83e)+_0x435689['pcs'][_0x8487ea][_0x4e687e(0x9f5)]+_0x4e687e(0x1c5)+_0x435689[_0x4e687e(0x76f)][_0x8487ea][_0x4e687e(0x4bd)]),_0x435689['setResolution'](_0x8487ea,_0x435689[_0x4e687e(0x76f)][_0x8487ea]['scaleWidth'],_0x435689[_0x4e687e(0x76f)][_0x8487ea][_0x4e687e(0x4bd)],_0x435689[_0x4e687e(0x76f)][_0x8487ea][_0x4e687e(0x289)],_0x435689['pcs'][_0x8487ea]['cover']);else _0x435689[_0x4e687e(0x76f)][_0x8487ea]['scale']!==![]&&(log(_0x4e687e(0x6be)),_0x435689[_0x4e687e(0x125)](_0x8487ea,_0x435689[_0x4e687e(0x76f)][_0x8487ea][_0x4e687e(0x61b)],!![]));},0x0,_0xe59fc7);}return![];},_0x435689['whipOutSetScale']=function(_0x2a0264=_0x435689[_0x120577(0x6c7)]){var _0x558d34=_0x120577;warnlog(_0x558d34(0x204));if(_0x435689[_0x558d34(0xc35)][_0x558d34(0x61b)]!==_0x2a0264){if(_0x2a0264==null){try{var _0x3de7c2=_0x435689['whipOut'][_0x558d34(0x83c)]()[_0x558d34(0x6f7)](function(_0x264fad){var _0x516b4a=_0x558d34;return _0x264fad[_0x516b4a(0xb26)]&&_0x264fad[_0x516b4a(0xb26)][_0x516b4a(0x2fe)]==_0x516b4a(0x57d);});}catch(_0x10e204){errorlog(_0x10e204);}if(!_0x3de7c2){warnlog(_0x558d34(0x7e1));return;}var _0xbb3278=_0x3de7c2[_0x558d34(0xb22)]();(!_0xbb3278['encodings']||_0xbb3278['encodings'][_0x558d34(0xb04)]==0x0)&&(_0xbb3278[_0x558d34(0x9e4)]=[{}]),_0x558d34(0xbcc)in _0xbb3278[_0x558d34(0x9e4)][0x0]?(_0x2a0264=0x64/_0xbb3278['encodings'][0x0][_0x558d34(0xbcc)],_0x2a0264=_0x2a0264*0.95):_0x2a0264=0x5f;}else _0x435689['whipOut'][_0x558d34(0x61b)]=_0x2a0264;try{if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad))log('iOS\x20devices\x20do\x20not\x20support\x20dynamic\x20bitrates\x20correctly;\x20skipping');else{if('RTCRtpSender'in window&&_0x558d34(0x8f7)in window['RTCRtpSender']['prototype']){try{var _0x3de7c2=_0x435689[_0x558d34(0xc35)][_0x558d34(0x83c)]()[_0x558d34(0x6f7)](function(_0x449bdb){var _0x22bd73=_0x558d34;return _0x449bdb[_0x22bd73(0xb26)]&&_0x449bdb[_0x22bd73(0xb26)]['kind']==_0x22bd73(0x57d);});}catch(_0x44e7f2){errorlog(_0x44e7f2);}if(!_0x3de7c2){warnlog(_0x558d34(0x7e1));return;}var _0x37e480={};if(_0x2a0264<=0x0||_0x2a0264==0x64){var _0x1614db=getChromiumVersion();_0x1614db>0x50?_0x37e480[_0x558d34(0xbcc)]=null:_0x37e480[_0x558d34(0xbcc)]=0x1;}else _0x37e480[_0x558d34(0xbcc)]=0x64/_0x2a0264;setEncodings(_0x3de7c2,_0x37e480,function(_0x243c52){var _0x51c9a0=_0x558d34;log(_0x51c9a0(0x6ca)),pokeIframeAPI('setVideoScale',_0x243c52,_0x51c9a0(0xc65)),pokeIframeAPI('set-video-scale',_0x243c52,_0x51c9a0(0xc65)),_0x435689[_0x51c9a0(0xc35)][_0x51c9a0(0x5c2)][_0x51c9a0(0x1ff)]=parseInt(_0x243c52)+'%';},_0x2a0264);return;}}}catch(_0x242b4c){errorlog(_0x242b4c);}}},_0x435689[_0x120577(0x125)]=function(_0x18852d,_0x2ae1d9,_0x5584ef=![]){var _0x1c003a=_0x120577;warnlog(_0x1c003a(0x5dc)+_0x2ae1d9);try{_0x435689[_0x1c003a(0x76f)][_0x18852d][_0x1c003a(0x5c2)][_0x1c003a(0x1ff)]=_0x2ae1d9;}catch(_0x24ec3f){errorlog(_0x24ec3f);}if(!_0x5584ef&&_0x435689['pcs'][_0x18852d]['scale']===_0x2ae1d9)return;if(_0x2ae1d9==null){try{var _0x3c2bda=getSenders2(_0x18852d)[_0x1c003a(0x6f7)](function(_0x1041e7){var _0x4696ac=_0x1c003a;return _0x1041e7[_0x4696ac(0xb26)]&&_0x1041e7['track'][_0x4696ac(0x2fe)]==_0x4696ac(0x57d);});}catch(_0x431f19){errorlog(_0x431f19);}if(!_0x3c2bda){warnlog(_0x1c003a(0x7e1));return;}var _0x2bcf5a=_0x3c2bda[_0x1c003a(0xb22)]();(!_0x2bcf5a[_0x1c003a(0x9e4)]||_0x2bcf5a[_0x1c003a(0x9e4)]['length']==0x0)&&(_0x2bcf5a[_0x1c003a(0x9e4)]=[{}]),'scaleResolutionDownBy'in _0x2bcf5a[_0x1c003a(0x9e4)][0x0]?(_0x2ae1d9=0x64/_0x2bcf5a[_0x1c003a(0x9e4)][0x0][_0x1c003a(0xbcc)],_0x2ae1d9=_0x2ae1d9*0.95):_0x2ae1d9=0x5f;}else _0x2ae1d9=Math['ceil'](_0x2ae1d9),_0x435689[_0x1c003a(0x76f)][_0x18852d][_0x1c003a(0x61b)]=_0x2ae1d9;try{if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad))log(_0x1c003a(0xc42));else{if(_0x1c003a(0x2a8)in window&&_0x1c003a(0x8f7)in window[_0x1c003a(0x2a8)][_0x1c003a(0x128)]){try{var _0x3c2bda=getSenders2(_0x18852d)['find'](function(_0x592f3c){var _0x39362d=_0x1c003a;return _0x592f3c[_0x39362d(0xb26)]&&_0x592f3c[_0x39362d(0xb26)][_0x39362d(0x2fe)]==_0x39362d(0x57d);});}catch(_0x574cd7){errorlog(_0x574cd7);}if(!_0x3c2bda){warnlog(_0x1c003a(0x7e1));return;}_0x2ae1d9=_0x435689[_0x1c003a(0xc10)](_0x18852d,![],_0x2ae1d9);var _0x9de7b7={};if(_0x2ae1d9<=0x0||_0x2ae1d9==0x64){var _0x24d05a=getChromiumVersion();_0x24d05a>0x50?_0x9de7b7[_0x1c003a(0xbcc)]=null:_0x9de7b7[_0x1c003a(0xbcc)]=0x1;}else _0x9de7b7['scaleResolutionDownBy']=0x64/_0x2ae1d9;setEncodings(_0x3c2bda,_0x9de7b7,function(_0x2d112c){var _0x32f324=_0x1c003a;log('scale\x20set!\x20'+_0x2d112c[0x0]),pokeIframeAPI('setVideoScale',_0x2d112c[0x0],_0x2d112c[0x1]),pokeIframeAPI(_0x32f324(0x9b9),_0x2d112c[0x0],_0x2d112c[0x1]),_0x435689[_0x32f324(0x76f)][_0x2d112c[0x1]][_0x32f324(0x5c2)]['scaleFactor']=parseInt(_0x2d112c[0x0])+'%';},[_0x2ae1d9,_0x18852d]);return;}}}catch(_0x164f72){errorlog(_0x164f72);}},_0x435689[_0x120577(0x3f6)]=function(_0x5b83c4,_0x107b89,_0x4c5ccb,_0x1824e2=![],_0x51d706=![],_0x5923fa=null){var _0x416e9d=_0x120577;if(!(_0x5b83c4 in _0x435689[_0x416e9d(0xc6c)]))return;_0x5923fa===null&&(_0x5923fa=_0x435689[_0x416e9d(0x2c8)]||![]);var _0x391576=![];!(_0x435689[_0x416e9d(0xc6c)][_0x5b83c4]['scaleWidth']==Math[_0x416e9d(0x8bb)](_0x107b89)||_0x435689[_0x416e9d(0xc6c)][_0x5b83c4][_0x416e9d(0x9f5)]===Math['ceil'](_0x107b89))&&(_0x107b89=Math['round'](_0x107b89),_0x435689[_0x416e9d(0xc6c)][_0x5b83c4][_0x416e9d(0x9f5)]=_0x107b89,_0x391576=!![]);!(_0x435689[_0x416e9d(0xc6c)][_0x5b83c4][_0x416e9d(0x4bd)]==Math[_0x416e9d(0x8bb)](_0x4c5ccb)||_0x435689['rpcs'][_0x5b83c4][_0x416e9d(0x4bd)]===Math[_0x416e9d(0xa44)](_0x4c5ccb))&&(_0x4c5ccb=Math[_0x416e9d(0x5de)](_0x4c5ccb),_0x435689[_0x416e9d(0xc6c)][_0x5b83c4]['scaleHeight']=_0x4c5ccb,_0x391576=!![]);_0x435689[_0x416e9d(0xc6c)][_0x5b83c4]['scaleSnap']!=_0x1824e2&&(_0x435689[_0x416e9d(0xc6c)][_0x5b83c4][_0x416e9d(0x289)]=_0x1824e2,_0x391576=!![]);_0x107b89=Math[_0x416e9d(0x5de)](_0x107b89),_0x4c5ccb=Math[_0x416e9d(0x5de)](_0x4c5ccb);if(_0x391576){var _0x52d9cc={};_0x52d9cc[_0x416e9d(0x9d6)]=_0x5b83c4,_0x52d9cc[_0x416e9d(0x3f6)]={'w':_0x107b89,'h':_0x4c5ccb,'s':_0x1824e2,'c':_0x5923fa},_0x51d706&&(_0x52d9cc['requestAs']=_0x51d706),log(_0x107b89+'\x20'+_0x4c5ccb),_0x435689[_0x416e9d(0x43f)](_0x52d9cc,_0x5b83c4);}_0x1824e2?_0x435689['rpcs'][_0x5b83c4]['stats'][_0x416e9d(0x8ea)]='~\x20'+parseInt(_0x107b89)+_0x416e9d(0x1c5)+parseInt(_0x4c5ccb):_0x435689[_0x416e9d(0xc6c)][_0x5b83c4][_0x416e9d(0x5c2)][_0x416e9d(0x8ea)]=parseInt(_0x107b89)+_0x416e9d(0x1c5)+parseInt(_0x4c5ccb);},_0x435689['calculateScale']=function(_0x2b5c17,_0x19ca8c=![],_0x5a2e43=![]){var _0x2f4e92=_0x120577;if(_0x5a2e43){}else _0x435689[_0x2f4e92(0x76f)][_0x2b5c17][_0x2f4e92(0x61b)]?_0x5a2e43=_0x435689[_0x2f4e92(0x76f)][_0x2b5c17]['scale']:_0x5a2e43=0x64;_0x435689['pcs'][_0x2b5c17][_0x2f4e92(0x96c)]&&_0x5a2e43>_0x435689[_0x2f4e92(0x76f)][_0x2b5c17][_0x2f4e92(0x96c)]&&(_0x5a2e43=_0x435689[_0x2f4e92(0x76f)][_0x2b5c17][_0x2f4e92(0x96c)]);if(_0x19ca8c)_0x5a2e43=_0x44a4f8(_0x2b5c17,_0x5a2e43,_0x19ca8c);else _0x435689[_0x2f4e92(0x76f)][_0x2b5c17][_0x2f4e92(0xbb0)]&&_0x435689[_0x2f4e92(0x76f)][_0x2b5c17][_0x2f4e92(0xbb0)]<_0x5a2e43&&(_0x5a2e43=_0x435689[_0x2f4e92(0x76f)][_0x2b5c17][_0x2f4e92(0xbb0)]);if(_0x435689[_0x2f4e92(0x4b9)]&&_0x435689['pcs'][_0x2b5c17]['scaleSnap']){if(_0x5a2e43>0x55)_0x5a2e43=0x64;else _0x5a2e43>0x2a&&_0x5a2e43<0x32&&(_0x5a2e43=0x32);}return _0x5a2e43=_0x435689[_0x2f4e92(0x346)](_0x5a2e43,_0x2b5c17),_0x5a2e43;},_0x435689['setResolution']=function(_0x6591a7=![],_0x4bc8d9=null,_0x38ba16=null,_0x44e08b=![],_0x32ed36=![]){var _0x2ad11a=_0x120577;log(_0x2ad11a(0x12e)+_0x4bc8d9+'x'+_0x38ba16);if(_0x6591a7&&!(_0x6591a7 in _0x435689[_0x2ad11a(0x76f)]))return;else{if(!_0x6591a7){for(var _0x2e314b in _0x435689[_0x2ad11a(0x76f)]){_0x435689['setResolution'](_0x2e314b,_0x435689['pcs'][_0x2e314b]['scaleWidth'],_0x435689[_0x2ad11a(0x76f)][_0x2e314b][_0x2ad11a(0x4bd)],_0x435689[_0x2ad11a(0x76f)][_0x2e314b][_0x2ad11a(0x289)],_0x435689[_0x2ad11a(0x76f)][_0x2e314b][_0x2ad11a(0x2c8)]);}return;}}_0x32ed36=_0x32ed36||![],snape=_0x44e08b||![];if(_0x4bc8d9===null&&_0x38ba16===null){if(!_0x435689[_0x2ad11a(0x76f)][_0x6591a7][_0x2ad11a(0x9f5)]&&!_0x435689[_0x2ad11a(0x76f)][_0x6591a7][_0x2ad11a(0x4bd)])return;else _0x4bc8d9=_0x435689[_0x2ad11a(0x76f)][_0x6591a7][_0x2ad11a(0x9f5)]||0x64,_0x38ba16=_0x435689[_0x2ad11a(0x76f)][_0x6591a7][_0x2ad11a(0x4bd)]||0x64;}else _0x435689[_0x2ad11a(0x76f)][_0x6591a7][_0x2ad11a(0x9f5)]=_0x4bc8d9,_0x435689[_0x2ad11a(0x76f)][_0x6591a7]['scaleHeight']=_0x38ba16,_0x435689[_0x2ad11a(0x76f)][_0x6591a7][_0x2ad11a(0x289)]=_0x44e08b,_0x435689[_0x2ad11a(0x76f)][_0x6591a7][_0x2ad11a(0x2c8)]=_0x32ed36;if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad))return;if('RTCRtpSender'in window&&_0x2ad11a(0x8f7)in window[_0x2ad11a(0x2a8)][_0x2ad11a(0x128)]){var _0x4a3e92=getSenders2(_0x6591a7)[_0x2ad11a(0x6f7)](function(_0xba0488){var _0x19952a=_0x2ad11a;return _0xba0488[_0x19952a(0xb26)]&&_0xba0488[_0x19952a(0xb26)][_0x19952a(0x2fe)]==_0x19952a(0x57d);});if(!_0x4a3e92){log(_0x2ad11a(0x815));return;}var _0x33af33={};if(_0x2ad11a(0xbb3)in _0x435689[_0x2ad11a(0x76f)][_0x6591a7]){var _0xda780d=_0x435689['screenStream']['getVideoTracks']();if(_0xda780d[_0x2ad11a(0xb04)])var _0x276748=_0xda780d[0x0][_0x2ad11a(0x3ef)](),_0x565839=_0x276748[_0x2ad11a(0x348)],_0x51774d=_0x276748[_0x2ad11a(0x3dd)];else return;}else{if(_0x435689[_0x2ad11a(0x917)]&&_0x435689[_0x2ad11a(0x917)]['srcObject']){var _0xda780d=_0x435689[_0x2ad11a(0x917)][_0x2ad11a(0x200)][_0x2ad11a(0x4df)]();if(_0xda780d[_0x2ad11a(0xb04)])var _0x276748=_0xda780d[0x0][_0x2ad11a(0x3ef)](),_0x565839=_0x276748[_0x2ad11a(0x348)],_0x51774d=_0x276748[_0x2ad11a(0x3dd)];else return;}else return;}var _0x52741d=0x64*_0x4bc8d9/_0x51774d,_0x3cb472=0x64*_0x38ba16/_0x565839;warnlog(_0x52741d+_0x2ad11a(0x1c5)+_0x3cb472);var _0x1c88a7=0x64;if(_0x4bc8d9===null)_0x1c88a7=_0x3cb472;else{if(_0x38ba16===null)_0x1c88a7=_0x52741d;else _0x32ed36?_0x52741d>_0x3cb472?_0x1c88a7=_0x52741d:_0x1c88a7=_0x3cb472:_0x52741d<_0x3cb472?_0x1c88a7=_0x52741d:_0x1c88a7=_0x3cb472;}_0x1c88a7>0x64&&(_0x1c88a7=0x64);log('resolution\x20scale:\x20'+_0x1c88a7),_0x435689['pcs'][_0x6591a7][_0x2ad11a(0x96c)]=_0x1c88a7;var _0x17f0f5=_0x435689[_0x2ad11a(0xc10)](_0x6591a7);if(_0x17f0f5<=0x0||_0x17f0f5==0x64){var _0x46c841=getChromiumVersion();_0x46c841>0x50?_0x33af33[_0x2ad11a(0xbcc)]=null:_0x33af33[_0x2ad11a(0xbcc)]=0x1;}else _0x33af33[_0x2ad11a(0xbcc)]=0x64/_0x17f0f5;setEncodings(_0x4a3e92,_0x33af33,function(_0x4aa064){var _0x3da37d=_0x2ad11a;log(_0x3da37d(0x6ca)),pokeIframeAPI(_0x3da37d(0xbf8),_0x4aa064[0x0],_0x4aa064[0x1]),pokeIframeAPI(_0x3da37d(0x9b9),_0x4aa064[0x0],_0x4aa064[0x1]),_0x435689[_0x3da37d(0x76f)][_0x4aa064[0x1]][_0x3da37d(0x5c2)][_0x3da37d(0x1ff)]=parseInt(_0x4aa064[0x0])+'%';},[_0x17f0f5,_0x6591a7]);return;}},_0x435689[_0x120577(0xbb5)]=function(_0x2af182=null,_0x500644=null){var _0x1af369=_0x120577;_0x500644&&_0x500644[_0x1af369(0x8e0)]();_0x435689[_0x1af369(0x826)]&&(_0x435689[_0x1af369(0x826)]['needKeyFrame']=!![],log('FORCING\x20A\x20CHUNKED\x20KEY\x20FRAME:\x20'+_0x2af182));if(iOS||iPad)return log(_0x1af369(0xc42)),![];else{if(_0x1af369(0x2a8)in window&&_0x1af369(0x8f7)in window[_0x1af369(0x2a8)][_0x1af369(0x128)]){log(_0x1af369(0x2c5)+_0x2af182);if(_0x2af182==null){for(_0x2af182 in _0x435689[_0x1af369(0x76f)]){_0x435689[_0x1af369(0xbb5)](_0x2af182);}return![];}if(!(_0x2af182 in _0x435689[_0x1af369(0x76f)]))return![];_0x435689[_0x1af369(0x76f)][_0x2af182][_0x1af369(0x2a6)]&&(_0x435689[_0x1af369(0x76f)][_0x2af182]['keyframeTimeout']&&(clearTimeout(_0x435689[_0x1af369(0x76f)][_0x2af182][_0x1af369(0x4e6)]),_0x435689[_0x1af369(0x76f)][_0x2af182][_0x1af369(0x4e6)]=null),_0x435689[_0x1af369(0x76f)][_0x2af182][_0x1af369(0x4e6)]=setTimeout(function(_0x4fa876){var _0x5d6a38=_0x1af369;!_0x435689[_0x5d6a38(0x76f)][_0x4fa876]?clearInterval(this):_0x435689[_0x5d6a38(0xbb5)](_0x4fa876);},parseInt(_0x435689[_0x1af369(0x76f)][_0x2af182][_0x1af369(0x2a6)]),_0x2af182));try{var _0x22502e=getSenders2(_0x2af182)[_0x1af369(0x6f7)](function(_0xd0893b){var _0xe4dcc5=_0x1af369;return _0xd0893b[_0xe4dcc5(0xb26)]&&_0xd0893b[_0xe4dcc5(0xb26)][_0xe4dcc5(0x2fe)]==_0xe4dcc5(0x57d);});if(!_0x22502e)return warnlog('can\x27t\x20change\x20bitrate;\x20no\x20video\x20sender\x20found'),![];var _0x250734={};return _0x250734[_0x1af369(0xbcc)]=0xa,setEncodings(_0x22502e,_0x250734,function(_0x5c3bc8){var _0x4c4f90=_0x1af369;log('scaleResolutionDownBy\x20set\x202a!\x20'+_0x5c3bc8[0x0]);var _0x426b4d=_0x435689['calculateScale'](_0x5c3bc8[0x0]),_0x13ab75={};if(_0x426b4d<=0x0||_0x426b4d==0x64){var _0x2392b4=getChromiumVersion();_0x2392b4>0x50?_0x13ab75[_0x4c4f90(0xbcc)]=null:_0x13ab75['scaleResolutionDownBy']=0x1;}else _0x13ab75[_0x4c4f90(0xbcc)]=0x64/_0x426b4d;setEncodings(_0x5c3bc8[0x1],_0x13ab75,function(){var _0x3b1b5c=_0x4c4f90;log(_0x3b1b5c(0x93e));});},[_0x2af182,_0x22502e]),!![];}catch(_0x1ffe5c){errorlog(_0x1ffe5c);}}}return![];},_0x435689[_0x120577(0xbe4)]=function(_0x598136){var _0x5b082b=_0x120577;log('enhacing\x20audio\x20encoder');var _0x241f15=getSenders2(_0x598136)[_0x5b082b(0x6f7)](function(_0x48f89e){var _0x580711=_0x5b082b;return _0x48f89e[_0x580711(0xb26)]&&_0x48f89e['track'][_0x580711(0x2fe)]==_0x580711(0x2de);});if(!_0x241f15)return log(_0x5b082b(0x630)),![];var _0x4169bd={};try{_0x4169bd[_0x5b082b(0xae2)]=_0x5b082b(0xb31),_0x4169bd['priority']='high',_0x4169bd[_0x5b082b(0x7dc)]=!![],setEncodings(_0x241f15,_0x4169bd,function(_0x1dab41){var _0x4126d2=_0x5b082b;log('done\x20clearing\x20audio'),pokeIframeAPI(_0x4126d2(0xad4),!![],_0x1dab41);},_0x598136);}catch(_0x1e5093){errorlog(_0x1e5093);}},_0x435689[_0x120577(0x68c)]=function(_0x1f76cd,_0x47fa45=_0x120577(0xaee)){var _0x2924b0=_0x120577,_0x47e2f0=getSenders2(_0x1f76cd)[_0x2924b0(0x6f7)](function(_0x337e1b){var _0x2591bb=_0x2924b0;return _0x337e1b[_0x2591bb(0xb26)]&&_0x337e1b[_0x2591bb(0xb26)][_0x2591bb(0x2fe)]==_0x2591bb(0x57d);});if(!_0x47e2f0)return log('no\x20video\x20track\x20to\x20control'),![];var _0x44d6a5={};try{_0x47fa45===!![]?(_0x44d6a5[_0x2924b0(0x68c)]=_0x2924b0(0xaee),log(_0x2924b0(0x2e9))):(_0x44d6a5[_0x2924b0(0x68c)]=_0x47fa45,log(_0x2924b0(0x5d4)+_0x47fa45)),setEncodings(_0x47e2f0,_0x44d6a5,(function(){var _0x21f41f=_0x2924b0;log(_0x21f41f(0x59e));}()));}catch(_0x190d12){errorlog(_0x190d12);}},_0x435689['limitMaxBandwidth']=function(_0x207775,_0x4dac92,_0xf57378=![]){var _0x149410=_0x120577;log(_0x149410(0x4ac)+_0x207775+_0x149410(0x2b7)+_0xf57378);if(_0x435689[_0x149410(0x9e7)]===![])return;_0x4dac92[_0x149410(0x9e7)]=parseInt(_0x435689[_0x149410(0x9e7)]/0x64*_0x207775),_0xf57378?_0x435689[_0x149410(0x524)](null):_0x435689[_0x149410(0x308)](_0x4dac92[_0x149410(0x9d6)],null);},_0x435689[_0x120577(0x9df)]=function(_0x16c699,_0x12157b=0x7d00,_0x43c52a=0x3e8){var _0x275cf0=_0x120577;log(_0x275cf0(0x61c));var _0x307bde=getSenders2(_0x16c699)[_0x275cf0(0x6f7)](function(_0xa248ad){var _0x5d3f32=_0x275cf0;return _0xa248ad[_0x5d3f32(0xb26)]&&_0xa248ad[_0x5d3f32(0xb26)][_0x5d3f32(0x2fe)]=='audio';});if(!_0x307bde)return log(_0x275cf0(0x630)),![];var _0x347aaa={};_0x347aaa[_0x275cf0(0x11e)]=_0x12157b,setEncodings(_0x307bde,_0x347aaa,function(_0x5a115a){var _0x3c05c6=_0x275cf0;pokeIframeAPI(_0x3c05c6(0x9da),_0x5a115a[0x0],_0x5a115a[0x1]),pokeIframeAPI(_0x3c05c6(0xacc),_0x5a115a[0x0],_0x5a115a[0x1]),_0x5a115a[0x2]>0x0&&setTimeout(function(){var _0x3151d4=_0x3c05c6;try{if(_0x5a115a[0x1]in _0x435689[_0x3151d4(0x76f)])var _0x3c1bcb=getSenders2(_0x5a115a[0x1])[_0x3151d4(0x6f7)](function(_0x6b9491){var _0x3efeca=_0x3151d4;return _0x6b9491['track']&&_0x6b9491['track'][_0x3efeca(0x2fe)]==_0x3efeca(0x2de);});else return![];if(!_0x3c1bcb)return log(_0x3151d4(0x630)),![];var _0x104cc5={};_0x104cc5['maxBitrate']=null,setEncodings(_0x3c1bcb,_0x104cc5,function(){var _0x30447b=_0x3151d4;log(_0x30447b(0x9ce));});}catch(_0x3d951d){errorlog(_0x3d951d);}},_0x5a115a[0x2],_0x5a115a[0x1]);},[_0x12157b,_0x16c699,_0x43c52a]);},_0x435689['directMigrateIssue']=function(_0x3f57c9,_0x178bfc,_0x1d5bbc){var _0x3bbb1d=_0x120577;pokeIframeAPI(_0x3bbb1d(0x557),_0x3f57c9,_0x1d5bbc);if(_0x435689[_0x3bbb1d(0x28c)])return generateHash(_0x3f57c9+_0x435689[_0x3bbb1d(0x28c)]+_0x435689[_0x3bbb1d(0x27e)],0x10)[_0x3bbb1d(0x7b6)](function(_0x368de9){var _0x100f62=_0x3bbb1d,_0x2701dd={};_0x178bfc[_0x100f62(0x725)]&&(_0x178bfc['roomenc']=_0x368de9);if(_0x435689[_0x100f62(0x781)]&&_0x435689[_0x100f62(0xb2f)])_0x2701dd[_0x100f62(0x3f0)]=_0x1d5bbc,_0x2701dd[_0x100f62(0xb0b)]=_0x368de9,_0x2701dd[_0x100f62(0x8fa)]=_0x178bfc,_0x435689[_0x100f62(0x43f)](_0x2701dd,_0x435689[_0x100f62(0xb2f)]),log(_0x2701dd);else{if(_0x178bfc[_0x100f62(0x725)])_0x2701dd[_0x100f62(0xaa1)]=_0x100f62(0x3f0),_0x2701dd['transferSettings']=_0x178bfc,log(_0x2701dd),_0x435689['sendRequest'](_0x2701dd,_0x1d5bbc,function(){var _0x42fff6=_0x100f62,_0x6ae844={};_0x6ae844[_0x42fff6(0xaa1)]=_0x42fff6(0x3f0),_0x6ae844[_0x42fff6(0xb0b)]=_0x368de9,_0x6ae844[_0x42fff6(0x5df)]=_0x1d5bbc,_0x435689['sendMsg'](_0x6ae844);}),log(_0x2701dd);else{if('broadcast'in _0x178bfc)_0x2701dd[_0x100f62(0xaa1)]=_0x100f62(0x3f0),_0x2701dd[_0x100f62(0x8fa)]=_0x178bfc,delete _0x2701dd[_0x100f62(0x8fa)][_0x100f62(0xb0b)],delete _0x2701dd[_0x100f62(0x8fa)][_0x100f62(0xc13)],log(_0x2701dd),_0x435689[_0x100f62(0x43f)](_0x2701dd,_0x1d5bbc,function(){var _0x41b8f8=_0x100f62,_0x151cd6={};_0x151cd6['request']='migrate',_0x151cd6[_0x41b8f8(0xb0b)]=_0x368de9,_0x151cd6['target']=_0x1d5bbc,_0x435689[_0x41b8f8(0x3ec)](_0x151cd6);}),log(_0x2701dd);else Object[_0x100f62(0x161)](_0x178bfc)[_0x100f62(0xb04)]?(_0x2701dd[_0x100f62(0xaa1)]=_0x100f62(0x3f0),_0x2701dd['transferSettings']=_0x178bfc,delete _0x2701dd[_0x100f62(0x8fa)][_0x100f62(0xb0b)],delete _0x2701dd['transferSettings'][_0x100f62(0xc13)],log(_0x2701dd),_0x435689[_0x100f62(0x43f)](_0x2701dd,_0x1d5bbc,function(){var _0x5efefb=_0x100f62,_0x4d1ca5={};_0x4d1ca5[_0x5efefb(0xaa1)]=_0x5efefb(0x3f0),_0x4d1ca5['roomid']=_0x368de9,_0x4d1ca5[_0x5efefb(0x5df)]=_0x1d5bbc,_0x435689[_0x5efefb(0x3ec)](_0x4d1ca5);}),log(_0x2701dd)):(_0x2701dd[_0x100f62(0xaa1)]='migrate',_0x2701dd[_0x100f62(0xb0b)]=_0x368de9,_0x2701dd[_0x100f62(0x5df)]=_0x1d5bbc,_0x435689[_0x100f62(0x3ec)](_0x2701dd));}}})[_0x3bbb1d(0x501)](errorlog);else{_0x178bfc[_0x3bbb1d(0x725)]&&(_0x178bfc[_0x3bbb1d(0xc13)]=_0x3f57c9);var _0x23c9c0={};if(_0x435689[_0x3bbb1d(0x781)]&&_0x435689[_0x3bbb1d(0xb2f)])_0x23c9c0[_0x3bbb1d(0x3f0)]=_0x1d5bbc,_0x23c9c0[_0x3bbb1d(0xb0b)]=_0x3f57c9,_0x23c9c0[_0x3bbb1d(0x8fa)]=_0x178bfc,_0x435689[_0x3bbb1d(0x43f)](_0x23c9c0,_0x435689[_0x3bbb1d(0xb2f)]),log(_0x23c9c0);else{if(_0x178bfc[_0x3bbb1d(0x725)])_0x23c9c0[_0x3bbb1d(0xaa1)]=_0x3bbb1d(0x3f0),_0x23c9c0[_0x3bbb1d(0x8fa)]=_0x178bfc,_0x435689['sendRequest'](_0x23c9c0,_0x1d5bbc,function(){var _0x1e660b=_0x3bbb1d,_0x2338b7={};_0x2338b7['request']='migrate',_0x2338b7[_0x1e660b(0xb0b)]=_0x3f57c9,_0x2338b7[_0x1e660b(0x5df)]=_0x1d5bbc,_0x435689[_0x1e660b(0x3ec)](_0x2338b7);});else{if('broadcast'in _0x178bfc)_0x23c9c0[_0x3bbb1d(0xaa1)]=_0x3bbb1d(0x3f0),_0x23c9c0['transferSettings']=_0x178bfc,delete _0x23c9c0[_0x3bbb1d(0x8fa)][_0x3bbb1d(0xb0b)],delete _0x23c9c0[_0x3bbb1d(0x8fa)]['roomenc'],_0x435689[_0x3bbb1d(0x43f)](_0x23c9c0,_0x1d5bbc,function(){var _0x392171=_0x3bbb1d,_0x94e670={};_0x94e670['request']='migrate',_0x94e670[_0x392171(0xb0b)]=_0x3f57c9,_0x94e670[_0x392171(0x5df)]=_0x1d5bbc,_0x435689[_0x392171(0x3ec)](_0x94e670);});else Object[_0x3bbb1d(0x161)](_0x178bfc)[_0x3bbb1d(0xb04)]?(_0x23c9c0['request']=_0x3bbb1d(0x3f0),_0x23c9c0[_0x3bbb1d(0x8fa)]=_0x178bfc,delete _0x23c9c0[_0x3bbb1d(0x8fa)][_0x3bbb1d(0xb0b)],delete _0x23c9c0['transferSettings']['roomenc'],log(_0x23c9c0),_0x435689['sendRequest'](_0x23c9c0,_0x1d5bbc,function(){var _0x6c26eb=_0x3bbb1d,_0x325b40={};_0x325b40[_0x6c26eb(0xaa1)]=_0x6c26eb(0x3f0),_0x325b40[_0x6c26eb(0xb0b)]=_0x3f57c9,_0x325b40[_0x6c26eb(0x5df)]=_0x1d5bbc,_0x435689['sendMsg'](_0x325b40);}),log(_0x23c9c0)):(_0x23c9c0[_0x3bbb1d(0xaa1)]='migrate',_0x23c9c0[_0x3bbb1d(0xb0b)]=_0x3f57c9,_0x23c9c0[_0x3bbb1d(0x5df)]=_0x1d5bbc,_0x435689[_0x3bbb1d(0x3ec)](_0x23c9c0));}}}},_0x435689[_0x120577(0x553)]=async function(_0x46e1de,_0x50f725){var _0x25b8cc=_0x120577;_0x50f725=parseInt(_0x50f725);try{var _0x3aa442=getSenders2(_0x46e1de)[_0x25b8cc(0x6f7)](function(_0x39ac15){var _0x308f85=_0x25b8cc;return _0x39ac15[_0x308f85(0xb26)]&&_0x39ac15[_0x308f85(0xb26)]['kind']==_0x308f85(0x2de);});if(!_0x3aa442){log('can\x27t\x20change\x20audio\x20bitrate;\x20no\x20audio\x20sender\x20found');return;}var _0x1aaa77={};if(_0x50f725<0x0){_0x1aaa77['active']=!![];if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad)){_0x50f725=0x20;if(_0x435689[_0x25b8cc(0x76f)][_0x46e1de][_0x25b8cc(0x9da)]!==![])_0x50f725=_0x435689[_0x25b8cc(0x76f)][_0x46e1de]['setAudioBitrate'];else _0x435689[_0x25b8cc(0xa3f)]&&(_0x50f725=_0x435689[_0x25b8cc(0xa3f)]);_0x1aaa77[_0x25b8cc(0x11e)]=_0x50f725*0x400;}else _0x435689[_0x25b8cc(0x76f)][_0x46e1de][_0x25b8cc(0x9da)]!==![]?(_0x50f725=_0x435689['pcs'][_0x46e1de]['setAudioBitrate'],_0x1aaa77[_0x25b8cc(0x11e)]=_0x50f725*0x400):_0x1aaa77[_0x25b8cc(0x11e)]=null;}else _0x50f725===0x0?_0x1aaa77[_0x25b8cc(0xaea)]=![]:(_0x1aaa77[_0x25b8cc(0xaea)]=!![],_0x1aaa77[_0x25b8cc(0x11e)]=_0x50f725*0x400);_0x435689[_0x25b8cc(0x76f)][_0x46e1de]['audioMutedOverride']&&(_0x1aaa77[_0x25b8cc(0xaea)]=![]),setEncodings(_0x3aa442,_0x1aaa77,function(_0x2d0c4a){var _0x43f7d1=_0x25b8cc;pokeIframeAPI(_0x43f7d1(0x9da),_0x2d0c4a[0x0],_0x2d0c4a[0x1]),pokeIframeAPI(_0x43f7d1(0xacc),_0x2d0c4a[0x0],_0x2d0c4a[0x1]),log(_0x43f7d1(0x462));},[_0x50f725,_0x46e1de]);}catch(_0x61c264){errorlog(_0x61c264),log(_0x46e1de),log(_0x435689['pcs'][_0x46e1de]);}},_0x435689[_0x120577(0xb1a)]=function(_0x3b222f){var _0xe652=_0x120577;if(_0x435689[_0xe652(0xafd)]&&_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x4ba)]===!![])_0x435689['limitBitrate'](_0x3b222f,0x0),_0x435689['pcs'][_0x3b222f][_0xe652(0x18d)]===0x0&&(_0x435689['pcs'][_0x3b222f]['obsState'][_0xe652(0x3bf)]===![]?_0x435689[_0xe652(0x553)](_0x3b222f,0x0):_0x435689[_0xe652(0x553)](_0x3b222f,-0x1));else{if(_0x435689[_0xe652(0x76f)][_0x3b222f]&&_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x18d)]!==![]){if(_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x599)][_0xe652(0x3bf)]===![]){var _0xc8895a=_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x18d)];_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x89f)]&&_0x435689[_0xe652(0x76f)][_0x3b222f]['savedBitrate']>0x0&&(_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x89f)]<_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x18d)]&&(_0xc8895a=_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x89f)])),_0x435689[_0xe652(0x308)](_0x3b222f,_0xc8895a),_0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x18d)]===0x0&&_0x435689[_0xe652(0x553)](_0x3b222f,0x0);}else _0x435689[_0xe652(0x76f)][_0x3b222f][_0xe652(0x18d)]===0x0&&(_0x435689[_0xe652(0x553)](_0x3b222f,-0x1),_0x435689[_0xe652(0x207)](),_0x435689[_0xe652(0x4d8)]&&_0x435689[_0xe652(0x308)](_0x3b222f,null));}else _0x435689[_0xe652(0x207)](),_0x435689[_0xe652(0x4d8)]&&_0x435689[_0xe652(0x308)](_0x3b222f,null);}},_0x435689[_0x120577(0x207)]=function(_0x499b62=0x0,_0x111191=![]){var _0x49f731=_0x120577;if(!_0x435689[_0x49f731(0x361)])return _0x499b62;if(!_0x435689[_0x49f731(0xb0b)]||_0x435689[_0x49f731(0xa34)]!==![])return log(_0x49f731(0x69a)),_0x435689['limitTotalBitrateAll'](_0x499b62,_0x111191),_0x499b62;if((iOS||iPad)&&SafariVersion&&SafariVersion<=0xd)return _0x499b62;var _0x9c2c87=_0x499b62;if(_0x111191===![])_0x9c2c87=0x0;else _0x9c2c87<0x0&&(_0x9c2c87=_0x435689[_0x49f731(0x76f)][_0x111191][_0x49f731(0x9d8)]||Math[_0x49f731(0x452)](_0x435689['outboundVideoBitrate']||0x0||(_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x9e7)]||0x0))||0x9c4);var _0x100776=0x0;for(var _0x5c8789 in _0x435689[_0x49f731(0x76f)]){if(_0x111191===_0x5c8789)continue;if(!_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x2ba)])continue;try{var _0x5884e2=getSenders2(_0x5c8789)[_0x49f731(0x6f7)](function(_0xe9bcf1){var _0x2ae289=_0x49f731;return _0xe9bcf1[_0x2ae289(0xb26)]&&_0xe9bcf1[_0x2ae289(0xb26)][_0x2ae289(0x2fe)]=='video';});if(!_0x5884e2)continue;var _0x574838=_0x5884e2[_0x49f731(0xb22)]();if(!_0x574838[_0x49f731(0x9e4)]||_0x574838['encodings'][_0x49f731(0xb04)]==0x0){_0x435689['pcs'][_0x5c8789]['setBitrate']<0x0?_0x9c2c87+=Math[_0x49f731(0x452)](_0x435689['outboundVideoBitrate']||0x0||(_0x435689['pcs'][_0x5c8789][_0x49f731(0x9e7)]||0x0))||0x9c4:_0x9c2c87+=_0x435689['pcs'][_0x5c8789]['setBitrate']||Math[_0x49f731(0x452)](_0x435689[_0x49f731(0x62c)]||0x0||(_0x435689[_0x49f731(0x76f)][_0x5c8789]['maxBandwidth']||0x0))||0x9c4;warnlog(_0x9c2c87),_0x100776+=0x1;continue;}if(_0x574838[_0x49f731(0x9e4)][0x0]['active']==![])continue;if(_0x574838[_0x49f731(0x9e4)][0x0][_0x49f731(0x11e)])_0x49f731(0x8cc)in _0x435689[_0x49f731(0x76f)][_0x5c8789]?_0x9c2c87+=parseInt(_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x8cc)]):_0x9c2c87+=parseInt(_0x574838[_0x49f731(0x9e4)][0x0][_0x49f731(0x11e)])/0x400;else _0x435689[_0x49f731(0x76f)][_0x5c8789]['setBitrate']<0x0?_0x9c2c87+=Math['min'](_0x435689[_0x49f731(0x62c)]||0x0||(_0x435689['pcs'][_0x5c8789][_0x49f731(0x9e7)]||0x0))||0x9c4:(_0x9c2c87+=_0x435689['pcs'][_0x5c8789][_0x49f731(0x9d8)]||Math[_0x49f731(0x452)](_0x435689[_0x49f731(0x62c)]||0x0||(_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x9e7)]||0x0))||0x9c4,warnlog(_0x9c2c87));_0x100776+=0x1;}catch(_0x27269b){errorlog(_0x27269b);}}if(!_0x9c2c87)return _0x9c2c87;warnlog('totalBitrate:\x20'+_0x9c2c87);var _0x3f1829=parseFloat(_0x9c2c87/_0x435689['limitTotalBitrate']);_0x3f1829<0x1&&(_0x3f1829=0x1);for(var _0x5c8789 in _0x435689[_0x49f731(0x76f)]){if(_0x111191===_0x5c8789)continue;if(!_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x2ba)])continue;try{var _0x5884e2=getSenders2(_0x5c8789)['find'](function(_0xb4041b){var _0x49924f=_0x49f731;return _0xb4041b['track']&&_0xb4041b[_0x49924f(0xb26)][_0x49924f(0x2fe)]==_0x49924f(0x57d);});if(!_0x5884e2)continue;var _0x574838=_0x5884e2[_0x49f731(0xb22)]();if(!_0x574838[_0x49f731(0x9e4)]||_0x574838['encodings'][_0x49f731(0xb04)]==0x0){if(_0x435689[_0x49f731(0x76f)][_0x5c8789]['setBitrate']<0x0)var _0x358b8b=Math[_0x49f731(0x452)](_0x435689[_0x49f731(0x62c)]||0x0||(_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x9e7)]||0x0))||0x9c4;else var _0x358b8b=_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x9d8)]||Math[_0x49f731(0x452)](_0x435689[_0x49f731(0x62c)]||0x0||(_0x435689['pcs'][_0x5c8789]['maxBandwidth']||0x0))||0x9c4;var _0x47ec35=parseInt(_0x358b8b/_0x3f1829);_0x435689[_0x49f731(0x308)](_0x5c8789,_0x47ec35,!![]);continue;}if(_0x574838[_0x49f731(0x9e4)][0x0][_0x49f731(0xaea)]==![])continue;if(_0x574838[_0x49f731(0x9e4)][0x0][_0x49f731(0x11e)]){if(_0x49f731(0x8cc)in _0x435689[_0x49f731(0x76f)][_0x5c8789])var _0x358b8b=parseInt(_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x8cc)]);else var _0x358b8b=parseInt(parseInt(_0x574838[_0x49f731(0x9e4)][0x0][_0x49f731(0x11e)])/0x400);var _0x47ec35=parseInt(_0x358b8b/_0x3f1829);_0x435689[_0x49f731(0x308)](_0x5c8789,_0x47ec35,!![]);}else{if(_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x9d8)]<0x0)var _0x358b8b=Math[_0x49f731(0x452)](_0x435689['outboundVideoBitrate']||0x0||(_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x9e7)]||0x0))||0x9c4;else var _0x358b8b=_0x435689[_0x49f731(0x76f)][_0x5c8789][_0x49f731(0x9d8)]||Math[_0x49f731(0x452)](_0x435689[_0x49f731(0x62c)]||0x0||(_0x435689[_0x49f731(0x76f)][_0x5c8789]['maxBandwidth']||0x0))||0x9c4;var _0x47ec35=parseInt(_0x358b8b/_0x3f1829);_0x435689[_0x49f731(0x308)](_0x5c8789,_0x47ec35,!![]);}}catch(_0x145382){errorlog(_0x145382);}}return parseInt(_0x499b62/_0x3f1829);},_0x435689['limitTotalBitrateAll']=function(_0x20cdd8=0x0,_0x1aadcc=![]){var _0x5e0e2a=_0x120577;if(!_0x435689[_0x5e0e2a(0x361)])return _0x20cdd8;if((iOS||iPad)&&SafariVersion&&SafariVersion<=0xd)return _0x20cdd8;var _0x1db39d=_0x20cdd8;if(_0x1aadcc===![])_0x1db39d=0x0;else _0x1db39d<0x0&&(_0x1db39d=_0x435689[_0x5e0e2a(0x76f)][_0x1aadcc][_0x5e0e2a(0x9d8)]||Math[_0x5e0e2a(0x452)](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689['pcs'][_0x5a0fee][_0x5e0e2a(0x9e7)]||0x0))||0x9c4);var _0x2b6d70=0x0;for(var _0x5a0fee in _0x435689[_0x5e0e2a(0x76f)]){if(_0x1aadcc===_0x5a0fee)continue;try{var _0x277279=getSenders2(_0x5a0fee)['find'](function(_0x162789){var _0x319109=_0x5e0e2a;return _0x162789[_0x319109(0xb26)]&&_0x162789['track']['kind']=='video';});if(!_0x277279)continue;var _0x560974=_0x277279[_0x5e0e2a(0xb22)]();if(!_0x560974['encodings']||_0x560974[_0x5e0e2a(0x9e4)][_0x5e0e2a(0xb04)]==0x0){_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9d8)]<0x0?_0x1db39d+=Math['min'](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee]['maxBandwidth']||0x0))||0x9c4:_0x1db39d+=_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee]['setBitrate']||Math['min'](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9e7)]||0x0))||0x9c4;warnlog(_0x1db39d),_0x2b6d70+=0x1;continue;}if(_0x560974[_0x5e0e2a(0x9e4)][0x0]['active']==![])continue;if(_0x560974[_0x5e0e2a(0x9e4)][0x0][_0x5e0e2a(0x11e)])'preLimitedBitrate'in _0x435689[_0x5e0e2a(0x76f)][_0x5a0fee]?_0x1db39d+=parseInt(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x8cc)]):_0x1db39d+=parseInt(_0x560974[_0x5e0e2a(0x9e4)][0x0][_0x5e0e2a(0x11e)])/0x400;else _0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9d8)]<0x0?_0x1db39d+=Math[_0x5e0e2a(0x452)](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee]['maxBandwidth']||0x0))||0x9c4:(_0x1db39d+=_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9d8)]||Math[_0x5e0e2a(0x452)](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9e7)]||0x0))||0x9c4,warnlog(_0x1db39d));_0x2b6d70+=0x1;}catch(_0x2b78df){errorlog(_0x2b78df);}}if(!_0x1db39d)return _0x1db39d;warnlog(_0x5e0e2a(0x7c4)+_0x1db39d);var _0xc96ade=parseFloat(_0x1db39d/_0x435689['limitTotalBitrate']);_0xc96ade<0x1&&(_0xc96ade=0x1);for(var _0x5a0fee in _0x435689['pcs']){if(_0x1aadcc===_0x5a0fee)continue;try{var _0x277279=getSenders2(_0x5a0fee)['find'](function(_0x1d663c){var _0x49ffba=_0x5e0e2a;return _0x1d663c[_0x49ffba(0xb26)]&&_0x1d663c['track'][_0x49ffba(0x2fe)]==_0x49ffba(0x57d);});if(!_0x277279)continue;var _0x560974=_0x277279[_0x5e0e2a(0xb22)]();if(!_0x560974[_0x5e0e2a(0x9e4)]||_0x560974['encodings'][_0x5e0e2a(0xb04)]==0x0){if(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9d8)]<0x0)var _0x400e31=Math[_0x5e0e2a(0x452)](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689['pcs'][_0x5a0fee][_0x5e0e2a(0x9e7)]||0x0))||0x9c4;else var _0x400e31=_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9d8)]||Math[_0x5e0e2a(0x452)](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9e7)]||0x0))||0x9c4;var _0x547092=parseInt(_0x400e31/_0xc96ade);_0x435689[_0x5e0e2a(0x308)](_0x5a0fee,_0x547092,!![]);continue;}if(_0x560974[_0x5e0e2a(0x9e4)][0x0][_0x5e0e2a(0xaea)]==![])continue;if(_0x560974[_0x5e0e2a(0x9e4)][0x0][_0x5e0e2a(0x11e)]){if(_0x5e0e2a(0x8cc)in _0x435689[_0x5e0e2a(0x76f)][_0x5a0fee])var _0x400e31=parseInt(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x8cc)]);else var _0x400e31=parseInt(parseInt(_0x560974['encodings'][0x0][_0x5e0e2a(0x11e)])/0x400);var _0x547092=parseInt(_0x400e31/_0xc96ade);_0x435689[_0x5e0e2a(0x308)](_0x5a0fee,_0x547092,!![]);}else{if(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9d8)]<0x0)var _0x400e31=Math['min'](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689[_0x5e0e2a(0x76f)][_0x5a0fee][_0x5e0e2a(0x9e7)]||0x0))||0x9c4;else var _0x400e31=_0x435689['pcs'][_0x5a0fee][_0x5e0e2a(0x9d8)]||Math[_0x5e0e2a(0x452)](_0x435689[_0x5e0e2a(0x62c)]||0x0||(_0x435689['pcs'][_0x5a0fee]['maxBandwidth']||0x0))||0x9c4;var _0x547092=parseInt(_0x400e31/_0xc96ade);_0x435689[_0x5e0e2a(0x308)](_0x5a0fee,_0x547092,!![]);}}catch(_0x4eeb63){errorlog(_0x4eeb63);}}return parseInt(_0x20cdd8/_0xc96ade);},_0x435689['announceCoDirector']=function(_0x5e2304,_0x1bf618=![]){var _0x322019=_0x120577,_0x4fff15={};_0x4fff15[_0x322019(0xb41)]={},_0x4fff15[_0x322019(0xb41)][_0x322019(0x52a)]=[_0x5e2304],_0x435689[_0x322019(0xbd9)](_0x4fff15,_0x1bf618),pokeIframeAPI(_0x322019(0x560),_0x5e2304);},_0x435689[_0x120577(0x524)]=function(_0x2b995b=null){var _0x5e5062=_0x120577;if(!_0x435689[_0x5e5062(0xc35)])return;_0x435689[_0x5e5062(0xc35)]['bitrateTimeout']&&(clearInterval(_0x435689['whipOut'][_0x5e5062(0x212)]),_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x212)]=null);if(_0x2b995b===null){if(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x89f)]===![])return;_0x2b995b=_0x435689[_0x5e5062(0xc35)]['savedBitrate'];}_0x2b995b=parseInt(_0x2b995b);if(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x9d8)]&&_0x2b995b>_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x9d8)])_0x2b995b=_0x435689['whipOut']['setBitrate'];else _0x435689[_0x5e5062(0xc35)][_0x5e5062(0x9d8)]===![]&&(_0x2b995b<0x0&&(_0x435689['outboundVideoBitrate']?_0x2b995b=_0x435689[_0x5e5062(0x62c)]:_0x2b995b=0x9c4));_0x435689[_0x5e5062(0x4d8)]&&(_0x2b995b>_0x435689['maxvideobitrate']&&(_0x2b995b=_0x435689[_0x5e5062(0x4d8)]));_0x435689[_0x5e5062(0xc35)]['savedBitrate']=_0x2b995b;_0x435689['whipOut'][_0x5e5062(0x18d)]!==![]&&(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x599)][_0x5e5062(0x3bf)]===![]&&(_0x2b995b>_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x18d)]&&(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x89f)]=_0x2b995b,_0x2b995b=parseInt(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x18d)])||0x0)));if(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x9e7)]!==null){if(_0x435689['whipOut'][_0x5e5062(0x9e7)]<_0x2b995b)_0x2b995b=_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x9e7)],_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x5c2)][_0x5e5062(0x336)]=_0x2b995b,warnlog('Max\x20bandwidth\x20being\x20capped:\x20'+_0x2b995b+_0x5e5062(0x804));else _0x435689[_0x5e5062(0xc35)][_0x5e5062(0x5c2)]&&(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x5c2)][_0x5e5062(0x336)]=![]);}else _0x5e5062(0x336)in _0x435689[_0x5e5062(0xc35)][_0x5e5062(0x5c2)]&&(_0x435689[_0x5e5062(0xc35)]['stats']['max_bandwidth_capped_kbps']=![]);if(_0x2b995b===0x0){var _0x17ce59=Date['now']()-_0x435689[_0x5e5062(0xc35)]['startTime'];_0x17ce59<_0x435689[_0x5e5062(0x405)]&&(_0x2b995b=_0x435689[_0x5e5062(0x816)],log(_0x5e5062(0xb00)+(Date[_0x5e5062(0x1ab)]()-_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x6ae)])),_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x212)]=setTimeout(function(){var _0x23a4d6=_0x5e5062;try{warnlog(_0x23a4d6(0xa05)+(Date['now']()-_0x435689['whipOut'][_0x23a4d6(0x6ae)])),_0x435689[_0x23a4d6(0x524)](null);}catch(_0x440639){}},_0x435689[_0x5e5062(0x405)]-_0x17ce59+0x5));}try{if((iOS||iPad)&&SafariVersion&&SafariVersion<=0xd){log(_0x5e5062(0xc42));var _0xdecade=_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x83c)]()[_0x5e5062(0x6f7)](function(_0x2c9eac){var _0xf9abde=_0x5e5062;return _0x2c9eac[_0xf9abde(0xb26)]&&_0x2c9eac[_0xf9abde(0xb26)]['kind']==_0xf9abde(0x57d);});if(!_0xdecade){warnlog('can\x27t\x20change\x20bitrate;\x20no\x20video\x20sender\x20found');return;}var _0x151130={};if(_0x2b995b<0x0)_0x151130[_0x5e5062(0xaea)]=!![],_0x2b995b=0x9c4,_0x435689[_0x5e5062(0xaa6)]&&(_0x2b995b=_0x435689['bitrate']),_0x435689[_0x5e5062(0x4d8)]&&(_0x2b995b>_0x435689['maxvideobitrate']&&(_0x2b995b=_0x435689[_0x5e5062(0x4d8)])),_0x151130['maxBitrate']=_0x2b995b*0x400;else _0x2b995b===0x0?_0x151130[_0x5e5062(0xaea)]=![]:(_0x151130[_0x5e5062(0xaea)]=!![],_0x151130[_0x5e5062(0x11e)]=_0x2b995b*0x400);setEncodings(_0xdecade,_0x151130,function(_0x17b405){var _0x2e604a=_0x5e5062;pokeIframeAPI(_0x2e604a(0xb0f),_0x17b405),log(_0x2e604a(0x96b)+_0x17b405);},_0x2b995b);return;}else{if(_0x5e5062(0x2a8)in window&&'setParameters'in window[_0x5e5062(0x2a8)][_0x5e5062(0x128)]){var _0xdecade=_0x435689['whipOut'][_0x5e5062(0x83c)]()[_0x5e5062(0x6f7)](function(_0x3e0ada){var _0x994ef2=_0x5e5062;return _0x3e0ada[_0x994ef2(0xb26)]&&_0x3e0ada[_0x994ef2(0xb26)][_0x994ef2(0x2fe)]==_0x994ef2(0x57d);});if(!_0xdecade){log(_0x5e5062(0x815));return;}var _0x151130={};if(_0x2b995b<0x0)_0x151130[_0x5e5062(0xaea)]==![]&&(_0x151130[_0x5e5062(0xaea)]=!![]),_0x151130['maxBitrate']=null;else _0x2b995b===0x0?(_0x151130[_0x5e5062(0xaea)]=![],Firefox&&(_0x151130[_0x5e5062(0x11e)]=0x1)):(_0x151130[_0x5e5062(0xaea)]=!![],_0x151130[_0x5e5062(0x11e)]=_0x2b995b*0x400);iPad||iOS||Firefox?_0x435689[_0x5e5062(0xc35)]['bitrateTimeoutFirefox']?(clearInterval(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x686)]),_0x435689['whipOut'][_0x5e5062(0x686)]=setTimeout(function(){var _0x58e2f7=_0x5e5062;log(_0x58e2f7(0xac3)+_0x2b995b),_0x435689[_0x58e2f7(0xc35)][_0x58e2f7(0x686)]=![],_0x435689[_0x58e2f7(0x524)](null);},0x1f4)):(_0x435689[_0x5e5062(0xc35)][_0x5e5062(0x686)]=setTimeout(function(){_0x435689['whipOut']['bitrateTimeoutFirefox']=![];},0x1f4),setEncodings(_0xdecade,_0x151130,function(_0x2e4394){var _0x428034=_0x5e5062;log(_0x428034(0x261)+_0x2e4394),pokeIframeAPI('set-meshcast-video-bitrate',_0x2e4394);},_0x2b995b)):setEncodings(_0xdecade,_0x151130,function(_0x505ccf){var _0x33c641=_0x5e5062;log('bandwidth\x20set\x20i!\x20'+_0x505ccf),pokeIframeAPI(_0x33c641(0xb0f),_0x505ccf);},_0x2b995b);return;}else warnlog(_0x5e5062(0x67a));}}catch(_0x346f7f){errorlog(_0x346f7f);}},_0x435689[_0x120577(0x916)]=function(_0x3a6114,_0x9d8f51){var _0x4d8c16=_0x120577;_0x9d8f51===![]?(_0x435689['pcs'][_0x3a6114]['setBitrate']=![],_0x435689[_0x4d8c16(0x308)](_0x3a6114,-0x1)):(_0x9d8f51=parseInt(_0x9d8f51)||-0x1,_0x9d8f51>=0x0&&(_0x435689[_0x4d8c16(0x76f)][_0x3a6114][_0x4d8c16(0x9d8)]=_0x9d8f51,_0x435689[_0x4d8c16(0x308)](_0x3a6114,_0x9d8f51)));},_0x435689['targetAudioBitrate']=function(_0x2dcdb3,_0x1aa4a8){var _0x444814=_0x120577;_0x1aa4a8===![]?(_0x435689[_0x444814(0x76f)][_0x2dcdb3][_0x444814(0x9da)]=![],_0x435689[_0x444814(0x553)](_0x2dcdb3,-0x1)):(_0x1aa4a8=parseInt(_0x1aa4a8)||-0x1,_0x1aa4a8>=0x0&&(_0x435689[_0x444814(0x76f)][_0x2dcdb3]['setAudioBitrate']=_0x1aa4a8,_0x435689['limitAudioBitrate'](_0x2dcdb3,_0x1aa4a8)));},_0x435689['limitBitrate']=function(_0x1ebcff,_0x3a70b6=null,_0x5de6d1=![]){var _0x223a3a=_0x120577;log(_0x223a3a(0x723)+_0x3a70b6);if(!(_0x1ebcff in _0x435689['pcs']))return;_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x212)]&&(clearInterval(_0x435689['pcs'][_0x1ebcff][_0x223a3a(0x212)]),_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x212)]=null);var _0x5384b7=!![];if(_0x3a70b6===null){if(_0x435689['pcs'][_0x1ebcff]['savedBitrate']===![]){if(_0x435689[_0x223a3a(0x76f)][_0x1ebcff]['maxBandwidth']===null)return;else _0x3a70b6=_0x435689['pcs'][_0x1ebcff][_0x223a3a(0x9e7)],_0x5384b7=![];}else _0x3a70b6=_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x89f)];}_0x3a70b6=parseInt(_0x3a70b6);if(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x9d8)]&&_0x3a70b6>_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x9d8)])_0x3a70b6=_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x9d8)];else _0x3a70b6<0x0&&(_0x3a70b6=_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x9d8)]||_0x435689[_0x223a3a(0x62c)]||0x9c4);let _0x98f2d3=_0x435689['maxvideobitrate'];_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x2ba)]==!![]&&(_0x98f2d3!==![]?_0x435689['roombitrate']!==![]&&(_0x435689[_0x223a3a(0x2c3)]<_0x98f2d3&&(_0x98f2d3=_0x435689[_0x223a3a(0x2c3)])):_0x98f2d3=_0x435689[_0x223a3a(0x2c3)]);_0x98f2d3&&(_0x3a70b6>_0x98f2d3&&(_0x3a70b6=_0x98f2d3));_0x5384b7&&!_0x5de6d1&&(log('save\x20bandwidth:\x20'+_0x3a70b6),_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x89f)]=_0x3a70b6);_0x435689[_0x223a3a(0x76f)][_0x1ebcff]['optimizedBitrate']!==![]&&(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x599)][_0x223a3a(0x3bf)]===![]&&(_0x3a70b6>_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x18d)]&&(_0x5384b7&&(_0x435689[_0x223a3a(0x76f)][_0x1ebcff]['savedBitrate']=_0x3a70b6),_0x3a70b6=parseInt(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x18d)])||0x0)));if(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x9e7)]!==null){if(_0x435689[_0x223a3a(0x76f)][_0x1ebcff]['maxBandwidth']<_0x3a70b6)_0x3a70b6=_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x9e7)],_0x435689[_0x223a3a(0x76f)][_0x1ebcff]['stats'][_0x223a3a(0x336)]=_0x3a70b6,warnlog(_0x223a3a(0x334)+_0x3a70b6+_0x223a3a(0x804));else _0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x9e7)]===_0x3a70b6&&!_0x5384b7?(_0x435689['pcs'][_0x1ebcff][_0x223a3a(0x5c2)][_0x223a3a(0x336)]=_0x3a70b6,warnlog(_0x223a3a(0x743)+_0x3a70b6+'-kbps')):(warnlog(_0x223a3a(0xb59)+_0x3a70b6+_0x223a3a(0x804)),_0x435689[_0x223a3a(0x76f)][_0x1ebcff]['stats']['max_bandwidth_capped_kbps']=![]);}else _0x223a3a(0x336)in _0x435689[_0x223a3a(0x76f)][_0x1ebcff]['stats']&&(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x5c2)][_0x223a3a(0x336)]=![]);_0x5de6d1===![]&&(_0x435689[_0x223a3a(0x361)]&&(_0x435689[_0x223a3a(0x76f)][_0x1ebcff]['preLimitedBitrate']=_0x3a70b6,_0x3a70b6=_0x435689[_0x223a3a(0x207)](_0x3a70b6,_0x1ebcff)));if(_0x3a70b6===0x0){var _0xfa47bc=Date[_0x223a3a(0x1ab)]()-_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x6ae)];_0xfa47bc<_0x435689['rampUpTime']&&(_0x3a70b6=_0x435689['preloadbitrate'],log(_0x223a3a(0xb00)+(Date[_0x223a3a(0x1ab)]()-_0x435689[_0x223a3a(0x76f)][_0x1ebcff]['startTime'])),_0x435689['pcs'][_0x1ebcff][_0x223a3a(0x212)]=setTimeout(function(_0x405599){var _0x57ba8e=_0x223a3a;try{warnlog('stopping\x20some\x20preload\x20bitrate\x20'+(Date[_0x57ba8e(0x1ab)]()-_0x435689[_0x57ba8e(0x76f)][_0x405599][_0x57ba8e(0x6ae)])),_0x435689['limitBitrate'](_0x405599,null);}catch(_0x234c06){}},_0x435689[_0x223a3a(0x405)]-_0xfa47bc+0x5,_0x1ebcff));}try{if((iOS||iPad)&&SafariVersion&&SafariVersion<=0xd){log(_0x223a3a(0x926));if(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x2ba)]==!![]&&_0x435689['pcs'][_0x1ebcff][_0x223a3a(0x337)]==![])return;var _0x52f6be=getSenders2(_0x1ebcff)[_0x223a3a(0x6f7)](function(_0x661ca0){var _0x4cdaa2=_0x223a3a;return _0x661ca0[_0x4cdaa2(0xb26)]&&_0x661ca0[_0x4cdaa2(0xb26)][_0x4cdaa2(0x2fe)]=='video';});if(!_0x52f6be){log('can\x27t\x20change\x20bitrate;\x20no\x20video\x20sender\x20found');return;}var _0x4500f4={};_0x3a70b6===0x0?_0x4500f4['active']=![]:(_0x4500f4[_0x223a3a(0xaea)]=!![],_0x4500f4[_0x223a3a(0x11e)]=_0x3a70b6*0x400);setEncodings(_0x52f6be,_0x4500f4,function(_0xbcddb8){var _0x290a08=_0x223a3a;pokeIframeAPI(_0x290a08(0x17a),_0xbcddb8[0x0],_0xbcddb8[0x1]),pokeIframeAPI('set-video-bitrate',_0xbcddb8[0x0],_0xbcddb8[0x1]),log(_0x290a08(0x54f)+_0xbcddb8[0x0]);},[_0x3a70b6,_0x1ebcff]);return;}else{if(_0x223a3a(0x2a8)in window&&'setParameters'in window[_0x223a3a(0x2a8)][_0x223a3a(0x128)]){var _0x52f6be=getSenders2(_0x1ebcff)[_0x223a3a(0x6f7)](function(_0xe2989e){var _0x55ee3f=_0x223a3a;return _0xe2989e[_0x55ee3f(0xb26)]&&_0xe2989e[_0x55ee3f(0xb26)]['kind']==_0x55ee3f(0x57d);});if(!_0x52f6be){log('can\x27t\x20change\x20bitrate;\x20no\x20video\x20sender\x20found');return;}var _0x4500f4={};_0x3a70b6===0x0?(_0x4500f4[_0x223a3a(0xaea)]=![],Firefox&&(_0x4500f4[_0x223a3a(0x11e)]=0x1,_0x4500f4[_0x223a3a(0xbcc)]=0x3e8)):(_0x4500f4['active']=!![],_0x4500f4[_0x223a3a(0x11e)]=_0x3a70b6*0x400);if(_0x3a70b6!==0x0){var _0x4d9a2f=_0x435689[_0x223a3a(0xc10)](_0x1ebcff,_0x3a70b6);if(_0x4d9a2f<=0x0||_0x4d9a2f==0x64){var _0x4a5362=getChromiumVersion();_0x4a5362>0x50?_0x4500f4[_0x223a3a(0xbcc)]=null:_0x4500f4[_0x223a3a(0xbcc)]=0x1;}else _0x4500f4[_0x223a3a(0xbcc)]=0x64/_0x4d9a2f;iPad||iOS||Firefox?_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x686)]?(clearInterval(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x686)]),_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x686)]=setTimeout(function(_0x7a25ec,_0x2cdf67){var _0x1b7122=_0x223a3a;log('bitrate\x20timeout;\x20ios/firefox\x20specific:\x20'+_0x3a70b6),_0x435689[_0x1b7122(0x76f)][_0x7a25ec][_0x1b7122(0x686)]=![],_0x435689['limitBitrate'](_0x7a25ec,null,_0x2cdf67);},0x1f4,_0x1ebcff,_0x5de6d1)):(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x686)]=setTimeout(function(_0x58d171){var _0x2648d2=_0x223a3a;_0x435689[_0x2648d2(0x76f)][_0x58d171]['bitrateTimeoutFirefox']=![];},0x1f4,_0x1ebcff),setEncodings(_0x52f6be,_0x4500f4,function(_0x5174ee){var _0xc36e0b=_0x223a3a;log(_0xc36e0b(0x2e8)+_0x5174ee[0x0]),_0x435689[_0xc36e0b(0x76f)][_0x5174ee[0x1]]['stats'][_0xc36e0b(0x1ff)]=parseInt(_0x5174ee[0x2])+'%',pokeIframeAPI(_0xc36e0b(0x17a),_0x5174ee[0x0],_0x5174ee[0x1]),pokeIframeAPI(_0xc36e0b(0xbf8),_0x5174ee[0x2],_0x5174ee[0x1]),pokeIframeAPI(_0xc36e0b(0xb34),_0x5174ee[0x0],_0x5174ee[0x1]),pokeIframeAPI(_0xc36e0b(0x9b9),_0x5174ee[0x2],_0x5174ee[0x1]);},[_0x3a70b6,_0x1ebcff,_0x4d9a2f])):(warnlog(_0x4500f4),setEncodings(_0x52f6be,_0x4500f4,function(_0x5104ee){var _0x4ffb03=_0x223a3a;log(_0x4ffb03(0x516)+_0x5104ee[0x0]),_0x435689[_0x4ffb03(0x76f)][_0x5104ee[0x1]]['stats']['scaleFactor']=parseInt(_0x5104ee[0x2])+'%',pokeIframeAPI(_0x4ffb03(0x17a),_0x5104ee[0x0],_0x5104ee[0x1]),pokeIframeAPI(_0x4ffb03(0xbf8),_0x5104ee[0x2],_0x5104ee[0x1]),pokeIframeAPI(_0x4ffb03(0xb34),_0x5104ee[0x0],_0x5104ee[0x1]),pokeIframeAPI('set-video-scale',_0x5104ee[0x2],_0x5104ee[0x1]);},[_0x3a70b6,_0x1ebcff,_0x4d9a2f]));}else iPad||iOS||Firefox?_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x686)]?(clearInterval(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x686)]),_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x686)]=setTimeout(function(_0xf47417,_0x423efe){var _0x216ad8=_0x223a3a;log('bitrate\x20timeout;\x20ios/firefox\x20specific:\x20'+_0x3a70b6),_0x435689['pcs'][_0xf47417]['bitrateTimeoutFirefox']=![],_0x435689[_0x216ad8(0x308)](_0xf47417,null,_0x423efe);},0x1f4,_0x1ebcff,_0x5de6d1)):(_0x435689[_0x223a3a(0x76f)][_0x1ebcff][_0x223a3a(0x686)]=setTimeout(function(_0x48994f){var _0xdebd78=_0x223a3a;_0x435689['pcs'][_0x48994f][_0xdebd78(0x686)]=![];},0x1f4,_0x1ebcff),setEncodings(_0x52f6be,_0x4500f4,function(_0x2dbb37){var _0x6e1e7f=_0x223a3a;log(_0x6e1e7f(0x70c)+_0x2dbb37[0x0]),pokeIframeAPI(_0x6e1e7f(0x17a),_0x2dbb37[0x0],_0x2dbb37[0x1]),pokeIframeAPI('set-video-bitrate',_0x2dbb37[0x0],_0x2dbb37[0x1]);},[_0x3a70b6,_0x1ebcff])):setEncodings(_0x52f6be,_0x4500f4,function(_0x3e737){var _0x4826fc=_0x223a3a;log(_0x4826fc(0x141)+_0x3e737[0x0]),pokeIframeAPI(_0x4826fc(0x17a),_0x3e737[0x0],_0x3e737[0x1]),pokeIframeAPI(_0x4826fc(0xb34),_0x3e737[0x0],_0x3e737[0x1]);},[_0x3a70b6,_0x1ebcff]);}else warnlog(_0x223a3a(0x67a));}}catch(_0x1a72a5){errorlog(_0x1a72a5);}};function _0x44a4f8(_0x50a0ce,_0x26b86d,_0x4f2e91){var _0x3255a7=_0x120577;if(_0x435689[_0x3255a7(0x5f2)])return _0x26b86d;warnlog('getOptimizedScale:\x20'+_0x26b86d+_0x3255a7(0x97e)+_0x4f2e91);if(_0x4f2e91<0x0)_0x435689[_0x3255a7(0x76f)][_0x50a0ce]['scaleDueToBitrate']=0x64;else{if(_0x4f2e91>=0x259)_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64;else{if(_0x3255a7(0xbb3)in _0x435689[_0x3255a7(0x76f)][_0x50a0ce])_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64;else{if(_0x435689[_0x3255a7(0x4b9)])_0x435689[_0x3255a7(0x76f)][_0x50a0ce]['scaleDueToBitrate']=0x64;else{var _0x43eb2a=getNativeOutputResolution();if(_0x43eb2a)try{_0x43eb2a=_0x43eb2a[_0x3255a7(0x3dd)]*_0x43eb2a[_0x3255a7(0x348)],_0x43eb2a=Math[_0x3255a7(0x259)](_0x43eb2a,0.5);}catch(_0x2e30ad){_0x43eb2a=![];}warnlog(_0x3255a7(0x3d6)+_0x43eb2a);if(_0x4f2e91>=0x15e){if(_0x43eb2a&&_0x43eb2a<=0x1e0)_0x435689['pcs'][_0x50a0ce][_0x3255a7(0xbb0)]=0x64;else{if(_0x435689['mobile']){if(_0x43eb2a&&_0x43eb2a>=0x5a0)_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x3;else _0x435689[_0x3255a7(0xb79)]?_0x43eb2a&&_0x43eb2a>=0x3c0?_0x435689['pcs'][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x2:_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64:_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x2;}else{if(_0x43eb2a&&_0x43eb2a>=0x5a0)_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/2.5;else _0x43eb2a&&_0x43eb2a>=0x3c0?_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x2:_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64;}}}else{if(_0x4f2e91>=0xc9){if(_0x43eb2a&&_0x43eb2a<0x1e0)_0x435689['pcs'][_0x50a0ce][_0x3255a7(0xbb0)]=0x64;else{if(_0x435689[_0x3255a7(0x612)]){if(_0x43eb2a&&_0x43eb2a>=0x5a0)_0x435689['pcs'][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x4;else _0x435689[_0x3255a7(0xb79)]?_0x435689['pcs'][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x2:_0x435689['pcs'][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/2.5;}else _0x43eb2a&&_0x43eb2a>=0x5a0?_0x435689[_0x3255a7(0x76f)][_0x50a0ce]['scaleDueToBitrate']=0x64/0x3:_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x2;}}else{if(_0x43eb2a&&_0x43eb2a<=0xf0)_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64;else{if(_0x4f2e91>=0x51){if(_0x435689[_0x3255a7(0x612)]){if(_0x43eb2a&&_0x43eb2a>=0x5a0)_0x435689[_0x3255a7(0x76f)][_0x50a0ce]['scaleDueToBitrate']=0x64/0x6;else _0x435689['flagship']?_0x435689['pcs'][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x3:_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x4;}else _0x43eb2a&&_0x43eb2a>=0x5a0?_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x4:_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x3;}else{if(_0x435689[_0x3255a7(0x612)]){if(_0x43eb2a&&_0x43eb2a>=0x3c0)_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x6;else _0x435689[_0x3255a7(0xb79)]?_0x435689['pcs'][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x4:_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x5;}else _0x43eb2a&&_0x43eb2a>=0x5a0?_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]=0x64/0x5:_0x435689[_0x3255a7(0x76f)][_0x50a0ce]['scaleDueToBitrate']=0x64/0x4;}}}}}}}}return _0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]<_0x26b86d&&(_0x26b86d=_0x435689[_0x3255a7(0x76f)][_0x50a0ce][_0x3255a7(0xbb0)]),_0x26b86d;}function _0x128ce6(_0x507658,_0x35404f=0x2710){var _0xb16cd1=_0x120577;_0x35404f=parseInt(_0x35404f);if(_0x435689[_0xb16cd1(0xa3f)])_0x35404f+=_0x435689['audiobitrate'];else{if(_0x435689[_0xb16cd1(0x781)]&&_0x435689[_0xb16cd1(0x18f)]==0x5)_0x35404f+=0x20;else _0x435689[_0xb16cd1(0x18f)]&&_0x435689['stereo']!=0x3?_0x435689['audioCodec']&&_0x435689[_0xb16cd1(0x844)]==_0xb16cd1(0x1a9)?_0x35404f+=_0x435689[_0xb16cd1(0x2e0)]*0x2:_0x35404f+=_0x435689[_0xb16cd1(0x2e0)]:_0x35404f+=0x20;}return log(_0xb16cd1(0x8ba)+_0x35404f),_0x35404f<0x1&&(_0x35404f=0x1),_0x507658=CodecsHandler[_0xb16cd1(0x197)](_0x507658,{'min':parseInt(_0x35404f/0xa)||0x1,'max':_0x35404f||0x1},_0x435689[_0xb16cd1(0x1a3)]),_0x507658;}_0x435689[_0x120577(0x491)]=function(_0x32db14,_0x202e33){var _0x1c798f=_0x120577;log(_0x32db14),!_0x435689[_0x1c798f(0x401)][_0x1c798f(0x30b)]&&warnlog(_0x1c798f(0x73a)),window[_0x1c798f(0x863)][_0x1c798f(0x37d)][_0x1c798f(0x9d0)]({'name':_0x1c798f(0x403)},_0x435689['mykey']['privateKey'],_0x435689[_0x1c798f(0x258)][_0x1c798f(0xc25)](_0x32db14))[_0x1c798f(0x7b6)](function(_0x10c40f){var _0x38d2d2=_0x1c798f;_0x10c40f=new Uint8Array(_0x10c40f),_0x10c40f=_0x10c40f['reduce']((_0x44bd8a,_0x19d535)=>_0x44bd8a+_0x19d535[_0x38d2d2(0x9b0)](0x10)[_0x38d2d2(0xb1b)](0x2,'0'),''),_0x202e33(_0x32db14,_0x10c40f),log(JSON[_0x38d2d2(0xc18)](_0x10c40f));})[_0x1c798f(0x501)](errorlog);},_0x435689[_0x120577(0x787)]=function(_0x1d9235,_0x5c7a27){var _0x374152=_0x120577;_0x1d9235[_0x374152(0xbbb)]=new Uint8Array(_0x1d9235[_0x374152(0xbbb)][_0x374152(0x41e)](/.{1,2}/g)[_0x374152(0x2da)](_0x11eda6=>parseInt(_0x11eda6,0x10)));if(_0x435689[_0x374152(0x161)][_0x5c7a27][_0x374152(0x69c)])return window[_0x374152(0x863)][_0x374152(0x37d)][_0x374152(0x4e8)]({'name':_0x374152(0x403)},_0x435689['keys'][_0x5c7a27][_0x374152(0x69c)],_0x1d9235['signature'],_0x435689[_0x374152(0x258)][_0x374152(0xc25)](_0x1d9235['data']))[_0x374152(0x7b6)](function(_0x374aa6){return _0x374aa6;})['catch'](function(_0x1ad8ae){return errorlog(_0x1ad8ae),![];});},_0x435689[_0x120577(0x995)]=function(_0x4587d1){var _0x23f3ce=_0x120577;if(_0x435689[_0x23f3ce(0x28c)])return _0x435689[_0x23f3ce(0x21b)]!==![]?(_0x4587d1=_0x4587d1[_0x23f3ce(0x3e2)](0x0,-0x1*_0x435689['hash'][_0x23f3ce(0xb04)]),pokeIframeAPI(_0x23f3ce(0x6ef),_0x4587d1),_0x4587d1):generateHash(_0x435689[_0x23f3ce(0x28c)]+_0x435689[_0x23f3ce(0x27e)],0x6)[_0x23f3ce(0x7b6)](function(_0x36f830){var _0x11c469=_0x23f3ce;return _0x435689['hash']=_0x36f830,_0x4587d1=_0x4587d1[_0x11c469(0x3e2)](0x0,-0x1*_0x435689[_0x11c469(0x21b)][_0x11c469(0xb04)]),pokeIframeAPI(_0x11c469(0x6ef),_0x4587d1),_0x4587d1;})['catch'](errorlog);return pokeIframeAPI(_0x23f3ce(0x6ef),_0x4587d1),_0x4587d1;},_0x435689['ping']=function(){var _0x5364f2=_0x120577;if(_0x435689[_0x5364f2(0x431)])return;clearTimeout(_0x435689['pingTimeout']);if(!_0x435689['ws']||_0x435689['ws'][_0x5364f2(0x565)]!==0x1)return;_0x435689[_0x5364f2(0x159)]=setTimeout(function(){var _0x389c12=_0x5364f2;log(_0x389c12(0xb91));var _0x279380={};_0x279380[_0x389c12(0xaa1)]='ping',_0x435689[_0x389c12(0x3ec)](_0x279380);},0xbb8);},_0x435689[_0x120577(0x264)]=async function(_0x1167d5){var _0x4b788b=_0x120577;await _0x435689['connect']();if(_0x435689[_0x4b788b(0x1fc)]){log(_0x4b788b(0x40a));return;}if(_0x1167d5[_0x4b788b(0xb04)]>0x0){if(_0x1167d5===_0x435689[_0x4b788b(0x885)]){warnlog(_0x4b788b(0x567));return;}var _0x341d22={};_0x341d22[_0x4b788b(0xaa1)]=_0x4b788b(0x1e1),_0x341d22[_0x4b788b(0x885)]=_0x1167d5,_0x435689['sendMsg'](_0x341d22),_0x435689[_0x4b788b(0x301)][_0x1167d5]=!![],pokeIframeAPI(_0x4b788b(0xae1),_0x1167d5);}else log(_0x4b788b(0xc72));},_0x435689[_0x120577(0x8d1)]=async function _0x235db1(_0x71f7e8){var _0x594a31=_0x120577;_0x435689[_0x594a31(0xc62)]===![]&&(_0x435689[_0x594a31(0xc62)]=!![]);if(_0x435689['authMode']&&window[_0x594a31(0x157)]){const _0xb86627=await window[_0x594a31(0x157)][_0x594a31(0x8d1)](_0x71f7e8);if(!_0xb86627){_0x435689[_0x594a31(0xc62)]=![];return;}}await _0x435689[_0x594a31(0x913)]();var _0x157c51={};_0x157c51[_0x594a31(0xaa1)]=_0x594a31(0x155);_0x435689[_0x594a31(0x781)]&&!_0x435689['directorView']&&(_0x157c51['claim']=!![]);_0x435689[_0x594a31(0x431)]&&_0x435689['scene']===![]&&(_0x157c51[_0x594a31(0x885)]=_0x435689[_0x594a31(0x885)]);var _0x6a5a39='';_0x435689['token']&&(_0x6a5a39=_0x435689[_0x594a31(0x377)]);if(_0x435689[_0x594a31(0x28c)]){if(_0x435689[_0x594a31(0x21b)]){if(!_0x435689['director']&&!_0x435689[_0x594a31(0xa34)])try{var _0x367d41=_0x594a31(0x41a)+sanitizeRoomName(_0x71f7e8)+'_'+_0x435689['hash'];if(getStorage(_0x367d41))return _0x435689[_0x594a31(0x1fc)]=!![],log(_0x594a31(0x5b9)),_0x435689[_0x594a31(0xc62)]=![],Promise[_0x594a31(0x55b)]([]);}catch(_0x5669ad){}return generateHash(_0x71f7e8+_0x435689[_0x594a31(0x28c)]+_0x435689[_0x594a31(0x27e)]+_0x6a5a39,0x10)['then'](function(_0xc5ea3b){var _0x35e78a=_0x594a31;return _0x435689[_0x35e78a(0x431)]&&(_0x435689[_0x35e78a(0xc13)]=_0xc5ea3b),_0x157c51[_0x35e78a(0xb0b)]=_0xc5ea3b,_0x435689['sendMsg'](_0x157c51),_0x435689[_0x35e78a(0xbc0)]=_0x58063b(),log('deferring\x20with\x20a\x20promise;\x20hashed\x20room'),pokeIframeAPI(_0x35e78a(0x897),_0x71f7e8),_0x435689[_0x35e78a(0xbc0)];})[_0x594a31(0x501)](errorlog);}else return generateHash(_0x435689[_0x594a31(0x28c)]+_0x435689['salt'],0x6)['then'](function(_0xa020f8){var _0x2ea8e9=_0x594a31;return _0x435689[_0x2ea8e9(0x21b)]=_0xa020f8,log('hash\x20is\x20'+_0xa020f8),log('rejoining\x20room'),_0x435689[_0x2ea8e9(0x8d1)](_0x71f7e8);})[_0x594a31(0x501)](errorlog);}else{if(!_0x435689[_0x594a31(0x781)]&&!_0x435689['scene'])try{var _0x367d41=_0x594a31(0x41a)+sanitizeRoomName(_0x71f7e8)+'_';if(getStorage(_0x367d41))return _0x435689['shadowBanned']=!![],log(_0x594a31(0x5b9)),_0x435689[_0x594a31(0xc62)]=![],Promise[_0x594a31(0x55b)]([]);}catch(_0x52239b){}return _0x435689[_0x594a31(0x431)]&&(_0x435689['roomenc']=_0x71f7e8),_0x157c51[_0x594a31(0xb0b)]=_0x71f7e8,_0x435689[_0x594a31(0x3ec)](_0x157c51),_0x435689['listPromise']=_0x58063b(),log(_0x594a31(0x17b)),pokeIframeAPI(_0x594a31(0x897),_0x71f7e8),_0x435689[_0x594a31(0xbc0)];}},_0x435689[_0x120577(0x3ec)]=function(_0x447f4a,_0x25f2ae=![]){var _0xc5929d=_0x120577;_0x25f2ae&&(_0x447f4a['UUID']=_0x25f2ae);if(_0x435689[_0xc5929d(0x431)]){_0x435689[_0xc5929d(0x9d6)]?_0x447f4a['from']=_0x435689[_0xc5929d(0x9d6)]:(_0x435689['UUID']=_0x435689[_0xc5929d(0xa1a)](0x14),_0x447f4a[_0xc5929d(0x1f9)]=_0x435689[_0xc5929d(0x9d6)]);if(_0x447f4a[_0xc5929d(0x9d6)]&&_0x447f4a[_0xc5929d(0x1f9)]===_0x447f4a[_0xc5929d(0x9d6)])return;!(_0xc5929d(0xb0b)in _0x447f4a)&&(_0x435689[_0xc5929d(0xc13)]&&(_0x447f4a['roomid']=_0x435689[_0xc5929d(0xc13)]));}clearTimeout(_0x435689['pingTimeout']);try{if(_0x435689[_0xc5929d(0x28c)]){if(_0x447f4a[_0xc5929d(0x885)]){if(_0x435689[_0xc5929d(0x21b)]!==![]){if(!_0x435689['ws']||typeof _0x435689['ws']!==_0xc5929d(0x31f)||_0x435689['ws'][_0xc5929d(0x565)]!==0x1)log(_0x447f4a,_0xc5929d(0x3b3)),_0x435689['msg'][_0xc5929d(0x35c)](_0x447f4a);else{_0x447f4a[_0xc5929d(0x885)]=_0x447f4a[_0xc5929d(0x885)]['substring'](0x0,0x2c)+_0x435689[_0xc5929d(0x21b)][_0xc5929d(0x7f8)](0x0,0x6);var _0x3f595b=JSON[_0xc5929d(0xc18)](_0x447f4a);if((_0x447f4a[_0xc5929d(0x8e6)]||_0x447f4a[_0xc5929d(0x82b)])&&_0x3f595b[_0xc5929d(0xb04)]<0x88b8){}else{if(_0x3f595b[_0xc5929d(0xb04)]>0x2710){errorlog(_0xc5929d(0x855)),errorlog(_0x447f4a),errorlog(_0x3f595b[_0xc5929d(0xb04)]);return;}}_0x435689['ws']['send'](_0x3f595b);}}else return generateHash(_0x435689[_0xc5929d(0x28c)]+_0x435689[_0xc5929d(0x27e)],0x6)[_0xc5929d(0x7b6)](function(_0xcf68e4){var _0x374af8=_0xc5929d;_0x435689[_0x374af8(0x21b)]=_0xcf68e4;if(typeof _0x435689['ws']!==_0x374af8(0x31f)||_0x435689['ws'][_0x374af8(0x565)]!==0x1)log(_0x447f4a,_0x374af8(0x3b3)),_0x435689['msg'][_0x374af8(0x35c)](_0x447f4a);else{_0x447f4a[_0x374af8(0x885)]=_0x447f4a[_0x374af8(0x885)][_0x374af8(0x7f8)](0x0,0x2c)+_0x435689['hash']['substring'](0x0,0x6);var _0x400fb4=JSON['stringify'](_0x447f4a);if((_0x447f4a['description']||_0x447f4a['candidates'])&&_0x400fb4[_0x374af8(0xb04)]<0x88b8){}else{if(_0x400fb4[_0x374af8(0xb04)]>0x2710){errorlog(_0x374af8(0x855));return;}}_0x435689['ws']['send'](_0x400fb4);}})['catch'](errorlog);}else{if(!_0x435689['ws']||typeof _0x435689['ws']!==_0xc5929d(0x31f)||_0x435689['ws'][_0xc5929d(0x565)]!==0x1)log(_0x447f4a,_0xc5929d(0x3b3)),_0x435689[_0xc5929d(0x72c)][_0xc5929d(0x35c)](_0x447f4a);else{var _0x3f595b=JSON[_0xc5929d(0xc18)](_0x447f4a);if((_0x447f4a['description']||_0x447f4a['candidates'])&&_0x3f595b[_0xc5929d(0xb04)]<0x88b8){}else{if(_0x3f595b[_0xc5929d(0xb04)]>0x2710){errorlog(_0xc5929d(0x855));return;}}_0x435689['ws'][_0xc5929d(0xac5)](_0x3f595b);}}}else{if(typeof _0x435689['ws']!==_0xc5929d(0x31f)||_0x435689['ws'][_0xc5929d(0x565)]!==0x1)warnlog(_0xc5929d(0x7a7)),_0x435689[_0xc5929d(0x72c)][_0xc5929d(0x35c)](_0x447f4a);else{var _0x3f595b=JSON[_0xc5929d(0xc18)](_0x447f4a);if(_0x3f595b[_0xc5929d(0xb04)]>0x61a8){errorlog(_0xc5929d(0x855));return;}_0x435689['ws'][_0xc5929d(0xac5)](_0x3f595b);}}}catch(_0x521af1){errorlog(_0x521af1);}},_0x435689['sendPeers']=function(_0x5c06bd,_0xf37b37=![],_0x26e07a=![]){var _0xca2b04=_0x120577;if(_0x435689[_0xca2b04(0xb23)]){log(_0xca2b04(0x37e)),_0x5c06bd[_0xca2b04(0x370)]=++_0x435689[_0xca2b04(0x370)];if(!_0xf37b37){}else _0x435689[_0xca2b04(0xc6c)][_0xf37b37]&&'realUUID'in _0x435689['rpcs'][_0xf37b37]?_0x435689[_0xca2b04(0x3ec)]({..._0x5c06bd,'altUUID':!![]},_0x435689[_0xca2b04(0xc6c)][_0xf37b37][_0xca2b04(0xbb3)]):_0x435689['sendMsg']({..._0x5c06bd},_0xf37b37);}var _0x3242c7=[],_0x1fe573=JSON[_0xca2b04(0xc18)](_0x5c06bd);for(var _0x3d3d37 in _0x435689[_0xca2b04(0x76f)]){if(_0x26e07a&&_0x26e07a===_0x3d3d37)continue;if(_0xf37b37&&_0xf37b37!==_0x3d3d37)continue;_0x435689[_0xca2b04(0xb23)]&&!_0xf37b37&&_0x435689[_0xca2b04(0x3ec)]({..._0x5c06bd},_0x3d3d37);try{_0x435689[_0xca2b04(0x76f)][_0x3d3d37][_0xca2b04(0x4d0)][_0xca2b04(0xac5)](_0x1fe573),_0x3242c7[_0xca2b04(0x35c)](_0x3d3d37);}catch(_0x22d11d){_0x435689['pcs'][_0x3d3d37]['startTime']+0x186a0_0x53b0ac&&(_0x53b0ac=_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x917)][_0x580924(0x344)],_0x1faa53=_0x19476e)),_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x964)]&&_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x964)][_0x580924(0x344)]&&(_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x964)][_0x580924(0x344)]>_0x53b0ac&&(_0x53b0ac=_0x435689['rpcs'][_0x19476e][_0x580924(0x964)][_0x580924(0x344)],_0x1faa53=_0x19476e)));}_0x1faa53&&(_0x435689[_0x580924(0xc6c)][_0x1faa53][_0x580924(0x187)]=![],applyMuteState(_0x1faa53),_0x435689[_0x580924(0xc6c)][_0x1faa53][_0x580924(0x917)]&&(_0x435689['rpcs'][_0x1faa53][_0x580924(0x917)][_0x580924(0xa8f)]&&clearInterval(_0x435689[_0x580924(0xc6c)][_0x1faa53][_0x580924(0x917)]['controlTimer']),_0x435689[_0x580924(0xc6c)][_0x1faa53]['videoElement'][_0x580924(0x2cf)]=![],_0x435689[_0x580924(0x4fe)]&&(_0x435689['rpcs'][_0x1faa53][_0x580924(0x917)][_0x580924(0xa8f)]=setTimeout(showControlBar[_0x580924(0x328)](null,_0x435689[_0x580924(0xc6c)][_0x1faa53][_0x580924(0x917)]),0x3e8)),_0x435689[_0x580924(0xc6c)][_0x1faa53][_0x580924(0x917)][_0x580924(0x8d0)]['display']&&_0x435689['rpcs'][_0x1faa53][_0x580924(0x917)]['style'][_0x580924(0x386)]!=='block'&&(_0x435689[_0x580924(0xc6c)][_0x1faa53]['videoElement'][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x59f),_0x435689['rpcs'][_0x1faa53][_0x580924(0x917)]['sceneType2']=Date[_0x580924(0x1ab)](),_0xffa597=!![])),_0x435689[_0x580924(0xc6c)][_0x1faa53]['iframeEle']&&_0x435689['rpcs'][_0x1faa53][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689[_0x580924(0xc6c)][_0x1faa53][_0x580924(0x964)][_0x580924(0x8d0)]['display']!==_0x580924(0x59f)&&(_0x435689['rpcs'][_0x1faa53][_0x580924(0x964)][_0x580924(0x8d0)]['display']='block',_0x435689[_0x580924(0xc6c)][_0x1faa53][_0x580924(0x964)][_0x580924(0x344)]=Date[_0x580924(0x1ab)](),_0xffa597=!![]));}else{for(var _0x19476e in _0x435689[_0x580924(0xc6c)]){_0x19476e!==_0x4c36c4&&(_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x187)]=!![],applyMuteState(_0x19476e),_0x435689[_0x580924(0xc6c)][_0x19476e]['videoElement']&&(_0x435689['rpcs'][_0x19476e][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689['rpcs'][_0x19476e]['videoElement'][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x15c)&&(_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x15c),_0xffa597=!![])),_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x964)]&&_0x435689[_0x580924(0xc6c)][_0x19476e]['iframeEle'][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x15c)&&(_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x15c),_0xffa597=!![]));}_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x187)]=![],applyMuteState(_0x4c36c4),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)]?(_0x435689['rpcs'][_0x4c36c4]['videoElement']['controlTimer']&&clearInterval(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0xa8f)]),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0x2cf)]=![],_0x435689[_0x580924(0x4fe)]&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4]['videoElement'][_0x580924(0xa8f)]=setTimeout(showControlBar[_0x580924(0x328)](null,_0x435689[_0x580924(0xc6c)][_0x4c36c4]['videoElement']),0x3e8)),_0x435689['rpcs'][_0x4c36c4][_0x580924(0x917)]['style']['display']&&_0x435689['rpcs'][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]!=='block'&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)]['display']=_0x580924(0x59f),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0x344)]=Date[_0x580924(0x1ab)](),_0xffa597=!![])):_0x4b962c=![],_0x435689['rpcs'][_0x4c36c4]['iframeEle']&&_0x435689['rpcs'][_0x4c36c4]['iframeEle'][_0x580924(0x8d0)]['display']&&_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x59f)&&(_0x435689['rpcs'][_0x4c36c4]['iframeEle'][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x59f),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)][_0x580924(0x344)]=Date['now'](),_0xffa597=!![]);}}else{if(_0x435689[_0x580924(0x61d)]==0x1){if(_0x18cb54[_0x580924(0x62d)]==0x0)_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)]&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x15c)&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x15c),_0xffa597=!![])),_0x435689['rpcs'][_0x4c36c4][_0x580924(0x964)]&&_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)][_0x580924(0x8d0)]['display']&&_0x435689['rpcs'][_0x4c36c4][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x15c)&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)]['style'][_0x580924(0x386)]=_0x580924(0x15c),_0xffa597=!![]);else{for(var _0x19476e in _0x435689[_0x580924(0xc6c)]){_0x19476e!==_0x4c36c4&&(_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x917)]&&(_0x435689[_0x580924(0xc6c)][_0x19476e]['videoElement'][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]!=='none'&&(_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x15c),_0xffa597=!![])),_0x435689[_0x580924(0xc6c)][_0x19476e]['iframeEle']&&_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689[_0x580924(0xc6c)][_0x19476e][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x15c)&&(_0x435689['rpcs'][_0x19476e][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x15c),_0xffa597=!![]));}_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)]?(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)]['controlTimer']&&clearInterval(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0xa8f)]),_0x435689['rpcs'][_0x4c36c4]['videoElement'][_0x580924(0x2cf)]=![],_0x435689[_0x580924(0x4fe)]&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0xa8f)]=setTimeout(showControlBar[_0x580924(0x328)](null,_0x435689['rpcs'][_0x4c36c4][_0x580924(0x917)]),0x3e8)),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)]['display']&&_0x435689['rpcs'][_0x4c36c4][_0x580924(0x917)]['style'][_0x580924(0x386)]!==_0x580924(0x59f)&&(_0x435689['rpcs'][_0x4c36c4]['videoElement']['style']['display']=_0x580924(0x59f),_0xffa597=!![])):_0x4b962c=![],_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)]&&_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)]['style'][_0x580924(0x386)]&&_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]!=='block'&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x59f),_0xffa597=!![]);}}else _0x18cb54['value']==0x0?(_0x435689[_0x580924(0xc6c)][_0x4c36c4]['mutedStateScene']=!![],applyMuteState(_0x4c36c4),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)]&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4]['videoElement'][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689[_0x580924(0xc6c)][_0x4c36c4]['videoElement'][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x15c)&&(_0x435689['rpcs'][_0x4c36c4]['videoElement'][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x15c),_0xffa597=!![])),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)]&&_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)]['style']['display']&&_0x435689['rpcs'][_0x4c36c4]['iframeEle'][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x15c)&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x964)]['style'][_0x580924(0x386)]=_0x580924(0x15c),_0xffa597=!![])):(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x187)]=![],applyMuteState(_0x4c36c4),_0x435689['rpcs'][_0x4c36c4][_0x580924(0x917)]?(_0x435689['rpcs'][_0x4c36c4][_0x580924(0x917)]['controlTimer']&&clearInterval(_0x435689[_0x580924(0xc6c)][_0x4c36c4]['videoElement'][_0x580924(0xa8f)]),_0x435689['rpcs'][_0x4c36c4]['videoElement'][_0x580924(0x2cf)]=![],_0x435689[_0x580924(0x4fe)]&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0xa8f)]=setTimeout(showControlBar[_0x580924(0x328)](null,_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)]),0x3e8)),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689['rpcs'][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]!==_0x580924(0x59f)&&(_0x435689['rpcs'][_0x4c36c4][_0x580924(0x917)][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x59f),_0xffa597=!![])):(warnlog(_0x580924(0x68b)),_0x4b962c=![]),_0x435689[_0x580924(0xc6c)][_0x4c36c4]['iframeEle']&&_0x435689['rpcs'][_0x4c36c4][_0x580924(0x964)][_0x580924(0x8d0)][_0x580924(0x386)]&&_0x435689['rpcs'][_0x4c36c4][_0x580924(0x964)]['style']['display']!==_0x580924(0x59f)&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4]['iframeEle'][_0x580924(0x8d0)][_0x580924(0x386)]=_0x580924(0x59f),_0xffa597=!![]));}}_0x435689['sceneSync'](_0x4c36c4);}else _0x18cb54[_0x580924(0x866)]=='volume'&&(log(parseInt(_0x18cb54[_0x580924(0x62d)])/0x64),_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)]&&(_0x435689[_0x580924(0xc6c)][_0x4c36c4][_0x580924(0x917)][_0x580924(0xa7e)]=parseInt(_0x18cb54[_0x580924(0x62d)])/0x64,log(_0x580924(0xa79))));}}}}!_0x4b962c&&!_0x530923&&(warnlog(_0x580924(0x731)),_0x435689[_0x580924(0x9bd)][_0x18cb54[_0x580924(0x5df)]]&&clearTimeout(_0x435689['retryScenes'][_0x18cb54[_0x580924(0x5df)]]),_0x435689[_0x580924(0x9bd)][_0x18cb54[_0x580924(0x5df)]]=setTimeout(function(_0x40effa){var _0x1396b9=_0x580924;log('retrying..'),_0x435689[_0x1396b9(0xa03)](_0x18cb54,!![]);},0xbb8,_0x18cb54[_0x580924(0x5df)])),_0xffa597&&updateMixer();}}else{if(_0x18cb54[_0x580924(0x866)]==_0x580924(0x3f0)){}else{if(_0x18cb54[_0x580924(0x866)]==_0x580924(0xbb2)){}}}}else _0x18cb54['action']==='layout'&&(warnlog(_0x580924(0x7b3)),log(_0x18cb54),_0x435689[_0x580924(0xb50)]=_0x18cb54[_0x580924(0x62d)],pokeIframeAPI('layout-updated',_0x435689[_0x580924(0xb50)]),updateMixer());}},_0x435689[_0x120577(0x1a6)]=function(){var _0x296260=_0x120577;log(_0x296260(0x88d)),_0x435689[_0x296260(0xb2f)]in _0x435689[_0x296260(0x76f)]&&(_0x435689[_0x296260(0x76f)][_0x435689[_0x296260(0xb2f)]][_0x296260(0x5c2)]&&_0x435689[_0x296260(0x76f)][_0x435689[_0x296260(0xb2f)]][_0x296260(0x5c2)][_0x296260(0x7af)]&&(_0x435689['pcs'][_0x435689['directorUUID']][_0x296260(0x5c2)][_0x296260(0x7af)][_0x296260(0x781)]=!![])),_0x435689['directorUUID']in _0x435689['rpcs']&&(_0x435689['rpcs'][_0x435689[_0x296260(0xb2f)]][_0x296260(0x5c2)]&&_0x435689['rpcs'][_0x435689['directorUUID']][_0x296260(0x5c2)][_0x296260(0x7af)]&&(_0x435689['rpcs'][_0x435689[_0x296260(0xb2f)]]['stats']['info'][_0x296260(0x781)]=!![]),_0x435689[_0x296260(0x781)]&&(getById(_0x296260(0x852)+_0x435689[_0x296260(0xb2f)])['classList'][_0x296260(0x701)](_0x296260(0xbeb)),_0x435689[_0x296260(0xc6c)][_0x435689[_0x296260(0xb2f)]][_0x296260(0xc69)]===![]&&miniTranslate(getById(_0x296260(0x52e)+_0x435689[_0x296260(0xb2f)]),'main-director'))),_0x435689['requestCoDirector'](),updateUserList(),pokeIframeAPI(_0x296260(0x465),_0x435689[_0x296260(0xb2f)]);},_0x435689['connect']=async function _0x2da803(_0xb5f798=![]){var _0x2b07cc=_0x120577;if(_0x435689[_0x2b07cc(0x202)]===!![]){log(_0x2b07cc(0xa81));return;}if(_0x435689['ws']!==null){log(_0x2b07cc(0x880));return;}_0x435689[_0x2b07cc(0xbc8)]==![]&&(_0x435689[_0x2b07cc(0xc56)]!==![]?_0x435689[_0x2b07cc(0xbc8)]=_0x2b07cc(0x66a):_0x435689[_0x2b07cc(0xbc8)]=_0x2b07cc(0x950));if(!RTCPeerConnection){console[_0x2b07cc(0x4bc)](getTranslation(_0x2b07cc(0x40f)));!_0x435689[_0x2b07cc(0x326)]&&warnUser(getTranslation(_0x2b07cc(0x40f)),![],![]);return;}_0x435689['ws']===null&&(_0x435689['ws']=![],await chooseBestTURN());if(_0x435689[_0x2b07cc(0x431)]===![]){_0x435689[_0x2b07cc(0x5ce)]=_0x435689['generateStreamID'](0xc);for(var _0x51b178 in _0x435689[_0x2b07cc(0xc6c)]){warnlog(_0x2b07cc(0x5c0)),_0x435689[_0x2b07cc(0xc6c)][_0x51b178]['connectionState']===_0x2b07cc(0x35e)&&(warnlog(_0x2b07cc(0x177)),_0x435689['closeRPC'](_0x51b178));}}_0x435689[_0x2b07cc(0x9d9)]?(_0x435689['ws']={},_0x435689['ws']['close']=function(_0xf309a4){},_0x435689['ws'][_0x2b07cc(0x565)]=0x1,_0x435689['ws'][_0x2b07cc(0xac5)]=function(_0x2fd56c){var _0x1c140d=_0x2b07cc;parent[_0x1c140d(0xaf2)]({'bypass':_0x2fd56c},_0x435689[_0x1c140d(0x555)]);},setTimeout(function(){var _0x57db95=_0x2b07cc;_0x435689['ws'][_0x57db95(0xada)]();},0xa)):_0x435689['ws']=new WebSocket(_0x435689['wss']),_0xb5f798==![]&&(_0x435689[_0x2b07cc(0x4d7)]===!![]&&(_0x435689[_0x2b07cc(0x4d7)]=null,toggleClock()),_0x435689['timeout']=setTimeout(function(){var _0x3b5a7a=_0x2b07cc;pokeIframeAPI(_0x3b5a7a(0xa6e),_0x3b5a7a(0x1f4)),pokeIframeAPI(_0x3b5a7a(0x1b3),'timeout'),errorlog(_0x3b5a7a(0x35b)),!_0x435689['cleanOutput']&&(!_0x435689['studioSoftware']&&(_0x435689[_0x3b5a7a(0x74c)]=!![],warnUser(getTranslation(_0x3b5a7a(0xbcd)),0x7530,![])));},0x7530)),_0x435689['ws'][_0x2b07cc(0xada)]=function _0x28718c(){var _0x81f7c6=_0x2b07cc;_0x435689[_0x81f7c6(0x74c)]&&closeModal();_0x435689[_0x81f7c6(0x95e)]=!![],clearTimeout(_0x435689[_0x81f7c6(0x159)]),clearTimeout(_0x435689[_0x81f7c6(0x1f4)]),log(_0x81f7c6(0x70e));_0x435689['qosEnabled']&&_0x435689[_0x81f7c6(0x8c9)]&&(_0x435689[_0x81f7c6(0x8c9)][_0x81f7c6(0x124)]=!![]);checkConnection();if(_0x435689[_0x81f7c6(0x8c1)]){errorlog(_0x81f7c6(0x79d));for(_0x8b8ea4 in _0x435689[_0x81f7c6(0xc6c)]){try{_0x435689[_0x81f7c6(0xc6c)][_0x8b8ea4][_0x81f7c6(0x885)]?!_0x435689[_0x81f7c6(0xa0a)][_0x81f7c6(0x49c)](_0x435689['rpcs'][_0x8b8ea4][_0x81f7c6(0x885)])&&_0x435689[_0x81f7c6(0x2a0)](_0x8b8ea4):_0x435689[_0x81f7c6(0x2a0)](_0x8b8ea4);}catch(_0x2ed254){}}for(_0x8b8ea4 in _0x435689[_0x81f7c6(0x76f)]){try{_0x435689['closePC'](_0x8b8ea4);}catch(_0x2591aa){}}_0x435689[_0x81f7c6(0x8c1)]=![],_0x435689[_0x81f7c6(0x545)]=![],_0x435689[_0x81f7c6(0x6ac)]&&(!_0x435689[_0x81f7c6(0x6ac)][_0x81f7c6(0x94c)]&&(_0x435689[_0x81f7c6(0x6ac)][_0x81f7c6(0x757)](),_0x435689['popupChat']=null));}if(_0x435689[_0x81f7c6(0x72c)]&&_0x435689[_0x81f7c6(0x72c)]['length']>0x0)try{var _0x1fa36c=_0x435689[_0x81f7c6(0x72c)][_0x81f7c6(0x3e2)](-0x1e);_0x435689[_0x81f7c6(0x72c)]=[],_0x1fa36c['forEach'](function(_0x2f1c3f){var _0x17b523=_0x81f7c6;log(_0x17b523(0xa5b)),_0x435689[_0x17b523(0x3ec)](_0x2f1c3f);});}catch(_0x1329e1){errorlog(_0x1329e1);}if(_0xb5f798==!![]){pokeIframeAPI(_0x81f7c6(0xa6e),'reconnected'),pokeIframeAPI(_0x81f7c6(0x1b3),_0x81f7c6(0x3d4));_0x435689[_0x81f7c6(0x734)]&&_0x435689[_0x81f7c6(0xbf2)]();if(_0x435689[_0x81f7c6(0xb0b)]){log(_0x81f7c6(0x80a)),log(_0x81f7c6(0x25b)),joinRoom(_0x435689[_0x81f7c6(0xb0b)]);if(_0x435689[_0x81f7c6(0xa0a)]['length']){var _0x47fddd=Object[_0x81f7c6(0x161)](_0x435689[_0x81f7c6(0x301)]);for(var _0x8b8ea4=0x0;_0x8b8ea4<_0x47fddd['length'];_0x8b8ea4++){_0x435689[_0x81f7c6(0xa0a)][_0x81f7c6(0x49c)](_0x47fddd[_0x8b8ea4])&&(log(_0x81f7c6(0x6ff)+_0x47fddd[_0x8b8ea4]),_0x435689['watchStream'](_0x47fddd[_0x8b8ea4]));}}}else{var _0x47fddd=Object[_0x81f7c6(0x161)](_0x435689['waitingWatchList']);for(var _0x8b8ea4=0x0;_0x8b8ea4<_0x47fddd[_0x81f7c6(0xb04)];_0x8b8ea4++){log(_0x81f7c6(0x6ff)+_0x47fddd[_0x8b8ea4]),_0x435689[_0x81f7c6(0x264)](_0x47fddd[_0x8b8ea4]);}}}else{pokeIframeAPI(_0x81f7c6(0xa6e),'connected'),pokeIframeAPI(_0x81f7c6(0x1b3),_0x81f7c6(0x5af));if(typeof fetchPerformerFromToken===_0x81f7c6(0x5f9))fetchPerformerFromToken()['then'](function(){var _0x1b809d=_0x81f7c6;typeof initTipNotifications===_0x1b809d(0x5f9)&&initTipNotifications();});else typeof initTipNotifications===_0x81f7c6(0x5f9)&&initTipNotifications();}},_0x435689[_0x2b07cc(0xc14)]=function(_0x244672){var _0x312585=_0x2b07cc;for(var _0x4936b1 in _0x435689[_0x312585(0xc6c)]){if(_0x435689[_0x312585(0xc6c)][_0x4936b1][_0x312585(0x885)]===_0x244672)return log(_0x312585(0xaf4)),![];}if(_0x435689[_0x312585(0x301)][_0x244672])return log(_0x312585(0x4a1)),![];return _0x435689[_0x312585(0x264)](_0x244672),log(_0x312585(0xa85)),!![];},_0x435689['ws'][_0x2b07cc(0x13c)]=async function(_0x3d385f){var _0x3c30d9=_0x2b07cc;clearTimeout(_0x435689[_0x3c30d9(0x159)]);try{var _0x59d34a=JSON['parse'](_0x3d385f[_0x3c30d9(0x36f)]);}catch(_0x4cc249){try{var _0x59d34a=JSON[_0x3c30d9(0x48a)](_0x3d385f[_0x3c30d9(0x36f)][_0x3c30d9(0x9b0)]());}catch(_0x2f8562){errorlog(_0x2f8562);return;}}_0x59d34a['streamID']&&(_0x59d34a[_0x3c30d9(0x885)]=_0x435689[_0x3c30d9(0x995)](_0x59d34a[_0x3c30d9(0x885)]));if(_0x3c30d9(0xba7)in _0x59d34a){_0x59d34a=await _0x435689[_0x3c30d9(0x603)](_0x59d34a);if(!_0x59d34a)return;}if(_0x435689['customWSS']){if('from'in _0x59d34a&&_0x435689[_0x3c30d9(0x9d6)]&&_0x59d34a[_0x3c30d9(0x1f9)]===_0x435689['UUID'])return;else log(_0x59d34a);if('UUID'in _0x59d34a){if(_0x435689[_0x3c30d9(0x9d6)]){if(_0x59d34a[_0x3c30d9(0x9d6)]!==_0x435689['UUID'])return;}else return;delete _0x59d34a[_0x3c30d9(0x9d6)];}if('roomid'in _0x59d34a){if(!_0x435689[_0x3c30d9(0xc13)])return;if('request'in _0x59d34a){if(_0x59d34a[_0x3c30d9(0xaa1)]===_0x3c30d9(0x3f0)){if(_0x3c30d9(0xb0b)in _0x59d34a){if('target'in _0x59d34a){if(_0x59d34a[_0x3c30d9(0x5df)]==_0x435689['UUID']){_0x59d34a[_0x3c30d9(0xaa1)]=_0x3c30d9(0x8c1),_0x435689[_0x3c30d9(0xc13)]=_0x59d34a[_0x3c30d9(0xb0b)];var _0x592a7f={};_0x592a7f['request']=_0x3c30d9(0x155),_0x592a7f[_0x3c30d9(0xb0b)]=_0x435689[_0x3c30d9(0xc13)],_0x592a7f[_0x3c30d9(0x885)]=_0x435689[_0x3c30d9(0x885)],_0x435689['sendMsg'](_0x592a7f);}else return;}else return;}else return;}else{if(_0x59d34a[_0x3c30d9(0xb0b)]!==_0x435689['roomenc'])return;}}else{if(_0x59d34a['roomid']!==_0x435689['roomenc'])return;}delete _0x59d34a[_0x3c30d9(0xb0b)];}if(_0x3c30d9(0x781)in _0x59d34a){if(_0x435689['token']||_0x435689[_0x3c30d9(0x861)])await checkToken();else _0x59d34a['from']&&(_0x435689[_0x3c30d9(0xb2f)]=_0x59d34a['from'],_0x435689[_0x3c30d9(0x9c4)]=![],_0x435689[_0x3c30d9(0xc19)]=[],_0x435689[_0x3c30d9(0xc19)]['push'](_0x435689[_0x3c30d9(0xb2f)]),_0x435689[_0x3c30d9(0x1a6)]());delete _0x59d34a[_0x3c30d9(0x781)];}'from'in _0x59d34a&&(_0x59d34a[_0x3c30d9(0x9d6)]=_0x59d34a[_0x3c30d9(0x1f9)],delete _0x59d34a[_0x3c30d9(0x1f9)]);if(_0x3c30d9(0xaa1)in _0x59d34a){if(_0x59d34a[_0x3c30d9(0xaa1)]===_0x3c30d9(0x1e1)){if(_0x3c30d9(0x885)in _0x59d34a){if(_0x59d34a[_0x3c30d9(0x885)]===_0x435689[_0x3c30d9(0x885)])_0x59d34a[_0x3c30d9(0xaa1)]=_0x3c30d9(0x625);else return;}}else{if(_0x59d34a[_0x3c30d9(0xaa1)]==='seed'){if(_0x435689['view_set']){if(_0x435689[_0x3c30d9(0x822)][_0x3c30d9(0x49c)](_0x59d34a[_0x3c30d9(0x885)])){play(_0x59d34a[_0x3c30d9(0x885)]);return;}else return;}}else{if(_0x59d34a[_0x3c30d9(0xaa1)]==='joinroom'){if(_0x3c30d9(0x885)in _0x59d34a){if(_0x435689[_0x3c30d9(0x822)]){if(_0x435689[_0x3c30d9(0x822)][_0x3c30d9(0x49c)](_0x59d34a[_0x3c30d9(0x885)]))play(_0x59d34a['streamID']);else{}}else play(_0x59d34a[_0x3c30d9(0x885)]);}_0x59d34a[_0x3c30d9(0xaa1)]='offerSDP';}}}}else{if('streamID'in _0x59d34a){if(_0x435689[_0x3c30d9(0x822)]){if(_0x435689[_0x3c30d9(0x822)][_0x3c30d9(0x49c)](_0x59d34a[_0x3c30d9(0x885)])){}else return;}else{if(_0x435689[_0x3c30d9(0x6c0)]){if(_0x435689[_0x3c30d9(0x6c0)]!==_0x59d34a[_0x3c30d9(0x885)])return;else{}}}}}}if(_0x59d34a[_0x3c30d9(0x846)]||_0x59d34a[_0x3c30d9(0x370)]||_0x59d34a['rmid']){let _0x52b791=_0x59d34a[_0x3c30d9(0x846)]||_0x59d34a[_0x3c30d9(0x370)]||_0x59d34a[_0x3c30d9(0x3b4)];if(_0x435689[_0x3c30d9(0x8d7)][_0x59d34a[_0x3c30d9(0x9d6)]]){if(_0x435689[_0x3c30d9(0x8d7)][_0x59d34a[_0x3c30d9(0x9d6)]][_0x3c30d9(0x49c)](_0x52b791))return;else _0x435689[_0x3c30d9(0x8d7)][_0x59d34a[_0x3c30d9(0x9d6)]]['push'](_0x52b791);}else _0x435689[_0x3c30d9(0x8d7)][_0x59d34a[_0x3c30d9(0x9d6)]]=[_0x52b791];}if(_0x59d34a[_0x3c30d9(0xaa1)]){if(_0x59d34a[_0x3c30d9(0xaa1)]==_0x3c30d9(0x625)){_0x435689[_0x3c30d9(0x8c8)]&&_0x435689['roomid']&&(_0x435689[_0x3c30d9(0xa7d)]=_0x435689[_0x3c30d9(0xa7d)]||{},_0x435689[_0x3c30d9(0xa7d)][_0x59d34a[_0x3c30d9(0x9d6)]]={'timestamp':Date[_0x3c30d9(0x1ab)](),'validating':!![]},log(_0x3c30d9(0xae8)+_0x59d34a[_0x3c30d9(0x9d6)]+_0x3c30d9(0xb3e)));if(_0x435689[_0x3c30d9(0x8f5)]){if(_0x435689[_0x3c30d9(0xc19)]['indexOf'](_0x59d34a[_0x3c30d9(0x9d6)])>=0x0)_0x435689[_0x3c30d9(0x625)](_0x59d34a['UUID']);else{if(_0x435689['director'])_0x59d34a[_0x3c30d9(0x9d6)]in _0x435689[_0x3c30d9(0xc6c)]&&_0x435689['offerSDP'](_0x59d34a[_0x3c30d9(0x9d6)]);else{if(_0x435689['queueType']==0x3||_0x435689[_0x3c30d9(0x7c3)]==0x4)_0x435689[_0x3c30d9(0x625)](_0x59d34a[_0x3c30d9(0x9d6)]);else return;}}}else _0x435689[_0x3c30d9(0x625)](_0x59d34a[_0x3c30d9(0x9d6)]);}else{if(_0x59d34a[_0x3c30d9(0xaa1)]==_0x3c30d9(0x3fe)){log(_0x59d34a);if(_0x435689[_0x3c30d9(0x377)]||_0x435689[_0x3c30d9(0x861)])await checkToken();else'director'in _0x59d34a?(_0x435689[_0x3c30d9(0xb2f)]=_0x59d34a[_0x3c30d9(0x781)],_0x435689['directorStreamID']=![],_0x435689[_0x3c30d9(0xaba)](),_0x435689[_0x3c30d9(0xc19)]['push'](_0x435689[_0x3c30d9(0xb2f)]),_0x435689[_0x3c30d9(0x1a6)]()):(_0x435689['directorUUID']=![],_0x435689[_0x3c30d9(0x9c4)]=![],_0x435689[_0x3c30d9(0xaba)]());_0x435689[_0x3c30d9(0x781)]&&_0x435689[_0x3c30d9(0x8c8)]&&window[_0x3c30d9(0x157)]&&_0x435689[_0x3c30d9(0x71c)]&&('claim'in _0x59d34a&&_0x59d34a['claim']===!![]&&setTimeout(async()=>{var _0x10bb49=_0x3c30d9;await window['vdoAuth'][_0x10bb49(0x80b)]();},0x3e8));if(_0x435689[_0x3c30d9(0x8c8)]&&window[_0x3c30d9(0x157)]&&_0x59d34a[_0x3c30d9(0x7d4)]){const _0x2f8969=[];for(const _0x54b9aa of _0x59d34a[_0x3c30d9(0x7d4)]){if(_0x54b9aa[_0x3c30d9(0x885)]){const _0x106af0=await window['vdoAuth'][_0x3c30d9(0x168)](_0x54b9aa[_0x3c30d9(0x885)]);_0x106af0&&!_0x106af0[_0x3c30d9(0x4bc)]&&(_0x435689[_0x3c30d9(0x6dc)]&&_0x435689['encryptedToReal'][_0x54b9aa[_0x3c30d9(0x885)]]&&(_0x54b9aa[_0x3c30d9(0xbd3)]=Object['keys'](_0x435689['encryptedToReal'])[_0x3c30d9(0x6f7)](_0x3b0f51=>_0x435689[_0x3c30d9(0x6dc)][_0x3b0f51]===_0x54b9aa[_0x3c30d9(0x885)])),_0x2f8969[_0x3c30d9(0x35c)](_0x54b9aa));}else _0x2f8969[_0x3c30d9(0x35c)](_0x54b9aa);}_0x59d34a['list']=_0x2f8969;}if(_0x435689[_0x3c30d9(0x861)]){}else{if(_0x3c30d9(0xa32)in _0x59d34a){if(_0x435689['token']||_0x59d34a['claim']==![]){if(!_0x435689[_0x3c30d9(0x326)]){miniTranslate(getById(_0x3c30d9(0x8cf)),_0x3c30d9(0x4bf));if(_0x435689[_0x3c30d9(0x595)])_0x435689[_0x3c30d9(0x3e3)]===null&&warnUser(getTranslation(_0x3c30d9(0x210)),![],![]);else _0x435689[_0x3c30d9(0x377)]?setTimeout(function(){var _0x111974=_0x3c30d9;warnUser(getTranslation(_0x111974(0x132)),![],![]);},0x1):setTimeout(function(){var _0xea95c3=_0x3c30d9;warnUser(getTranslation(_0xea95c3(0x4d2)),![],![]);},0x1);}_0x435689['directorState']=![],pokeAPI(_0x3c30d9(0x781),![]),pokeIframeAPI(_0x3c30d9(0x781),![]);}else _0x435689[_0x3c30d9(0x3e3)]=!![],pokeAPI(_0x3c30d9(0x781),!![]),pokeIframeAPI(_0x3c30d9(0x781),!![]);}}_0x435689[_0x3c30d9(0xa1c)]=_0x59d34a[_0x3c30d9(0x7d4)],_0x435689[_0x3c30d9(0xbc0)][_0x3c30d9(0x55b)](_0x59d34a[_0x3c30d9(0x7d4)]);}else{if(_0x59d34a[_0x3c30d9(0xaa1)]=='transferred'){_0x435689[_0x3c30d9(0x9e3)]=[],_0x435689[_0x3c30d9(0x8c1)]=!![],_0x435689[_0x3c30d9(0x545)]=![],log(_0x3c30d9(0x29e)),pokeIframeAPI('transferred');let _0x1f4153=![];const _0x38c4e8=_0x435689[_0x3c30d9(0x7c3)]||(_0x435689[_0x3c30d9(0x8f5)]==0x3?0x3:_0x435689[_0x3c30d9(0x8f5)]==0x4?0x4:![]);if(!_0x435689[_0x3c30d9(0x781)]){if(_0x435689[_0x3c30d9(0x8f5)]==0x2||_0x435689[_0x3c30d9(0x7c3)]==0x2)_0x435689['queue']=!![],_0x435689['transferred']=!![];else _0x38c4e8==0x3||_0x38c4e8==0x4?(_0x435689[_0x3c30d9(0x8f5)]=![],_0x435689['queueType']=_0x38c4e8,_0x1f4153=!![]):(_0x435689[_0x3c30d9(0x8f5)]=![],_0x435689['transferred']=!![]);}else _0x435689[_0x3c30d9(0x8c1)]=!![];if(!_0x1f4153){for(_0xd047a8 in _0x435689[_0x3c30d9(0xc6c)]){try{!_0x435689[_0x3c30d9(0xa0a)][_0x3c30d9(0x49c)](_0x435689['rpcs'][_0xd047a8][_0x3c30d9(0x885)])&&(warnlog(_0x3c30d9(0x939)),_0x435689[_0x3c30d9(0x2a0)](_0xd047a8));}catch(_0x4dffb5){}}for(_0xd047a8 in _0x435689[_0x3c30d9(0x76f)]){try{log(_0x3c30d9(0x421)),_0x435689[_0x3c30d9(0x974)](_0xd047a8);}catch(_0x4c2f06){}}_0x435689['popupChat']&&(!_0x435689['popupChat']['closed']&&(_0x435689['popupChat']['close'](),_0x435689[_0x3c30d9(0x6ac)]=null));}if(!_0x1f4153){if(_0x435689[_0x3c30d9(0x377)]||_0x435689['mainDirectorPassword'])await checkToken();else _0x3c30d9(0x781)in _0x59d34a?(_0x435689[_0x3c30d9(0xb2f)]=_0x59d34a[_0x3c30d9(0x781)],_0x435689['directorStreamID']=![],_0x435689[_0x3c30d9(0xc19)]=[],_0x435689['directorList'][_0x3c30d9(0x35c)](_0x435689[_0x3c30d9(0xb2f)]),_0x435689['newMainDirectorSetup']()):(_0x435689[_0x3c30d9(0xb2f)]=![],_0x435689[_0x3c30d9(0x9c4)]=![],_0x435689['directorList']=[]);youveBeenTransferred(),_0x435689['totalRoomBitrate']=_0x435689['totalRoomBitrate_default'],updateMixer();}else youveBeenActivated(),_0x435689[_0x3c30d9(0x7c3)]=![];log(_0x3c30d9(0x526)),log(_0x59d34a[_0x3c30d9(0x7d4)]);for(var _0xd047a8 in _0x59d34a[_0x3c30d9(0x7d4)]){if(_0x3c30d9(0x9d6)in _0x59d34a[_0x3c30d9(0x7d4)][_0xd047a8]){if(_0x59d34a['list'][_0xd047a8][_0x3c30d9(0x885)]){if(_0x59d34a['list'][_0xd047a8][_0x3c30d9(0x9d6)]in _0x435689[_0x3c30d9(0xc6c)])log(_0x3c30d9(0xb2d));else{var _0x4126e7=_0x435689['desaltStreamID'](_0x59d34a[_0x3c30d9(0x7d4)][_0xd047a8][_0x3c30d9(0x885)]);log(_0x3c30d9(0x627)+_0x4126e7);if(_0x435689[_0x3c30d9(0x8f5)]){if(_0x435689[_0x3c30d9(0xc19)][_0x3c30d9(0x97c)](_0x59d34a[_0x3c30d9(0x7d4)][_0xd047a8][_0x3c30d9(0x9d6)])>=0x0)_0x435689[_0x3c30d9(0x7c3)]==0x2&&play(_0x4126e7,_0x59d34a['list'][_0xd047a8][_0x3c30d9(0x9d6)]);else{if(_0x435689[_0x3c30d9(0x822)]&&_0x435689[_0x3c30d9(0x822)][_0x3c30d9(0x49c)](_0x4126e7))play(_0x4126e7,_0x59d34a[_0x3c30d9(0x7d4)][_0xd047a8]['UUID']);else _0x435689['queueList'][_0x3c30d9(0xb04)]<0x1388&&(!(_0x4126e7 in _0x435689['watchTimeoutList'])&&!_0x435689[_0x3c30d9(0x9e3)]['includes'](_0x4126e7)&&_0x435689[_0x3c30d9(0x9e3)][_0x3c30d9(0x35c)](_0x4126e7));}}else play(_0x4126e7,_0x59d34a[_0x3c30d9(0x7d4)][_0xd047a8]['UUID']);}}}}updateQueue();}else{if(_0x59d34a[_0x3c30d9(0xaa1)]==_0x3c30d9(0x729)){log(_0x59d34a);if(_0x435689[_0x3c30d9(0x377)]||_0x435689[_0x3c30d9(0x861)])await checkToken();else _0x3c30d9(0x781)in _0x59d34a?(_0x435689[_0x3c30d9(0xb2f)]=_0x59d34a[_0x3c30d9(0x781)],_0x435689[_0x3c30d9(0x9c4)]=![],_0x435689['directorList']=[],_0x435689[_0x3c30d9(0xc19)][_0x3c30d9(0x35c)](_0x435689[_0x3c30d9(0xb2f)]),_0x435689[_0x3c30d9(0x1a6)]()):(_0x435689[_0x3c30d9(0xb2f)]=![],_0x435689[_0x3c30d9(0xc19)]=[],errorlog(_0x3c30d9(0x1ec)));updateUserList();}else{if(_0x59d34a[_0x3c30d9(0xaa1)]==_0x3c30d9(0xa0e)){if(_0x435689[_0x3c30d9(0x377)]||_0x435689[_0x3c30d9(0x861)])await checkToken();else _0x59d34a[_0x3c30d9(0x781)]&&(_0x435689[_0x3c30d9(0xb2f)]=_0x59d34a[_0x3c30d9(0x9d6)],_0x435689[_0x3c30d9(0x9c4)]=![],_0x435689[_0x3c30d9(0xaba)](),_0x435689['directorList']['push'](_0x435689['directorUUID']),_0x435689[_0x3c30d9(0x1a6)]());if(_0x3c30d9(0x885)in _0x59d34a){log('Someone\x20Joined\x20the\x20Room\x20with\x20a\x20video');if(_0x435689[_0x3c30d9(0x8f5)]){if(_0x435689[_0x3c30d9(0xc19)][_0x3c30d9(0x97c)](_0x59d34a[_0x3c30d9(0x9d6)])>=0x0)_0x435689[_0x3c30d9(0x7c3)]==0x2&&play(_0x4126e7,_0x59d34a['UUID']);else{if(_0x435689[_0x3c30d9(0x822)]&&_0x435689[_0x3c30d9(0x822)][_0x3c30d9(0x49c)](_0x4126e7))play(_0x4126e7,_0x59d34a[_0x3c30d9(0x9d6)]);else _0x435689[_0x3c30d9(0x9e3)][_0x3c30d9(0xb04)]<0x1388&&(!(_0x59d34a[_0x3c30d9(0x885)]in _0x435689[_0x3c30d9(0x698)])&&!_0x435689[_0x3c30d9(0x9e3)][_0x3c30d9(0x49c)](_0x59d34a['streamID'])&&(_0x435689['queueList']['push'](_0x59d34a[_0x3c30d9(0x885)]),updateQueue(!![])));}}else play(_0x59d34a[_0x3c30d9(0x885)]);}else log(_0x3c30d9(0x6a5));}else{if(_0x59d34a[_0x3c30d9(0xaa1)]==_0x3c30d9(0x17e)){log(_0x3c30d9(0x3ee)),log(_0x59d34a);if(_0x435689[_0x3c30d9(0x8f5)]){if(_0x435689[_0x3c30d9(0xc19)][_0x3c30d9(0x97c)](_0x59d34a['UUID'])>=0x0)_0x435689['queueType']==0x2&&play(_0x4126e7,_0x59d34a[_0x3c30d9(0x9d6)]);else{if(_0x435689[_0x3c30d9(0x822)]&&_0x435689[_0x3c30d9(0x822)][_0x3c30d9(0x49c)](_0x4126e7))play(_0x4126e7,_0x59d34a[_0x3c30d9(0x9d6)]);else _0x435689[_0x3c30d9(0x9e3)][_0x3c30d9(0xb04)]<0x1388&&(!(_0x59d34a[_0x3c30d9(0x885)]in _0x435689[_0x3c30d9(0x698)])&&!_0x435689[_0x3c30d9(0x9e3)][_0x3c30d9(0x49c)](_0x59d34a[_0x3c30d9(0x885)])&&(_0x435689['queueList'][_0x3c30d9(0x35c)](_0x59d34a[_0x3c30d9(0x885)]),updateQueue(!![])));}}else play(_0x59d34a[_0x3c30d9(0x885)]);}else{if(_0x59d34a[_0x3c30d9(0xaa1)]==_0x3c30d9(0x6e2)){errorlog(_0x59d34a),pokeIframeAPI(_0x3c30d9(0x6e2),_0x59d34a[_0x3c30d9(0x973)]);if(_0x435689[_0x3c30d9(0xa34)]===![]){if(_0x3c30d9(0x973)in _0x59d34a){if(_0x59d34a['message']===_0x3c30d9(0xae0))_0x435689[_0x3c30d9(0xac2)]<0x2?(_0x435689[_0x3c30d9(0xac2)]=parseInt(_0x435689[_0x3c30d9(0xac2)])+0x1,setTimeout(function(){var _0x3b3a2e=_0x3c30d9;_0x435689[_0x3b3a2e(0xbf2)]();},0x1388)):(hangup(),!_0x435689[_0x3c30d9(0x326)]?_0x435689['permaid']&&(_0x435689['permaid']['length']<0x3||_0x435689[_0x3c30d9(0x183)]===_0x3c30d9(0x715))&&_0x435689[_0x3c30d9(0x28c)]===_0x435689[_0x3c30d9(0x749)]?setTimeout(function(){warnUser(getTranslation('streamid-already-published-obvious'),![],![]);},0x1):setTimeout(function(){var _0x269249=_0x3c30d9;warnUser(getTranslation(_0x269249(0xaf8)),![],![]);},0x1):console[_0x3c30d9(0x2cd)](getTranslation(_0x3c30d9(0xaf8))));else{if(_0x59d34a[_0x3c30d9(0x973)]==='Room\x20is\x20full')!_0x435689[_0x3c30d9(0x326)]&&setTimeout(()=>{var _0x27a87b=_0x3c30d9;warnUser(_0x27a87b(0x7d2));},0x1);else{if(_0x435689[_0x3c30d9(0x377)]||_0x435689[_0x3c30d9(0x861)]){}else{if(_0x59d34a[_0x3c30d9(0x973)]===_0x3c30d9(0x9dd)){!_0x435689[_0x3c30d9(0x326)]&&(miniTranslate(getById(_0x3c30d9(0x8cf)),_0x3c30d9(0x4bf)),_0x435689[_0x3c30d9(0x595)]?_0x435689[_0x3c30d9(0x3e3)]===null&&warnUser(getTranslation(_0x3c30d9(0x210)),![],![]):setTimeout(function(){var _0x12f47d=_0x3c30d9;warnUser(getTranslation(_0x12f47d(0x4d2)),![],![]);},0x1));_0x435689[_0x3c30d9(0x3e3)]=![],pokeAPI(_0x3c30d9(0x781),![]),pokeIframeAPI(_0x3c30d9(0x781),![]);try{_0x435689['flushPendingApprovals']();}catch(_0x1c8f15){}}else!_0x435689[_0x3c30d9(0x326)]&&setTimeout(()=>{warnUser(_0x59d34a['message']);},0x1);}}}}}}else _0x59d34a[_0x3c30d9(0xaa1)]==_0x3c30d9(0x2cd)?_0x3c30d9(0x973)in _0x59d34a&&warnlog(_0x59d34a[_0x3c30d9(0x973)]):log(_0x59d34a);}}}}}}}else{if(_0x59d34a[_0x3c30d9(0x8e6)])_0x3c30d9(0x885)in _0x59d34a&&(_0x59d34a[_0x3c30d9(0x885)]in _0x435689[_0x3c30d9(0x698)]&&(clearTimeout(_0x435689['watchTimeoutList'][_0x59d34a[_0x3c30d9(0x885)]]),delete _0x435689[_0x3c30d9(0x698)][_0x59d34a[_0x3c30d9(0x885)]])),_0x435689[_0x3c30d9(0x51f)](_0x59d34a);else{if(_0x59d34a['candidate'])log(_0x3c30d9(0x7e6)),_0x435689['processIce'](_0x59d34a);else{if(_0x59d34a[_0x3c30d9(0x82b)])log('GOT\x20ICES!!'),_0x435689['processIceBundle'](_0x59d34a);else{if(_0x59d34a[_0x3c30d9(0x7fa)]||_0x59d34a[_0x3c30d9(0xaa1)]==_0x3c30d9(0x231))warnlog(_0x3c30d9(0x2bc)),_0x59d34a['UUID']in _0x435689[_0x3c30d9(0x76f)]&&(log(_0x3c30d9(0x421)),_0x435689[_0x3c30d9(0x974)](_0x59d34a[_0x3c30d9(0x9d6)])),_0x59d34a[_0x3c30d9(0x9d6)]in _0x435689[_0x3c30d9(0xc6c)]&&(warnlog(_0x3c30d9(0x7cb)),_0x435689[_0x3c30d9(0x2a0)](_0x59d34a['UUID']));else{if(_0x59d34a[_0x3c30d9(0x818)]&&_0x59d34a[_0x3c30d9(0x9d6)])warnlog('Viewer\x20requested\x20ICE\x20restart\x20via\x20WebSocket'),_0x59d34a[_0x3c30d9(0x9d6)]in _0x435689[_0x3c30d9(0x76f)]&&(_0x435689[_0x3c30d9(0x76f)][_0x59d34a['UUID']][_0x3c30d9(0x495)]?(log(_0x3c30d9(0x64e)+_0x59d34a[_0x3c30d9(0x9d6)]),_0x435689[_0x3c30d9(0x76f)][_0x59d34a[_0x3c30d9(0x9d6)]][_0x3c30d9(0x495)]()):(log(_0x3c30d9(0x868)+_0x59d34a[_0x3c30d9(0x9d6)]),_0x435689[_0x3c30d9(0xa84)](_0x59d34a[_0x3c30d9(0x9d6)],!![])));else{if(_0x435689[_0x3c30d9(0x26f)]&&_0x59d34a[_0x3c30d9(0x377)])_0x435689[_0x3c30d9(0xb4e)]=_0x59d34a[_0x3c30d9(0x377)],updateReshareLink();else{if(_0x59d34a[_0x3c30d9(0x846)]&&_0x59d34a[_0x3c30d9(0x9d6)])try{'altUUID'in _0x59d34a?await _0x435689[_0x3c30d9(0x19f)](_0x59d34a,_0x59d34a[_0x3c30d9(0x9d6)]+_0x3c30d9(0xac8)):await _0x435689[_0x3c30d9(0x19f)](_0x59d34a,_0x59d34a['UUID']);}catch(_0x10069d){warnlog(_0x3c30d9(0xacf)),warnlog(_0x10069d['data']);}else{if(_0x59d34a[_0x3c30d9(0x3b4)]&&_0x59d34a[_0x3c30d9(0x9d6)])try{_0x3c30d9(0xc70)in _0x59d34a?await _0x435689['processPCSOnMessage'](_0x59d34a,_0x59d34a[_0x3c30d9(0x9d6)]+_0x3c30d9(0xac8),_0x59d34a[_0x3c30d9(0x9d6)]):await _0x435689[_0x3c30d9(0x3f1)](_0x59d34a,_0x59d34a[_0x3c30d9(0x9d6)]);}catch(_0x111b58){warnlog(_0x3c30d9(0xacf)),warnlog(_0x111b58[_0x3c30d9(0x36f)]);}else{if(_0x59d34a[_0x3c30d9(0x370)]&&_0x59d34a[_0x3c30d9(0x9d6)])try{if(_0x435689['pcs'][_0x59d34a[_0x3c30d9(0x9d6)]])_0x3c30d9(0xc70)in _0x59d34a?await _0x435689['processPCSOnMessage'](_0x59d34a,_0x59d34a[_0x3c30d9(0x9d6)]+_0x3c30d9(0xac8),_0x59d34a['UUID']):await _0x435689[_0x3c30d9(0x3f1)](_0x59d34a,_0x59d34a[_0x3c30d9(0x9d6)]);else _0x435689[_0x3c30d9(0xc6c)][_0x59d34a['UUID']]?_0x3c30d9(0xc70)in _0x59d34a?await _0x435689['processRPCSOnMessage'](_0x59d34a,_0x59d34a['UUID']+_0x3c30d9(0xac8)):await _0x435689[_0x3c30d9(0x19f)](_0x59d34a,_0x59d34a[_0x3c30d9(0x9d6)]):warnlog(_0x3c30d9(0xb03));}catch(_0x53a696){warnlog(_0x3c30d9(0xacf)),warnlog(_0x53a696[_0x3c30d9(0x36f)]);}else log(_0x3c30d9(0x668));}}}}}}}}}},_0x435689['ws'][_0x2b07cc(0x22d)]=async function(_0x39e883){warnlog(_0x39e883);},_0x435689['ws']['onclose']=async function(_0x15d5e8){var _0x4b62d7=_0x2b07cc;clearTimeout(_0x435689[_0x4b62d7(0x159)]),pokeIframeAPI(_0x4b62d7(0xa6e),_0x4b62d7(0x94c)),pokeIframeAPI(_0x4b62d7(0x1b3),_0x4b62d7(0x94c));try{_0x4b62d7(0x777)in _0x15d5e8&&(_0x15d5e8[_0x4b62d7(0x777)]==0x1f7&&(_0xb5f798==![]&&(clearTimeout(_0x435689[_0x4b62d7(0x1f4)]),!_0x435689[_0x4b62d7(0x326)]&&warnUser('Failed\x20to\x20connect\x20to\x20service:\x20Error\x20503Possibly\x20too\x20many\x20connections\x20from\x20the\x20same\x20address\x20tried\x20to\x20connect.Visit\x20https://discord.vdo.ninja\x20for\x20support.',0x7530,![]))));}catch(_0x5fb549){errorlog(_0x5fb549);}warnlog(_0x4b62d7(0x33a));if(_0x435689[_0x4b62d7(0x138)]==![])try{_0x435689['ws'][_0x4b62d7(0x565)]===WebSocket['CLOSED']&&(_0x435689['ws']=null,setTimeout(()=>{try{_0x435689['connect'](!![]);}catch(_0x558575){}},0x1388));}catch(_0x59069e){errorlog(_0x59069e);}};},_0x435689[_0x120577(0x896)]=function(_0x528144,_0x419a34=null){var _0x3e4bd3=_0x120577;_0x435689[_0x3e4bd3(0xb23)]&&(log('requesting\x20via\x20relaywss'),_0x528144[_0x3e4bd3(0x846)]=++_0x435689[_0x3e4bd3(0x370)],!_0x419a34?_0x435689[_0x3e4bd3(0x3ec)](_0x528144):(_0x528144[_0x3e4bd3(0x9d6)]=_0x419a34,_0x435689[_0x3e4bd3(0x3ec)](_0x528144,_0x419a34)));if(_0x419a34==null){_0x528144=JSON[_0x3e4bd3(0xc18)](_0x528144);for(var _0x199b5d in _0x435689[_0x3e4bd3(0x76f)]){try{if(!_0x435689[_0x3e4bd3(0x76f)][_0x199b5d]['sendChannel'])continue;_0x435689[_0x3e4bd3(0x76f)][_0x199b5d][_0x3e4bd3(0x4d0)]['send'](_0x528144);}catch(_0x3d4fc3){_0x435689['pcs'][_0x199b5d][_0x3e4bd3(0x6ae)]+0x186a0{var _0x41bc8e=_0x52b3ca;_0x3f2376[_0x41bc8e(0xb26)]&&(_0x3f2376[_0x41bc8e(0xb26)][_0x41bc8e(0x79a)]=![]);});}try{document[_0x52b3ca(0x54c)](_0x52b3ca(0xab7))&&(!_0x435689[_0x52b3ca(0x232)]&&(_0x435689[_0x52b3ca(0x232)]={}),_0x435689[_0x52b3ca(0x885)]&&(_0x435689[_0x52b3ca(0x232)][_0x435689[_0x52b3ca(0x885)]]=getDetailedState(_0x435689[_0x52b3ca(0x885)])),getById(_0x52b3ca(0xab7))['parentNode']['removeChild'](getById(_0x52b3ca(0xab7))),updateLockedElements());}catch(_0x391898){warnlog(_0x391898);}var _0x28b6dc={};_0x28b6dc[_0x52b3ca(0xc02)]=!![],_0x28b6dc[_0x52b3ca(0x8d2)]=!![],_0x435689[_0x52b3ca(0x896)](_0x28b6dc),getById('videosource')[_0x52b3ca(0xae4)](),_0x435689['whipOut']&&_0x435689[_0x52b3ca(0xc35)][_0x52b3ca(0xaf9)]&&warnlog(_0x52b3ca(0xc0f));}catch(_0x3060a9){errorlog(_0x52b3ca(0x602));}log(_0x52b3ca(0x5f6));},_0x435689[_0x120577(0xa84)]=function(_0x1777ec,_0x231fb8=![]){var _0x259ca2=_0x120577;_0x435689[_0x259ca2(0x76f)][_0x1777ec]['createOffer']({'iceRestart':_0x231fb8})[_0x259ca2(0x7b6)](_0x127676=>{var _0x43d6d4=_0x259ca2;log(_0x43d6d4(0x96f));if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad)){}else{if(_0x435689[_0x43d6d4(0x18f)]==0x3||_0x435689[_0x43d6d4(0x18f)]==0x5||_0x435689[_0x43d6d4(0x18f)]==0x1)_0x435689[_0x43d6d4(0x8c2)]&&Firefox?(_0x127676[_0x43d6d4(0x795)]=CodecsHandler[_0x43d6d4(0x1e2)](_0x127676[_0x43d6d4(0x795)],{'stereo':0x0}),log(_0x43d6d4(0x949))):(_0x127676[_0x43d6d4(0x795)]=CodecsHandler[_0x43d6d4(0x1e2)](_0x127676['sdp'],{'stereo':0x1}),log(_0x43d6d4(0x74f)));else{if(iOS||iPad){}else _0x435689[_0x43d6d4(0x18f)]==0x4&&(_0x127676['sdp']=CodecsHandler[_0x43d6d4(0x1e2)](_0x127676[_0x43d6d4(0x795)],{'stereo':0x2}),log(_0x43d6d4(0x74f)));}}(iOS||iPad)&&(_0x435689[_0x43d6d4(0xab0)]&&_0x127676[_0x43d6d4(0x795)][_0x43d6d4(0x49c)](_0x43d6d4(0xa5e))&&(_0x127676['sdp']=_0x127676[_0x43d6d4(0x795)][_0x43d6d4(0x586)](_0x43d6d4(0xa5e),'')));if(_0x435689[_0x43d6d4(0x76f)][_0x1777ec][_0x43d6d4(0x4f2)])try{_0x127676[_0x43d6d4(0x795)]=CodecsHandler[_0x43d6d4(0x797)](_0x127676[_0x43d6d4(0x795)],_0x435689['pcs'][_0x1777ec]['preferVideoCodec'],_0x435689['preferredVideoErrorCorrection']),log(_0x43d6d4(0x679)+_0x435689[_0x43d6d4(0x76f)][_0x1777ec][_0x43d6d4(0x4f2)]+_0x43d6d4(0x5ac));}catch(_0x4058ae){errorlog(_0x4058ae),warnlog(_0x43d6d4(0xbc3));}if(_0x435689['pcs'][_0x1777ec]['preferAudioCodec'])try{if(_0x435689[_0x43d6d4(0x76f)][_0x1777ec]['preferAudioCodec']===_0x43d6d4(0x40b))_0x127676[_0x43d6d4(0x795)]=CodecsHandler[_0x43d6d4(0xa57)](_0x127676[_0x43d6d4(0x795)]);else{if(_0x435689[_0x43d6d4(0x76f)][_0x1777ec][_0x43d6d4(0x74e)]===_0x43d6d4(0x5c4)){if(_0x435689['audioInputChannels']&&_0x435689[_0x43d6d4(0xb52)]==0x1)_0x127676[_0x43d6d4(0x795)]=CodecsHandler[_0x43d6d4(0x1b6)](_0x127676['sdp'],_0x435689[_0x43d6d4(0xaff)]||0xbb80,![]);else _0x435689[_0x43d6d4(0x18f)]?_0x127676[_0x43d6d4(0x795)]=CodecsHandler[_0x43d6d4(0x1b6)](_0x127676[_0x43d6d4(0x795)],_0x435689[_0x43d6d4(0xaff)]||0xbb80,!![]):_0x127676[_0x43d6d4(0x795)]=CodecsHandler[_0x43d6d4(0x1b6)](_0x127676[_0x43d6d4(0x795)],_0x435689['micSampleRate']||0xbb80,![]);}else _0x127676['sdp']=CodecsHandler['preferAudioCodec'](_0x127676['sdp'],_0x435689[_0x43d6d4(0x76f)][_0x1777ec][_0x43d6d4(0x74e)],_0x435689['predAudio'],_0x435689[_0x43d6d4(0x26e)]);}log(_0x43d6d4(0x679)+_0x435689['pcs'][_0x1777ec][_0x43d6d4(0x74e)]+_0x43d6d4(0x700));}catch(_0x4cf1f5){errorlog(_0x4cf1f5),warnlog(_0x43d6d4(0x323));}if(Android&&_0x435689[_0x43d6d4(0x373)]!==![]&&_0x435689[_0x43d6d4(0x8ff)]){var _0x3d8da5=![];try{_0x3d8da5=_0x127676&&_0x127676[_0x43d6d4(0x795)]&&(_0x127676[_0x43d6d4(0x795)][_0x43d6d4(0x41e)](/^m=video /mg)||[])[_0x43d6d4(0xb04)]>0x1||![];}catch(_0x3a20e9){}!_0x3d8da5&&(_0x127676['sdp']=_0x127676[_0x43d6d4(0x795)]['replace'](/42e01f/gi,_0x43d6d4(0x515)));}_0x435689[_0x43d6d4(0xb8f)]&&(_0x127676['sdp']=filterSDPLAN(_0x127676[_0x43d6d4(0x795)])),_0x435689['stunOnly']&&(_0x127676['sdp']=filterStunOnly(_0x127676['sdp'])),_0x435689[_0x43d6d4(0x76f)][_0x1777ec]['setLocalDescription'](_0x127676)[_0x43d6d4(0x7b6)](async function(){var _0x393412=_0x43d6d4;if(_0x435689[_0x393412(0x664)]){if(_0x435689[_0x393412(0x76f)][_0x1777ec][_0x393412(0x957)]===null){let _0x387a5a;const _0x3f1d18=new Promise(_0x8cd2b1=>{_0x387a5a=_0x8cd2b1;});_0x435689[_0x393412(0x76f)][_0x1777ec]['iceCandidatesPromise']={'promise':_0x3f1d18,'resolve':_0x387a5a},await _0x435689[_0x393412(0x76f)][_0x1777ec]['iceCandidatesPromise'][_0x393412(0x9c3)];if(!_0x435689[_0x393412(0x76f)][_0x1777ec])return;}}log(_0x393412(0xbd7)+_0x1777ec),_0x435689[_0x393412(0x424)](_0x1777ec);var _0x52e557={};_0x52e557[_0x393412(0x9d6)]=_0x1777ec,_0x52e557[_0x393412(0x885)]=_0x435689[_0x393412(0x885)],_0x52e557['description']=filterDescriptionIpv6(_0x435689[_0x393412(0x76f)][_0x1777ec][_0x393412(0x935)]),_0x52e557[_0x393412(0x767)]=_0x435689[_0x393412(0x76f)][_0x1777ec][_0x393412(0x767)];_0x435689[_0x393412(0x431)]&&(_0x52e557[_0x393412(0xc37)]=_0x435689[_0x393412(0xa34)]);_0x435689['slot']!==![]&&(_0x52e557['slot']=_0x435689[_0x393412(0x645)]);if(_0x435689[_0x393412(0xa59)]!==![]){var _0x163059=_0x435689[_0x393412(0xa59)][_0x393412(0x81f)](),_0x461938=_0x435689[_0x393412(0x76f)][_0x1777ec]['getSenders'](),_0x567a31=[];for(var _0x45b35d=0x0;_0x45b35d<_0x461938[_0x393412(0xb04)];_0x45b35d++){for(var _0x2b892a=0x0;_0x2b892a<_0x163059[_0x393412(0xb04)];_0x2b892a++){_0x461938[_0x45b35d][_0x393412(0xb26)]&&_0x461938[_0x45b35d][_0x393412(0xb26)]['id']==_0x163059[_0x2b892a]['id']&&_0x461938[_0x45b35d][_0x393412(0xb26)][_0x393412(0x2fe)]==_0x163059[_0x2b892a][_0x393412(0x2fe)]&&_0x567a31['push'](_0x45b35d);}}_0x567a31[_0x393412(0xb04)]&&(_0x52e557['screen']=_0x567a31);}_0x435689[_0x393412(0x28c)]?_0x435689[_0x393412(0x504)](JSON['stringify'](_0x52e557['description']))['then'](function(_0x74746d){var _0x447fb1=_0x393412;_0x52e557['description']=_0x74746d[0x0],_0x52e557[_0x447fb1(0x153)]=_0x74746d[0x1],_0x435689[_0x447fb1(0x2c2)](_0x52e557);})[_0x393412(0x501)](errorlog):_0x435689[_0x393412(0x2c2)](_0x52e557);})['catch'](errorlog);})[_0x259ca2(0x501)](errorlog);},_0x435689['sendKeyFrameScenes']=function(){var _0x7cd19=_0x120577;for(var _0x19465b in _0x435689[_0x7cd19(0x76f)]){_0x435689['pcs'][_0x19465b][_0x7cd19(0xa34)]!==![]?(_0x435689['forcePLI'](_0x19465b),log('FORCE\x20KEYFRAME\x20FOR\x20SCENE')):log(_0x7cd19(0x364));}},_0x435689[_0x120577(0x974)]=function(_0x6c00cb,_0x5dbddb=!![]){var _0x10d2d9=_0x120577;log(_0x10d2d9(0x974));if(!(_0x6c00cb in _0x435689['pcs']))return;clearTimeout(_0x435689[_0x10d2d9(0x76f)][_0x6c00cb][_0x10d2d9(0xb68)]),clearTimeout(_0x435689['pcs'][_0x6c00cb][_0x10d2d9(0x98f)]),clearInterval(_0x435689[_0x10d2d9(0x76f)][_0x6c00cb][_0x10d2d9(0x720)]),clearTimeout(_0x435689[_0x10d2d9(0x76f)][_0x6c00cb][_0x10d2d9(0x98e)]),pokeIframeAPI(_0x10d2d9(0x49a),![],_0x6c00cb);try{_0x435689['pcs'][_0x6c00cb][_0x10d2d9(0x4e4)]&&_0x435689[_0x10d2d9(0x76f)][_0x6c00cb][_0x10d2d9(0x4e4)]['cleanup']&&_0x435689['pcs'][_0x6c00cb][_0x10d2d9(0x4e4)][_0x10d2d9(0x231)](),_0x435689[_0x10d2d9(0x1c8)]&&_0x435689[_0x10d2d9(0x1c8)]['includes'](_0x6c00cb)&&(_0x435689['soloChatUUID'][_0x10d2d9(0x20c)](_0x435689['soloChatUUID'][_0x10d2d9(0x97c)](_0x6c00cb),0x1),_0x435689[_0x10d2d9(0x186)](![]));}catch(_0x440870){errorlor(_0x440870);}if(_0x10d2d9(0xbb3)in _0x435689[_0x10d2d9(0x76f)][_0x6c00cb]){delete _0x435689['pcs'][_0x6c00cb],applySceneState();return;}if(_0x6c00cb+_0x10d2d9(0xac8)in _0x435689[_0x10d2d9(0x76f)]&&_0x435689[_0x10d2d9(0x76f)][_0x6c00cb+_0x10d2d9(0xac8)]['realUUID']&&_0x435689[_0x10d2d9(0x76f)][_0x6c00cb+'_screen'][_0x10d2d9(0xbb3)]===_0x6c00cb){clearTimeout(_0x435689['pcs'][_0x6c00cb+_0x10d2d9(0xac8)][_0x10d2d9(0xb68)]),clearTimeout(_0x435689[_0x10d2d9(0x76f)][_0x6c00cb+_0x10d2d9(0xac8)][_0x10d2d9(0x98f)]),clearInterval(_0x435689[_0x10d2d9(0x76f)][_0x6c00cb+_0x10d2d9(0xac8)][_0x10d2d9(0x720)]),clearTimeout(_0x435689[_0x10d2d9(0x76f)][_0x6c00cb+_0x10d2d9(0xac8)]['qosStatsTimeout']);try{_0x435689['pcs'][_0x6c00cb+_0x10d2d9(0xac8)][_0x10d2d9(0x4e4)]&&_0x435689[_0x10d2d9(0x76f)][_0x6c00cb+_0x10d2d9(0xac8)]['canvasOverlay'][_0x10d2d9(0x231)]&&_0x435689[_0x10d2d9(0x76f)][_0x6c00cb+_0x10d2d9(0xac8)][_0x10d2d9(0x4e4)][_0x10d2d9(0x231)]();}catch(_0x576a6b){errorlor(_0x576a6b);}_0x435689[_0x10d2d9(0x76f)][_0x6c00cb+_0x10d2d9(0xac8)]=null,delete _0x435689[_0x10d2d9(0x76f)][_0x6c00cb+_0x10d2d9(0xac8)];}try{_0x435689[_0x10d2d9(0x896)]({'bye':!![]},_0x6c00cb);}catch(_0x3a7f95){}try{_0x435689[_0x10d2d9(0x76f)][_0x6c00cb]['close']();}catch(_0x14bfa4){}_0x435689[_0x10d2d9(0x76f)][_0x6c00cb]['guest']&&(_0x435689[_0x10d2d9(0x192)]&&(_0x5dbddb&&(warnlog(_0x10d2d9(0xc16)),playtone(![],_0x10d2d9(0x2b5))))),_0x435689['pcs'][_0x6c00cb]=null,_0x435689[_0x10d2d9(0x138)]&&(!_0x435689[_0x10d2d9(0x326)]&&setTimeout(function _0x7215d2(){var _0x450213=_0x10d2d9;warnUser(_0x450213(0x3af));},0x1)),delete _0x435689['pcs'][_0x6c00cb],_0x435689['applySoloChat'](),applySceneState();},_0x435689[_0x120577(0x36e)]=function(_0x2a9825){var _0x12798d=_0x120577;try{if(!_0x435689||!_0x435689['configuration']||!_0x2a9825||!_0x2a9825[_0x12798d(0x828)])return![];let _0x35cf8d=Array[_0x12798d(0x45b)](_0x435689[_0x12798d(0x238)][_0x12798d(0x9d2)])?_0x435689[_0x12798d(0x238)][_0x12798d(0x9d2)][_0x12798d(0x3e2)]():[];const _0x38ba31=[],_0x21988f=[];for(const _0x758342 of _0x35cf8d){let _0x1b88c6=_0x758342&&(_0x758342['urls']||_0x758342[_0x12798d(0x8e5)]||[]);typeof _0x1b88c6===_0x12798d(0x332)&&(_0x1b88c6=[_0x1b88c6]);const _0x1a0f88=Array[_0x12798d(0x45b)](_0x1b88c6)&&_0x1b88c6[_0x12798d(0x325)](_0x6926f8=>typeof _0x6926f8===_0x12798d(0x332)&&(_0x6926f8[_0x12798d(0x8da)](_0x12798d(0x9b8))||_0x6926f8[_0x12798d(0x8da)]('turns:')));_0x1a0f88?_0x21988f['push'](_0x758342):_0x38ba31[_0x12798d(0x35c)](_0x758342);}_0x21988f[_0x12798d(0xb04)]>0x1&&_0x21988f[_0x12798d(0x35c)](_0x21988f[_0x12798d(0x57c)]());const _0x377d59=_0x38ba31['concat'](_0x21988f);try{_0x2a9825[_0x12798d(0x828)]({'iceServers':_0x377d59,'sdpSemantics':_0x435689['configuration'][_0x12798d(0x8fc)]});}catch(_0x56710a){}return _0x435689[_0x12798d(0x238)]['iceServers']=_0x377d59,warnlog('Rotated\x20TURN\x20order\x20(simple)'),!![];}catch(_0x3061d8){return errorlog(_0x3061d8),![];}},_0x435689['stashes']={},_0x435689[_0x120577(0x2a0)]=function(_0x4d02f2,_0x2cdb07=![],_0x15ce92=![]){var _0x5c3757=_0x120577;_0x150827(_0x4d02f2);if(!(_0x4d02f2 in _0x435689[_0x5c3757(0xc6c)])){log(_0x5c3757(0x24b));try{var _0x19840f=document[_0x5c3757(0x54c)](_0x5c3757(0x852)+_0x4d02f2);if(_0x19840f){warnlog('Removing\x20orphaned\x20control\x20box\x20for\x20UUID:\x20'+_0x4d02f2);var _0x380aa4=_0x19840f[_0x5c3757(0x413)]('[data-sid]'),_0x36b2d4=_0x380aa4?_0x380aa4[_0x5c3757(0xc6b)][_0x5c3757(0x952)]:null;_0x19840f[_0x5c3757(0x631)][_0x5c3757(0x5fc)](_0x19840f),updateLockedElements();if(_0x36b2d4&&_0x435689[_0x5c3757(0x781)]&&typeof syncDirectorState===_0x5c3757(0x5f9))try{syncDirectorState({'dataset':{'sid':_0x36b2d4}});}catch(_0x1aa901){warnlog(_0x1aa901);}}var _0x4d402c=document['getElementById']('container_'+_0x4d02f2+_0x5c3757(0xac8));_0x4d402c&&(warnlog('Removing\x20orphaned\x20screen\x20container\x20for\x20UUID:\x20'+_0x4d02f2),_0x4d402c['parentNode'][_0x5c3757(0x5fc)](_0x4d402c));}catch(_0x3625e2){warnlog(_0x3625e2);}return![];}warnlog('closeRPC'),clearInterval(_0x435689['rpcs'][_0x4d02f2]['closeTimeout']),clearTimeout(_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x5b6)]);if(_0x435689[_0x5c3757(0x1c8)]&&_0x435689[_0x5c3757(0x1c8)][_0x5c3757(0x49c)](_0x4d02f2))try{_0x435689[_0x5c3757(0x1c8)][_0x5c3757(0x20c)](_0x435689['soloChatUUID'][_0x5c3757(0x97c)](_0x4d02f2),0x1),_0x435689[_0x5c3757(0x186)](![]);}catch(_0xeb2c23){}if(_0x435689[_0x5c3757(0xb23)]&&_0x15ce92)_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x614)]=!![];else{if(_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x614)]&&!_0x2cdb07)return!![];else{_0x435689[_0x5c3757(0xc6c)][_0x4d02f2]['stashed']=![];try{_0x435689['sendRequest']({'bye':!![]},_0x4d02f2),warnlog('SEND\x20BYE');}catch(_0x1e58be){}}}try{var _0x26a14f=_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x885)];}catch(_0x30c6c6){}try{_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x757)]();}catch(_0x16d432){warnlog(_0x5c3757(0x5d9));}if(_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x614)])return!![];_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x33f)]&&clearInterval(_0x435689[_0x5c3757(0xc6c)][_0x4d02f2]['motionDetectionInterval']);try{_0x435689[_0x5c3757(0xc6c)][_0x4d02f2]['streamSrc']&&_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x857)][_0x5c3757(0x81f)]()[_0x5c3757(0x3d5)](function(_0x24a58c){var _0x27ec6a=_0x5c3757;_0x24a58c[_0x27ec6a(0xaef)](),log(_0x27ec6a(0x819));});}catch(_0x181da5){}if(_0x435689['director'])try{_0x435689[_0x5c3757(0xc6c)][_0x4d02f2]['videoElement']&&_0x5c3757(0x6c8)in _0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x917)]&&_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x917)][_0x5c3757(0x6c8)][_0x5c3757(0xaef)]();}catch(_0x1901fd){warnlog(_0x1901fd);}else!_0x435689[_0x5c3757(0xb0b)]&&(_0x435689['beepToNotify']&&playtone(![],_0x5c3757(0x2b5)));try{document[_0x5c3757(0x54c)](_0x5c3757(0x852)+_0x4d02f2)&&(!_0x435689['syncState']&&(_0x435689['syncState']={}),_0x26a14f&&(_0x435689[_0x5c3757(0x232)][_0x26a14f]=getDetailedState(_0x26a14f)),getById(_0x5c3757(0x852)+_0x4d02f2)[_0x5c3757(0x631)]['removeChild'](getById('container_'+_0x4d02f2)),updateLockedElements());}catch(_0x27f471){warnlog(_0x27f471);}try{_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x917)]&&_0x435689['rpcs'][_0x4d02f2][_0x5c3757(0x917)][_0x5c3757(0xae4)]();}catch(_0x1d82bd){}try{if(_0x435689['broadcast']!==![]){if(_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x964)]){try{_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x964)][_0x5c3757(0xae4)]();}catch(_0x36f472){errorlog(_0x36f472);}_0x435689[_0x5c3757(0xc6c)][_0x4d02f2]['iframeEle'][_0x5c3757(0xae4)]();}}}catch(_0x4ff922){}try{_0x435689['rpcs'][_0x4d02f2][_0x5c3757(0xa8d)]&&_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0xa8d)][_0x5c3757(0xae4)]();}catch(_0x30dca4){}try{_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x7ea)]&&_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x7ea)][_0x5c3757(0xae4)]();}catch(_0x51fb57){}_0x5c3757(0x294)in _0x435689[_0x5c3757(0xc6c)][_0x4d02f2]&&clearInterval(_0x435689[_0x5c3757(0xc6c)][_0x4d02f2]['eventPlayActive']);pokeIframeAPI(_0x5c3757(0x191),![],_0x4d02f2),pokeAPI(_0x5c3757(0xbbf),_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x885)]);if(_0x435689['discordHook'])try{pokeDiscord('endViewConnection',{'streamID':_0x435689[_0x5c3757(0xc6c)][_0x4d02f2]['streamID'],'label':_0x435689['rpcs'][_0x4d02f2][_0x5c3757(0xc69)],'session':_0x435689['rpcs'][_0x4d02f2][_0x5c3757(0x767)],'startTime':_0x435689['rpcs'][_0x4d02f2][_0x5c3757(0x6ae)],'hangup':_0x2cdb07});}catch(_0x362414){console[_0x5c3757(0x2cd)](_0x362414);}_0x435689[_0x5c3757(0xc6c)][_0x4d02f2][_0x5c3757(0x4fb)]&&(_0x26a14f=![]);if(_0x435689[_0x5c3757(0x427)])try{onGuestLeftMixMinus(_0x4d02f2);}catch(_0x1df531){}try{_0x435689[_0x5c3757(0xc6c)][_0x4d02f2]=null,delete _0x435689[_0x5c3757(0xc6c)][_0x4d02f2];}catch(_0x58881f){}try{_0x435689['closeRPC'](_0x4d02f2+_0x5c3757(0xac8));}catch(_0x3c626b){}(!_0x435689[_0x5c3757(0x781)]||_0x435689[_0x5c3757(0x313)])&&setTimeout(function(){updateMixer();},0x1);if(typeof _0x26a14f=='undefined')return![];try{warnlog('Should\x20we\x20ask\x20to\x20play\x20the\x20stream\x20Again?'),_0x26a14f&&(_0x26a14f in _0x435689['watchTimeoutList']&&(log(_0x5c3757(0x1fa)+_0x26a14f),clearTimeout(_0x435689[_0x5c3757(0x698)][_0x26a14f]),delete _0x435689[_0x5c3757(0x698)][_0x26a14f]),_0x435689[_0x5c3757(0x698)][_0x26a14f]=setTimeout(function(_0x16107f){var _0x15fa39=_0x5c3757;try{delete _0x435689['watchTimeoutList'][_0x16107f];}catch(_0x5f1bad){return warnlog(_0x15fa39(0xa98)),![];}log('watchTimeoutList2:'+_0x16107f);try{for(var _0x397f08 in _0x435689[_0x15fa39(0xc6c)]){if(_0x435689['rpcs'][_0x397f08]['streamID']===_0x16107f){if(_0x435689[_0x15fa39(0xc6c)][_0x397f08][_0x15fa39(0x8e8)]==='connected')return warnlog(_0x15fa39(0x40e)),![];}}}catch(_0xa0e5f3){errorlog(_0xa0e5f3);}warnlog(_0x15fa39(0x8b5)),_0x435689[_0x15fa39(0x264)](_0x16107f);},_0x435689['retryTimeout'],_0x26a14f));}catch(_0x4df83b){errorlog(_0x4df83b);}pokeIframeAPI(_0x5c3757(0x182),![],_0x4d02f2);_0x26a14f!==null?pokeIframeAPI(_0x5c3757(0x190),_0x26a14f,_0x4d02f2):pokeIframeAPI(_0x5c3757(0x190),!![],_0x4d02f2);try{closeModal(![],'approval-'+_0x4d02f2);}catch(_0x3d6a47){}return updateUserList(),![];},_0x435689[_0x120577(0x942)]=null,_0x435689[_0x120577(0x5a4)]=function(){var _0x1b1ce6=_0x120577,_0x401898=![];if(_0x435689['view']){_0x435689[_0x1b1ce6(0xbe0)]&&clearTimeout(_0x435689[_0x1b1ce6(0x942)]);if(_0x435689['ws']===null||typeof _0x435689['ws']!==_0x1b1ce6(0x31f)||_0x435689['ws']['readyState']!==0x1){}else{var _0x3a770b=_0x435689[_0x1b1ce6(0x6c0)]['split'](',');for(var _0x2f5019 in _0x3a770b){if(_0x3a770b[_0x2f5019]){var _0x45f1f0=![];for(var _0x2ab68f in _0x435689[_0x1b1ce6(0xc6c)]){if(_0x435689[_0x1b1ce6(0xc6c)][_0x2ab68f][_0x1b1ce6(0x885)]&&_0x435689['rpcs'][_0x2ab68f][_0x1b1ce6(0x885)]===_0x3a770b[_0x2f5019]){_0x45f1f0=!![];break;}}_0x3a770b[_0x2f5019]in _0x435689[_0x1b1ce6(0x698)]&&(_0x45f1f0=!![]);if(_0x45f1f0)continue;_0x435689[_0x1b1ce6(0x264)](_0x3a770b[_0x2f5019]),_0x401898=!![];}}}_0x435689['forceRetry']&&_0x435689[_0x1b1ce6(0xbe0)]<0xa&&(_0x435689['forceRetry']=0xa),_0x435689['forceRetry']&&(_0x435689['forceRetryTimeout']=setTimeout(function(){var _0x160c6c=_0x1b1ce6;log(_0x160c6c(0x39f)),_0x435689[_0x160c6c(0x5a4)]();},_0x435689[_0x1b1ce6(0xbe0)]*0x3e8));}return _0x401898;},_0x435689[_0x120577(0x625)]=async function(_0x28f225){var _0x1535b5=_0x120577;if(_0x28f225 in _0x435689['pcs']){if(_0x435689[_0x1535b5(0x76f)][_0x28f225]['connectionState']===_0x1535b5(0x35e)||_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x8e8)]===_0x1535b5(0x94c))log(_0x1535b5(0xa42)),_0x435689[_0x1535b5(0x974)](_0x28f225),warnlog(_0x1535b5(0x177));else{if(iPad||iOS)log('closing\x207'),_0x435689[_0x1535b5(0x974)](_0x28f225),warnlog(_0x1535b5(0xbef));else{if(_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x8e8)]!==_0x1535b5(0x5af)){await sleep(0x3e8);if(_0x435689[_0x1535b5(0x76f)][_0x28f225]){if(_0x435689[_0x1535b5(0x76f)][_0x28f225]['connectionState']!==_0x1535b5(0x5af))log(_0x1535b5(0xa42)),_0x435689['closePC'](_0x28f225),warnlog(_0x1535b5(0x177));else{warnlog(_0x1535b5(0x7ce)+_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x8e8)]);return;}}}else{warnlog(_0x1535b5(0x7ce)+_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x8e8)]);return;}}}}else log(_0x1535b5(0x1ba));if(_0x435689[_0x1535b5(0x89e)]!==![]){if(Object['keys'](_0x435689[_0x1535b5(0x76f)])[_0x1535b5(0xb04)]>_0x435689[_0x1535b5(0x89e)]){log(_0x1535b5(0x65a)),log(_0x1535b5(0x42d)),_0x435689['closePC'](_0x28f225);return;}}else{if(_0x435689[_0x1535b5(0x7c1)]!==![]){if(Object[_0x1535b5(0x161)](_0x435689['rpcs'])[_0x1535b5(0xb04)]+Object['keys'](_0x435689[_0x1535b5(0x76f)])[_0x1535b5(0xb04)]>_0x435689[_0x1535b5(0x7c1)]){log('closing\x202'),log(_0x1535b5(0x4c6)),_0x435689[_0x1535b5(0x974)](_0x28f225);return;}}}!_0x435689[_0x1535b5(0x238)]&&await chooseBestTURN();_0x435689['encodedInsertableStreams']&&(_0x435689['configuration'][_0x1535b5(0x8e7)]=!![]);_0x435689['bundlePolicy']&&(_0x435689[_0x1535b5(0x238)][_0x1535b5(0x570)]=_0x435689[_0x1535b5(0x570)]);try{_0x435689[_0x1535b5(0x76f)][_0x28f225]=new RTCPeerConnection(_0x435689['configuration']);try{_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x626)]=_0x555482=>{warnlog(_0x555482);};}catch(_0x1de48e){warnlog(_0x1de48e);}}catch(_0x4fcb75){!_0x435689['cleanOutput']&&warnUser('An\x20RTC\x20error\x20occurred');errorlog(_0x4fcb75);return;}if(_0x435689[_0x1535b5(0x138)]){if(Object[_0x1535b5(0x161)](_0x435689[_0x1535b5(0x76f)])[_0x1535b5(0xb04)]>0x1){log('closing\x203'),log(_0x1535b5(0x7f7)),_0x435689[_0x1535b5(0x974)](_0x28f225);return;}}_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x5c2)]={},_0x435689[_0x1535b5(0x76f)][_0x28f225]['session']=_0x435689[_0x1535b5(0x812)]+_0x435689[_0x1535b5(0xa1a)](0x5),_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x2ed)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225]['sceneMute']=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x599)]={},_0x435689['pcs'][_0x28f225][_0x1535b5(0x599)][_0x1535b5(0x3bf)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x599)][_0x1535b5(0xaab)]=null,_0x435689['pcs'][_0x28f225][_0x1535b5(0x599)][_0x1535b5(0x3ca)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x599)][_0x1535b5(0x7b2)]=null,_0x435689['pcs'][_0x28f225][_0x1535b5(0x599)][_0x1535b5(0xb27)]=null,_0x435689['pcs'][_0x28f225][_0x1535b5(0x18d)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x89f)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0xb15)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0xb50)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225]['layoutState']=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x212)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x9e7)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x511)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0x686)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0x908)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0x9d8)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x9da)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x2ba)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x3c8)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0x9d4)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x68c)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0x784)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x337)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225]['allowVideo']=![],_0x435689[_0x1535b5(0x76f)][_0x28f225]['allowAudio']=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0x9ee)]=![],_0x435689['pcs'][_0x28f225]['allowIframe']=![],_0x435689[_0x1535b5(0x76f)][_0x28f225]['allowWidget']=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x699)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0x657)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0xa83)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x228)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225]['allowBroadcast']=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x208)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0x53b)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x927)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x43a)]=null,_0x435689['pcs'][_0x28f225][_0x1535b5(0x288)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0xb80)]=null,_0x435689['pcs'][_0x28f225][_0x1535b5(0x9d6)]=_0x28f225,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x61b)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225]['rotation']=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0xbb0)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x9f5)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x4bd)]=![],_0x435689['pcs'][_0x28f225]['scaleSnap']=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x2c8)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x96c)]=![],_0x435689['pcs'][_0x28f225][_0x1535b5(0xb33)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0xa34)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x2a6)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225]['keyframeTimeout']=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0xc69)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x367)]=![],_0x435689['pcs'][_0x28f225]['preferVideoCodec']=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x74e)]=![],_0x435689['pcs'][_0x28f225]['closeTimeout']=null,_0x435689['pcs'][_0x28f225][_0x1535b5(0x5ce)]=_0x435689[_0x1535b5(0x5ce)],_0x435689['pcs'][_0x28f225][_0x1535b5(0xba7)]=![],_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x6ae)]=Date[_0x1535b5(0x1ab)](),_0x435689[_0x1535b5(0x76f)][_0x28f225]['needsPublishing']=null;function _0xbeada8(_0x2aae4e=![]){var _0x510057=_0x1535b5;if(_0x2aae4e)return;_0x435689[_0x510057(0x76f)][_0x28f225]['sendChannel']=_0x435689['pcs'][_0x28f225][_0x510057(0x644)](_0x510057(0x4d0)),_0x435689[_0x510057(0x76f)][_0x28f225][_0x510057(0x4d0)][_0x510057(0x9d6)]=_0x28f225,_0x435689[_0x510057(0x76f)][_0x28f225]['sendChannel'][_0x510057(0x22d)]=_0x19f8d4=>{var _0x178b3f=_0x510057;_0x19f8d4[_0x178b3f(0x4bc)]&&_0x19f8d4[_0x178b3f(0x4bc)][_0x178b3f(0x7bc)]&&_0x19f8d4[_0x178b3f(0x4bc)][_0x178b3f(0x7bc)]!==0xc&&warnlog(_0x19f8d4),log(_0x178b3f(0xa96)+_0x28f225);},_0x435689[_0x510057(0x76f)][_0x28f225][_0x510057(0x4d0)][_0x510057(0xada)]=()=>{var _0x2813a4=_0x510057;if(_0x2aae4e)return;_0x435689['pcs'][_0x28f225][_0x2813a4(0xac4)]=0x0,log(_0x2813a4(0x2a9)),msg={},msg[_0x2813a4(0x7af)]={},msg[_0x2813a4(0x7af)][_0x2813a4(0xc69)]=_0x435689[_0x2813a4(0xc69)],msg['info'][_0x2813a4(0x920)]=_0x435689[_0x2813a4(0x920)],msg[_0x2813a4(0x7af)]['order']=_0x435689[_0x2813a4(0x367)],msg[_0x2813a4(0x7af)]['muted']=_0x435689[_0x2813a4(0xb62)],msg[_0x2813a4(0x7af)][_0x2813a4(0x51c)]=_0x435689[_0x2813a4(0x8f5)];_0x435689[_0x2813a4(0x222)]&&(msg['info']['preferChannel']=_0x435689[_0x2813a4(0x222)]);_0x435689[_0x2813a4(0x871)]&&(msg[_0x2813a4(0x7af)][_0x2813a4(0x80c)]=!![],msg[_0x2813a4(0x7af)][_0x2813a4(0xa35)]=_0x435689[_0x2813a4(0xa35)]||_0x435689[_0x2813a4(0x885)],msg['info']['tipServer']=_0x435689[_0x2813a4(0x58d)],msg[_0x2813a4(0x7af)][_0x2813a4(0x12a)]=_0x435689[_0x2813a4(0x12a)],msg[_0x2813a4(0x7af)][_0x2813a4(0xadc)]=_0x435689[_0x2813a4(0xadc)]);try{(_0x435689[_0x2813a4(0x1cd)][_0x2813a4(0xb04)]||_0x435689['allowNoGroup'])&&(msg['info'][_0x2813a4(0x27b)]=_0x435689[_0x2813a4(0x1cd)][_0x2813a4(0x28e)](','));}catch(_0x25b0f8){}msg[_0x2813a4(0x7af)][_0x2813a4(0x8f9)]=_0x435689[_0x2813a4(0x8f9)],msg[_0x2813a4(0x7af)][_0x2813a4(0x310)]=_0x435689[_0x2813a4(0x310)],msg[_0x2813a4(0x7af)][_0x2813a4(0x5b4)]=_0x435689['directorVideoMuted'],msg[_0x2813a4(0x7af)]['directorMirror']=_0x435689[_0x2813a4(0xb47)],msg[_0x2813a4(0x7af)]['directorFlip']=_0x435689[_0x2813a4(0xb0c)],msg[_0x2813a4(0x7af)][_0x2813a4(0xad3)]=_0x435689[_0x2813a4(0xc02)];_0x435689[_0x2813a4(0xb0b)]?msg[_0x2813a4(0x7af)][_0x2813a4(0x1bc)]=!![]:msg[_0x2813a4(0x7af)][_0x2813a4(0x1bc)]=![];msg[_0x2813a4(0x7af)]['proaudio_init']=_0x435689[_0x2813a4(0x901)];_0x435689[_0x2813a4(0x467)]&&(msg[_0x2813a4(0x7af)][_0x2813a4(0x3ea)]=_0x435689[_0x2813a4(0x467)]);if(_0x435689['director']){if(!_0x435689[_0x2813a4(0x861)]&&_0x435689[_0x2813a4(0xb2f)]&&_0x435689[_0x2813a4(0xb2f)]===_0x28f225)_0x435689[_0x2813a4(0x1a6)]();else{msg[_0x2813a4(0xb41)]={};_0x435689[_0x2813a4(0x861)]&&(msg[_0x2813a4(0xb41)]['tokenDirector']=!![]);msg[_0x2813a4(0xb41)][_0x2813a4(0x72d)]=_0x435689[_0x2813a4(0x72d)];_0x435689['soloChatUUID']['length']&&!_0x435689['soloChatUUID'][_0x2813a4(0x49c)](_0x28f225)&&(msg['info'][_0x2813a4(0xb62)]=!![]);var _0x43cac8=[];for(var _0xe2a600 in _0x435689[_0x2813a4(0x76f)]){_0x435689['pcs'][_0xe2a600][_0x2813a4(0x908)]===!![]&&_0x43cac8['push'](_0xe2a600);}_0x435689[_0x2813a4(0x639)]&&(msg[_0x2813a4(0xb41)][_0x2813a4(0x542)]=!![]),_0x43cac8[_0x2813a4(0xb04)]&&(msg[_0x2813a4(0xb41)]['addCoDirector']=_0x43cac8);}_0x435689[_0x2813a4(0x883)]&&(msg[_0x2813a4(0x7af)][_0x2813a4(0xa8b)]=_0x435689[_0x2813a4(0x883)]);}_0x435689['broadcast']!==![]?msg[_0x2813a4(0x7af)][_0x2813a4(0x4a2)]=!![]:msg['info'][_0x2813a4(0x4a2)]=![];_0x435689[_0x2813a4(0xba7)]?msg[_0x2813a4(0x7af)][_0x2813a4(0xba7)]=!![]:msg[_0x2813a4(0x7af)][_0x2813a4(0xba7)]=![];_0x435689[_0x2813a4(0x9ee)]?msg[_0x2813a4(0x7af)]['allowdrawing']=!![]:msg[_0x2813a4(0x7af)][_0x2813a4(0x2dc)]=![];if(_0x435689['obsControls'])msg[_0x2813a4(0x7af)][_0x2813a4(0x702)]=_0x435689['obsControls'];else{if(_0x435689[_0x2813a4(0xa24)]===![])msg[_0x2813a4(0x7af)][_0x2813a4(0x702)]=![];else _0x435689[_0x2813a4(0xb0b)]&&!_0x435689['director']?msg[_0x2813a4(0x7af)][_0x2813a4(0x702)]=![]:msg['info'][_0x2813a4(0x702)]=null;}_0x435689['consent']&&(msg[_0x2813a4(0x7af)][_0x2813a4(0x3e6)]=!![]);msg[_0x2813a4(0x7af)][_0x2813a4(0x2a1)]=_0x435689[_0x2813a4(0x75e)];!_0x435689['notifyScreenShare']&&(msg[_0x2813a4(0x7af)][_0x2813a4(0x9c6)]=!![]);_0x435689[_0x2813a4(0xa29)]?msg['info'][_0x2813a4(0x4b9)]=!!_0x435689[_0x2813a4(0x4b9)]:msg['info'][_0x2813a4(0x4b9)]=![];msg['info']['width_url']=_0x435689[_0x2813a4(0x3dd)],msg[_0x2813a4(0x7af)][_0x2813a4(0x2ad)]=_0x435689[_0x2813a4(0x348)];try{if(_0x435689[_0x2813a4(0x857)]){let _0x21e622=_0x435689[_0x2813a4(0x857)]['getVideoTracks']();if(_0x21e622[_0x2813a4(0xb04)]){let _0x3213f6=_0x21e622[0x0][_0x2813a4(0x3ef)]();msg['info'][_0x2813a4(0x5e6)]=_0x3213f6[_0x2813a4(0x3dd)]||![],msg['info']['video_init_height']=_0x3213f6[_0x2813a4(0x348)]||![],msg[_0x2813a4(0x7af)][_0x2813a4(0x751)]=parseInt(_0x3213f6[_0x2813a4(0xb4a)])||![];}}if(_0x435689[_0x2813a4(0xa59)]&&_0x435689[_0x2813a4(0xa59)][_0x2813a4(0x200)]){let _0x2d7cd8=_0x435689[_0x2813a4(0xa59)][_0x2813a4(0x200)]['getVideoTracks']();if(_0x2d7cd8[_0x2813a4(0xb04)]){let _0x5d4dbe=_0x2d7cd8[0x0]['getSettings']();msg['info']['video_2_init_width']=_0x5d4dbe[_0x2813a4(0x3dd)]||![],msg['info']['video_2_init_height']=_0x5d4dbe[_0x2813a4(0x348)]||![],msg[_0x2813a4(0x7af)][_0x2813a4(0xa8c)]=parseInt(_0x5d4dbe[_0x2813a4(0xb4a)])||![];}}}catch(_0x3838e4){errorlog(_0x3838e4);}(_0x435689[_0x2813a4(0x538)]||_0x435689[_0x2813a4(0x7df)])&&(msg['info']['midi_url']=!![]);msg['info']['quality_url']=_0x435689[_0x2813a4(0x92e)],msg[_0x2813a4(0x7af)][_0x2813a4(0x7b7)]=_0x435689[_0x2813a4(0x4d8)],msg[_0x2813a4(0x7af)]['maxviewers_url']=_0x435689[_0x2813a4(0x89e)],msg[_0x2813a4(0x7af)]['stereo_url']=_0x435689['stereo'],msg[_0x2813a4(0x7af)][_0x2813a4(0x954)]=_0x435689[_0x2813a4(0xa55)],msg[_0x2813a4(0x7af)][_0x2813a4(0x444)]=_0x435689['autoGainControl'],msg['info']['denoise_url']=_0x435689[_0x2813a4(0xaa8)],msg[_0x2813a4(0x7af)][_0x2813a4(0x66e)]=_0x435689[_0x2813a4(0x14f)],msg[_0x2813a4(0x7af)][_0x2813a4(0xa46)]=_0x435689['version'],msg[_0x2813a4(0x7af)][_0x2813a4(0x9ed)]=_0x435689['audioGain'],msg['info']['recording_audio_compressor_type']=_0x435689['compressor'],msg[_0x2813a4(0x7af)][_0x2813a4(0x732)]=_0x435689['micDelay'],msg[_0x2813a4(0x7af)][_0x2813a4(0x174)]=_0x435689['audioLatency'],msg[_0x2813a4(0x7af)][_0x2813a4(0x218)]=!_0x435689['disableWebAudio'],msg[_0x2813a4(0x7af)]['playback_audio_pipeline']=_0x435689[_0x2813a4(0x205)],msg[_0x2813a4(0x7af)][_0x2813a4(0x4a8)]=_0x435689[_0x2813a4(0x148)],msg['info']['playback_audio_volume_meter']=_0x435689[_0x2813a4(0x655)];_0x435689[_0x2813a4(0x15d)]&&(msg[_0x2813a4(0x7af)][_0x2813a4(0x15d)]=_0x435689[_0x2813a4(0x15d)]);_0x435689[_0x2813a4(0x5c2)][_0x2813a4(0xbc7)]&&(msg['info'][_0x2813a4(0x483)]=_0x435689['stats'][_0x2813a4(0xbc7)]);_0x435689[_0x2813a4(0x7ac)]!==![]?_0x435689[_0x2813a4(0xc40)]?msg[_0x2813a4(0x7af)][_0x2813a4(0x814)]=_0x435689[_0x2813a4(0x7ac)]+parseInt(_0x435689[_0x2813a4(0xc40)]):msg[_0x2813a4(0x7af)][_0x2813a4(0x814)]=_0x435689[_0x2813a4(0x7ac)]:msg[_0x2813a4(0x7af)][_0x2813a4(0x814)]=_0x435689[_0x2813a4(0xc40)];msg['info'][_0x2813a4(0x814)]&&msg['info'][_0x2813a4(0x814)]>=0x168&&(msg[_0x2813a4(0x7af)][_0x2813a4(0x814)]-=0x168);try{navigator&&navigator[_0x2813a4(0x19a)]&&(msg[_0x2813a4(0x7af)][_0x2813a4(0xaf0)]=navigator[_0x2813a4(0x19a)]);navigator&&navigator[_0x2813a4(0x2ef)]&&(msg['info'][_0x2813a4(0x2ef)]=navigator['platform']);gpgpuSupport&&(msg['info']['gpGPU']=gpgpuSupport);cpuSupport&&(msg[_0x2813a4(0x7af)]['CPU']=cpuSupport);iOS&&(msg[_0x2813a4(0x7af)]['iPhone12Up']=iPhone12Up);if(SafariVersion)msg[_0x2813a4(0x7af)][_0x2813a4(0xa5c)]=_0x2813a4(0xb98)+SafariVersion;else{if(getChromiumVersion()>0x3c)msg[_0x2813a4(0x7af)][_0x2813a4(0xa5c)]='Chromium-based\x20v'+getChromiumVersion();else{if(Firefox)msg[_0x2813a4(0x7af)][_0x2813a4(0xa5c)]=_0x2813a4(0x6f2);else navigator[_0x2813a4(0x19a)][_0x2813a4(0x97c)]('CriOS')>=0x0?msg[_0x2813a4(0x7af)][_0x2813a4(0xa5c)]=_0x2813a4(0x983):msg[_0x2813a4(0x7af)][_0x2813a4(0xa5c)]=_0x2813a4(0xb1c);}}}catch(_0x2b9b5b){}_0x435689[_0x2813a4(0x307)]&&(_0x2813a4(0xc1d)in _0x435689[_0x2813a4(0x307)]&&(typeof _0x435689[_0x2813a4(0x307)]['level']==_0x2813a4(0x91d)?msg[_0x2813a4(0x7af)][_0x2813a4(0x23c)]=parseInt(_0x435689[_0x2813a4(0x307)][_0x2813a4(0xc1d)]*0x64):msg[_0x2813a4(0x7af)]['power_level']=_0x435689['batteryState']['level']),_0x2813a4(0x18b)in _0x435689[_0x2813a4(0x307)]&&(msg['info'][_0x2813a4(0xa77)]=_0x435689['batteryState']['charging']));if(_0x435689[_0x2813a4(0x781)]&&_0x435689['roomTimerGlobal']){if(_0x435689[_0x2813a4(0xa09)]&&_0x435689[_0x2813a4(0xa09)]>0x0)msg[_0x2813a4(0x4f4)]=_0x435689[_0x2813a4(0xa09)]-Date[_0x2813a4(0x1ab)]()/0x3e8,msg[_0x2813a4(0x349)]=!![],msg[_0x2813a4(0x16c)]=!![];else _0x435689[_0x2813a4(0xa09)]&&_0x435689['roomTimer']<0x0&&(msg[_0x2813a4(0x4f4)]=_0x435689['roomTimer']*-0x1,msg['showClock']=!![],msg[_0x2813a4(0x16c)]=!![],msg[_0x2813a4(0x963)]=!![]);_0x435689[_0x2813a4(0x943)]&&(msg['showTime']=!![]);}_0x435689[_0x2813a4(0x3b7)]&&(msg[_0x2813a4(0x7af)][_0x2813a4(0x3b7)]=_0x435689[_0x2813a4(0x3b7)]);try{_0x435689[_0x2813a4(0x7af)][_0x2813a4(0x4d6)]&&(msg[_0x2813a4(0xa27)]={},msg[_0x2813a4(0xa27)]['out']={},msg[_0x2813a4(0xa27)][_0x2813a4(0x4d6)]['c']=_0x435689[_0x2813a4(0x7af)][_0x2813a4(0x4d6)]['c']);}catch(_0x4c0531){}_0x435689[_0x2813a4(0x896)](msg,_0x28f225),pokeIframeAPI('new-push-connection',!![],_0x28f225),pokeIframeAPI(_0x2813a4(0x49a),!![],_0x28f225),updateUserList();},_0x435689[_0x510057(0x76f)][_0x28f225][_0x510057(0x4d0)][_0x510057(0x4ab)]=()=>{var _0x3525fb=_0x510057;pokeIframeAPI('new-push-connection',![],_0x28f225),_0x435689['ping'](),warnlog(_0x3525fb(0xc57));return;},_0x435689[_0x510057(0x3a0)]=async function(_0x5d5ef6,_0x27ea2a){var _0xc93f94=_0x510057;log('received\x20data\x20from\x20viewer');try{var _0x56438c=JSON['parse'](_0x5d5ef6[_0xc93f94(0x36f)]);}catch(_0x43fc82){warnlog(_0xc93f94(0x4dc)),log(_0x5d5ef6[_0xc93f94(0x36f)]);try{var _0x3dd51c=new TextDecoder()[_0xc93f94(0x456)](_0x5d5ef6[_0xc93f94(0x36f)]),_0x56438c=JSON[_0xc93f94(0x48a)](_0x3dd51c);}catch(_0x4ac272){try{var _0x56438c=await new Response(_0x5d5ef6[_0xc93f94(0x36f)])[_0xc93f94(0xa26)]();_0x56438c=JSON['parse'](_0x56438c);}catch(_0x38263e){return;}}}log(_0x56438c);if(_0xc93f94(0xba7)in _0x56438c)try{_0x56438c=await _0x435689[_0xc93f94(0x603)](_0x56438c);if(!_0x56438c)return;}catch(_0x4c2fa1){errorlog(_0x4c2fa1);}if(_0x56438c[_0xc93f94(0x3b4)]||_0x56438c[_0xc93f94(0x370)]){let _0x255f12=_0x56438c['rmid']||_0x56438c[_0xc93f94(0x370)];if(_0x435689[_0xc93f94(0x8d7)][_0x27ea2a]){if(_0x435689[_0xc93f94(0x8d7)][_0x27ea2a][_0xc93f94(0x49c)](_0x255f12))return;else _0x435689[_0xc93f94(0x8d7)][_0x27ea2a][_0xc93f94(0x35c)](_0x255f12);}else _0x435689[_0xc93f94(0x8d7)][_0x27ea2a]=[_0x255f12];}_0xc93f94(0xc70)in _0x56438c?await _0x435689['processPCSOnMessage'](_0x56438c,_0x27ea2a+_0xc93f94(0xac8),_0x27ea2a):await _0x435689[_0xc93f94(0x3f1)](_0x56438c,_0x27ea2a);};}!_0x435689[_0x1535b5(0x4e5)]&&_0xbeada8(![]);_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x1d9)]=_0x8503e0=>{var _0x3b5362=_0x1535b5;warnlog('data\x20channel\x20being\x20used\x20in\x20reverse;\x20this\x20shouldn\x27t\x20really\x20happen,\x20except\x20if\x20maybe\x20doing\x20a\x20file\x20transfer'),warnlog(_0x8503e0);if(_0x8503e0['channel'][_0x3b5362(0xc69)]&&_0x8503e0['channel'][_0x3b5362(0xc69)]!==_0x3b5362(0x4d0)){_0x435689[_0x3b5362(0x24c)](_0x435689[_0x3b5362(0xc6c)],_0x28f225,_0x8503e0['channel']);return;}},_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x5e8)]=_0x3a4a70=>{var _0x141149=_0x1535b5;log(_0x141149(0xb92));try{_0x435689[_0x141149(0xa84)](_0x28f225);}catch(_0x3cfb3d){warnlog(_0x3cfb3d);}},_0x435689['pcs'][_0x28f225][_0x1535b5(0xa51)]=_0x193b61=>{errorlog('Publisher\x20is\x20being\x20sent\x20a\x20video\x20stream???\x20NOT\x20EXPECTED!');},_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0xb68)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225]['iceBundle']=[],_0x435689[_0x1535b5(0x76f)][_0x28f225]['delayIceSend']=0xa,_0x435689['pcs'][_0x28f225][_0x1535b5(0x957)]=null,_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x73b)]=_0x42e42a=>{var _0xc54591=_0x1535b5;if(_0x42e42a['candidate']==null){log(_0xc54591(0x9a1));_0x435689[_0xc54591(0x664)]&&_0x435689['pcs'][_0x28f225][_0xc54591(0x957)]&&(_0x435689[_0xc54591(0x76f)][_0x28f225]['iceCandidatesPromise']['resolve'](),_0x435689[_0xc54591(0x76f)][_0x28f225]['iceCandidatesPromise']=![]);return;}else{if(_0x435689[_0xc54591(0x664)]&&_0x435689[_0xc54591(0x76f)][_0x28f225][_0xc54591(0x957)])return;}log(_0x42e42a);try{if(_0x435689['icefilter']){if(_0x42e42a['candidate'][_0xc54591(0x2c7)]['indexOf'](_0x435689['icefilter'])===-0x1){log(_0xc54591(0x4b5));return;}else log(_0x42e42a['candidate']);}}catch(_0x52b651){errorlog(_0x52b651);}try{if(_0x435689[_0xc54591(0xb8f)]){if(!filterIceLAN(_0x42e42a['candidate']))return;}if(_0x435689['stunOnly']){if(!filterStunOnly(_0x42e42a[_0xc54591(0x2c7)]))return;}}catch(_0x171a89){errorlog(_0x171a89);}if(_0x435689[_0xc54591(0x76f)][_0x28f225][_0xc54591(0xb68)]!==null){_0x435689[_0xc54591(0x76f)][_0x28f225]['iceBundle']['push'](_0x42e42a[_0xc54591(0x2c7)]);return;}_0x435689['pcs'][_0x28f225][_0xc54591(0xb5b)][_0xc54591(0x35c)](_0x42e42a[_0xc54591(0x2c7)]),_0x435689[_0xc54591(0x76f)][_0x28f225][_0xc54591(0xb68)]=setTimeout(function(_0x34a2a1){var _0x3bf093=_0xc54591;try{_0x435689[_0x3bf093(0x76f)][_0x34a2a1][_0x3bf093(0xb68)]=null;}catch(_0x380a6d){warnlog(_0x3bf093(0x6d4));return;}var _0x424d61={};_0x424d61[_0x3bf093(0x9d6)]=_0x34a2a1,_0x424d61['type']='local';var _0x4b4224=_0x435689[_0x3bf093(0x76f)][_0x34a2a1][_0x3bf093(0xb5b)];try{if(_0x435689[_0x3bf093(0x392)]){var _0x11d248=filterIpv6FromCandidates(_0x4b4224);_0x4b4224=_0x11d248[_0x3bf093(0x661)];}else _0x435689['preferIpv4']!==![]&&(_0x4b4224=reorderCandidatesIpv4First(_0x4b4224));}catch(_0x54ab78){warnlog(_0x3bf093(0x77f),_0x54ab78);}_0x424d61[_0x3bf093(0x82b)]=_0x4b4224,_0x424d61[_0x3bf093(0x767)]=_0x435689[_0x3bf093(0x76f)][_0x34a2a1]['session'],_0x435689[_0x3bf093(0x76f)][_0x34a2a1][_0x3bf093(0xb5b)]=[],_0x435689['pcs'][_0x28f225][_0x3bf093(0xac4)]=0x3e8,_0x435689[_0x3bf093(0x28c)]?_0x435689[_0x3bf093(0x504)](JSON[_0x3bf093(0xc18)](_0x424d61[_0x3bf093(0x82b)]))[_0x3bf093(0x7b6)](function(_0x4ba410){var _0x4ccc9a=_0x3bf093;_0x424d61['candidates']=_0x4ba410[0x0],_0x424d61[_0x4ccc9a(0x153)]=_0x4ba410[0x1],_0x435689[_0x4ccc9a(0x2c2)](_0x424d61);})['catch'](errorlog):_0x435689[_0x3bf093(0x2c2)](_0x424d61);},_0x435689[_0xc54591(0x76f)][_0x28f225][_0xc54591(0xac4)],_0x28f225);},_0x435689[_0x1535b5(0x3f1)]=async function(_0x42b9da,_0x570b33,_0x2d50f3=![]){var _0x3595c2=_0x1535b5;_0x42b9da[_0x3595c2(0x9d6)]=_0x570b33;if(_0x42b9da[_0x3595c2(0x8e6)]){_0x435689[_0x3595c2(0x51f)](_0x42b9da);return;}else{if(_0x42b9da[_0x3595c2(0x2c7)]){log(_0x3595c2(0x7e6)),_0x435689[_0x3595c2(0xa9d)](_0x42b9da);return;}else{if(_0x42b9da[_0x3595c2(0x82b)]){log(_0x3595c2(0x886)),_0x435689[_0x3595c2(0x7eb)](_0x42b9da);return;}else{if(_0x3595c2(0x5a1)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0xa91)]=_0x42b9da['ping'],_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33),warnlog(_0x3595c2(0x724));return;}else{if(_0x3595c2(0xa91)in _0x42b9da){try{_0x435689[_0x3595c2(0x76f)][_0x570b33]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33]['lastPongToken']=_0x42b9da['pong'],_0x435689[_0x3595c2(0x76f)][_0x570b33]['lastPongAt']=Date[_0x3595c2(0x1ab)]());}catch(_0x426da5){}warnlog(_0x3595c2(0x923));return;}else{if(_0x3595c2(0x7fa)in _0x42b9da){warnlog('BYE'),log(_0x3595c2(0x821)),_0x435689[_0x3595c2(0x974)](_0x570b33);return;}else{if(_0x3595c2(0x818)in _0x42b9da){warnlog(_0x3595c2(0x968));_0x435689[_0x3595c2(0x76f)][_0x570b33]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x495)]?(log('Performing\x20ICE\x20restart\x20for\x20viewer\x20'+_0x570b33),_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x495)]()):(log(_0x3595c2(0x868)+_0x570b33),_0x435689[_0x3595c2(0xa84)](_0x570b33,!![])));return;}}}}}}}if(_0x435689['director']){if(_0x3595c2(0x85e)in _0x42b9da&&'vector'in _0x42b9da){if(_0x435689['directorPassword'])_0x435689['directorHash']?_0x435689[_0x3595c2(0x51b)](_0x42b9da['requestCoDirector'],_0x42b9da[_0x3595c2(0x153)],_0x435689[_0x3595c2(0x8a6)])[_0x3595c2(0x7b6)](function(_0x32ae80){var _0x20c350=_0x3595c2;if(_0x32ae80===_0x435689[_0x20c350(0x8a6)]){_0x435689[_0x20c350(0x76f)][_0x570b33]['coDirector']=!![],_0x435689[_0x20c350(0xc19)][_0x20c350(0x35c)](_0x570b33),getById(_0x20c350(0x852)+_0x570b33)[_0x20c350(0xab1)]['add'](_0x20c350(0x56f)),_0x435689['announceCoDirector'](_0x570b33),_0x435689[_0x20c350(0x458)](_0x570b33);var _0x47bc03={};_0x47bc03[_0x20c350(0xb96)]='requestCoDirector',_0x435689[_0x20c350(0x896)](_0x47bc03,_0x570b33);}else{warnlog(_0x20c350(0x146));var _0x47bc03={};_0x47bc03['rejected']=_0x20c350(0x85e),_0x435689[_0x20c350(0x896)](_0x47bc03,_0x570b33);}})[_0x3595c2(0x501)](function(){var _0x566d69=_0x3595c2;warnlog('Failed\x20attempt\x20to\x20connect\x20as\x20co-director');var _0x155527={};_0x155527[_0x566d69(0x592)]=_0x566d69(0x85e),_0x435689[_0x566d69(0x896)](_0x155527,_0x570b33);}):generateHash(_0x435689[_0x3595c2(0x595)]+_0x435689[_0x3595c2(0x27e)]+_0x3595c2(0x352),0xc)[_0x3595c2(0x7b6)](function(_0x270e05){var _0x40a2e8=_0x3595c2;_0x435689[_0x40a2e8(0x8a6)]=_0x270e05,_0x435689[_0x40a2e8(0x51b)](_0x42b9da['requestCoDirector'],_0x42b9da['vector'],_0x435689[_0x40a2e8(0x8a6)])['then'](function(_0x255fc8){var _0x246501=_0x40a2e8;if(_0x255fc8===_0x435689[_0x246501(0x8a6)]){_0x435689[_0x246501(0x76f)][_0x570b33]['coDirector']=!![],_0x435689[_0x246501(0xc19)][_0x246501(0x35c)](_0x570b33),getById(_0x246501(0x852)+_0x570b33)[_0x246501(0xab1)][_0x246501(0x701)](_0x246501(0x56f)),_0x435689[_0x246501(0xb88)](_0x570b33),_0x435689[_0x246501(0x458)](_0x570b33);var _0x9069bf={};_0x9069bf[_0x246501(0xb96)]=_0x246501(0x85e),_0x435689['sendRequest'](_0x9069bf,_0x570b33);}else{warnlog('codirector\x20request\x20hash\x20failed');var _0x9069bf={};_0x9069bf[_0x246501(0x592)]=_0x246501(0x85e),_0x435689['sendRequest'](_0x9069bf,_0x570b33);}})[_0x40a2e8(0x501)](function(){var _0x5e107b=_0x40a2e8;warnlog(_0x5e107b(0x271));var _0x2f80df={};_0x2f80df[_0x5e107b(0x592)]=_0x5e107b(0x85e),_0x435689['sendRequest'](_0x2f80df,_0x570b33);});return;})[_0x3595c2(0x501)](errorlog);else{warnlog('reject\x20co');var _0x2a1d7b={};_0x2a1d7b['rejected']=_0x3595c2(0x85e),_0x435689['sendRequest'](_0x2a1d7b,_0x570b33);}}if(_0x3595c2(0x3f0)in _0x42b9da&&'roomid'in _0x42b9da){log(_0x3595c2(0xb4f));if(_0x435689['codirector_transfer']){if(_0x570b33 in _0x435689['pcs']&&_0x435689['pcs'][_0x570b33][_0x3595c2(0x908)]===!![]){log(_0x3595c2(0x35a));var _0x2a1d7b={};if(_0x42b9da[_0x3595c2(0x8fa)]&&_0x42b9da[_0x3595c2(0x8fa)]['updateurl'])_0x2a1d7b['request']=_0x3595c2(0x3f0),_0x2a1d7b[_0x3595c2(0x8fa)]=_0x42b9da[_0x3595c2(0x8fa)],log(_0x2a1d7b),_0x435689[_0x3595c2(0x43f)](_0x2a1d7b,_0x42b9da['migrate'][_0x3595c2(0x9b0)](),function(){var _0x73f89=_0x3595c2,_0x227ee1={};_0x227ee1['request']=_0x73f89(0x3f0),_0x227ee1[_0x73f89(0xb0b)]=_0x42b9da['roomid'],_0x227ee1[_0x73f89(0x5df)]=_0x42b9da[_0x73f89(0x3f0)][_0x73f89(0x9b0)](),_0x435689[_0x73f89(0x3ec)](_0x227ee1);}),log(_0x2a1d7b);else{if(_0x42b9da[_0x3595c2(0x8fa)]&&_0x3595c2(0x532)in _0x42b9da[_0x3595c2(0x8fa)])_0x2a1d7b[_0x3595c2(0xaa1)]=_0x3595c2(0x3f0),_0x2a1d7b[_0x3595c2(0x8fa)]=_0x42b9da[_0x3595c2(0x8fa)],delete _0x2a1d7b[_0x3595c2(0x8fa)][_0x3595c2(0xb0b)],delete _0x2a1d7b[_0x3595c2(0x8fa)][_0x3595c2(0xc13)],log(_0x2a1d7b),_0x435689[_0x3595c2(0x43f)](_0x2a1d7b,_0x42b9da['migrate'][_0x3595c2(0x9b0)](),function(){var _0x4e512d=_0x3595c2,_0x340463={};_0x340463[_0x4e512d(0xaa1)]=_0x4e512d(0x3f0),_0x340463[_0x4e512d(0xb0b)]=_0x42b9da[_0x4e512d(0xb0b)],_0x340463[_0x4e512d(0x5df)]=_0x42b9da[_0x4e512d(0x3f0)][_0x4e512d(0x9b0)](),_0x435689[_0x4e512d(0x3ec)](_0x340463);}),log(_0x2a1d7b);else Object[_0x3595c2(0x161)](_0x42b9da[_0x3595c2(0x8fa)])[_0x3595c2(0xb04)]?(_0x2a1d7b[_0x3595c2(0xaa1)]=_0x3595c2(0x3f0),_0x2a1d7b['transferSettings']=_0x42b9da[_0x3595c2(0x8fa)],delete _0x2a1d7b[_0x3595c2(0x8fa)][_0x3595c2(0xb0b)],delete _0x2a1d7b[_0x3595c2(0x8fa)][_0x3595c2(0xc13)],log(_0x2a1d7b),_0x435689[_0x3595c2(0x43f)](_0x2a1d7b,_0x42b9da[_0x3595c2(0x3f0)][_0x3595c2(0x9b0)](),function(){var _0x22dc8c=_0x3595c2,_0x2fc0e1={};_0x2fc0e1['request']='migrate',_0x2fc0e1[_0x22dc8c(0xb0b)]=_0x42b9da[_0x22dc8c(0xb0b)],_0x2fc0e1[_0x22dc8c(0x5df)]=_0x42b9da[_0x22dc8c(0x3f0)][_0x22dc8c(0x9b0)](),_0x435689['sendMsg'](_0x2fc0e1);}),log(_0x2a1d7b)):(_0x2a1d7b[_0x3595c2(0xaa1)]=_0x3595c2(0x3f0),_0x2a1d7b[_0x3595c2(0xb0b)]=_0x42b9da[_0x3595c2(0xb0b)],_0x2a1d7b[_0x3595c2(0x5df)]=_0x42b9da[_0x3595c2(0x3f0)]['toString'](),_0x435689[_0x3595c2(0x3ec)](_0x2a1d7b));}pokeIframeAPI(_0x3595c2(0x557),_0x42b9da['roomid'],_0x42b9da[_0x3595c2(0x3f0)][_0x3595c2(0x9b0)]());}}else{var _0x2a1d7b={};_0x2a1d7b['rejected']=_0x3595c2(0x411),_0x435689['sendRequest'](_0x2a1d7b,_0x570b33);}}}if(_0x435689[_0x3595c2(0x781)]&&_0x42b9da[_0x3595c2(0xaa1)]===_0x3595c2(0xaeb)&&_0x42b9da[_0x3595c2(0x2ba)]){try{if(_0x570b33 in _0x435689[_0x3595c2(0x76f)]&&_0x435689[_0x3595c2(0x76f)][_0x570b33]['coDirector']===!![]){var _0x683ede={'directorSettings':{'addCoDirector':[_0x570b33]}};_0x435689[_0x3595c2(0x43f)](_0x683ede,_0x42b9da[_0x3595c2(0x2ba)][_0x3595c2(0x9b0)]());}}catch(_0x58b1ce){errorlog(_0x58b1ce);}return;}if(_0x3595c2(0x907)in _0x42b9da){if(!_0x42b9da['UUID']){log('no\x20UUID\x20in\x20msg');return;}var _0xa2673c=_0x42b9da[_0x3595c2(0x907)];if(!_0x435689[_0x3595c2(0x76f)][_0xa2673c]){log('no\x20pcs[UUID]');return;}if(_0x435689[_0x3595c2(0xc19)]['indexOf'](_0xa2673c)>=0x0){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x907),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x42b9da['UUID']),warnlog(_0x3595c2(0x934));return;}if(_0x435689[_0x3595c2(0x781)]&&_0x42b9da['request']===_0x3595c2(0xaeb)&&_0x42b9da['guest']){try{if(_0x570b33 in _0x435689[_0x3595c2(0x76f)]&&_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x908)]===!![]){var _0x31cec2={'directorSettings':{'addCoDirector':[_0x570b33]}};_0x435689[_0x3595c2(0x43f)](_0x31cec2,_0x42b9da[_0x3595c2(0x2ba)]['toString']());}}catch(_0x7b8e41){errorlog(_0x7b8e41);}return;}if(_0x435689[_0x3595c2(0xba7)]){if(_0x3595c2(0xba7)in _0x42b9da&&_0x42b9da['remote']===_0x435689['remote']&&_0x435689[_0x3595c2(0xba7)]){}else{if(_0x435689[_0x3595c2(0xba7)]===!![]){}}}else{if(_0x435689['directorList'][_0x3595c2(0x97c)](_0x42b9da[_0x3595c2(0x9d6)])>=0x0){}else return;}'targetBitrate'in _0x42b9da&&_0x435689[_0x3595c2(0x916)](_0xa2673c,_0x42b9da[_0x3595c2(0x916)]);_0x3595c2(0x6dd)in _0x42b9da&&_0x435689[_0x3595c2(0x6dd)](_0xa2673c,_0x42b9da[_0x3595c2(0x6dd)]);if('requestResolution'in _0x42b9da)try{_0x435689['setResolution'](_0xa2673c,_0x42b9da[_0x3595c2(0x3f6)]['w'],_0x42b9da['requestResolution']['h'],_0x42b9da[_0x3595c2(0x3f6)]['s'],_0x42b9da[_0x3595c2(0x3f6)]['c']);}catch(_0x4362d8){errorlog(_0x4362d8);}return;}manageSceneState(_0x42b9da,_0x570b33);try{if('info'in _0x42b9da){_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x5c2)][_0x3595c2(0x7af)]=_0x42b9da[_0x3595c2(0x7af)];_0x3595c2(0xc69)in _0x42b9da[_0x3595c2(0x7af)]&&(typeof _0x42b9da[_0x3595c2(0x7af)][_0x3595c2(0xc69)]==_0x3595c2(0x332)?_0x435689['pcs'][_0x570b33][_0x3595c2(0xc69)]=sanitizeLabel(_0x42b9da['info'][_0x3595c2(0xc69)]):_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xc69)]=![]);'acceptsTips'in _0x42b9da[_0x3595c2(0x7af)]&&_0x42b9da['info'][_0x3595c2(0x80c)]&&(_0x435689['pcs'][_0x570b33][_0x3595c2(0x80c)]=!![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xa35)]=_0x42b9da[_0x3595c2(0x7af)]['tipId']||null,_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x58d)]=_0x42b9da['info'][_0x3595c2(0x58d)]||_0x435689[_0x3595c2(0x58d)]||_0x3595c2(0x8dd),_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x12a)]=_0x42b9da[_0x3595c2(0x7af)][_0x3595c2(0x12a)]||[0x5,0xa,0x19,0x32,0x64],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xadc)]=_0x42b9da[_0x3595c2(0x7af)][_0x3595c2(0xadc)]||_0x3595c2(0x425),_0x435689['showTips']&&!_0x435689[_0x3595c2(0x326)]&&(typeof addTipIconToVideo==='function'&&addTipIconToVideo(_0x570b33)));if(_0x2d50f3){if(_0x2d50f3===_0x435689['directorUUID'])try{_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x5c2)]['info'][_0x3595c2(0x781)]=!![];}catch(_0x1dd289){}else{if(_0x435689[_0x3595c2(0xc19)]['indexOf'](_0x2d50f3)>=0x0)try{_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x5c2)][_0x3595c2(0x7af)]['coDirector']=!![];}catch(_0xd584a0){}}}else{if(_0x570b33===_0x435689['directorUUID'])try{_0x435689['pcs'][_0x570b33][_0x3595c2(0x5c2)][_0x3595c2(0x7af)]['director']=!![];}catch(_0x5bbdc9){}else{if(_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x97c)](_0x570b33)>=0x0)try{_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x5c2)][_0x3595c2(0x7af)]['coDirector']=!![];}catch(_0x5880b3){}}}_0x435689['layouts']&&_0x435689[_0x3595c2(0x781)]&&_0x3595c2(0x44b)in _0x42b9da['info']&&_0x42b9da[_0x3595c2(0x7af)][_0x3595c2(0x44b)]&&(broadcastSlotUpdate(_0x570b33),_0x435689[_0x3595c2(0x39d)]?_0x435689[_0x3595c2(0x896)]({'obsSceneTriggers':_0x435689[_0x3595c2(0x39d)],'layouts':_0x435689[_0x3595c2(0x582)]},_0x570b33):_0x435689[_0x3595c2(0x896)]({'layouts':_0x435689[_0x3595c2(0x582)]},_0x570b33));if(Firefox||_0x42b9da['info']['firefox']||(iOS||iPad)&&SafariVersion&&SafariVersion>0x10)try{_0x3595c2(0x256)in _0x42b9da[_0x3595c2(0x7af)]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x89f)]===![]&&(_0x42b9da['info'][_0x3595c2(0x256)]&&parseInt(_0x42b9da[_0x3595c2(0x7af)][_0x3595c2(0x256)])>0x0&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x89f)]=parseInt(_0x42b9da[_0x3595c2(0x7af)][_0x3595c2(0x256)]),_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x212)]&&clearTimeout(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x212)]),_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x212)]=setTimeout(function(_0x4b0b0c){var _0x5b34ea=_0x3595c2;_0x435689[_0x5b34ea(0x308)](_0x4b0b0c,null);},0x3e8,_0x570b33))));}catch(_0x5af919){errorlog(_0x5af919);}pokeIframeAPI(_0x3595c2(0x3da),_0x42b9da[_0x3595c2(0x7af)],_0x570b33);}if(_0x3595c2(0x25c)in _0x42b9da){if(_0x435689[_0x3595c2(0xafd)])try{_0x435689[_0x3595c2(0xafd)][_0x3595c2(0x8da)](_0x3595c2(0xaa7))&&processIframeSyncFeedback(_0x42b9da[_0x3595c2(0x25c)],_0x570b33);}catch(_0x4349f7){errorlog(_0x4349f7);}}_0x3595c2(0x6cc)in _0x42b9da&&_0x435689[_0x3595c2(0x4d9)](_0x42b9da['pipe'],_0x570b33);if(_0x3595c2(0x393)in _0x42b9da){if(_0x435689[_0x3595c2(0x9ee)]){!_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4e4)]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xbb3)]?_0x435689[_0x3595c2(0x76f)][_0x570b33]['canvasOverlay']=receiveDrawingOnVideo(_0x435689[_0x3595c2(0x2ce)],_0x570b33):_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4e4)]=receiveDrawingOnVideo(_0x435689[_0x3595c2(0x917)],_0x570b33));if(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4e4)]){if(typeof _0x42b9da[_0x3595c2(0x393)]==_0x3595c2(0x332)){if(_0x42b9da[_0x3595c2(0x393)]==_0x3595c2(0x761))_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4e4)][_0x3595c2(0x887)]();else{if(_0x42b9da[_0x3595c2(0x393)]==_0x3595c2(0x231))_0x435689[_0x3595c2(0x76f)][_0x570b33]['canvasOverlay'][_0x3595c2(0x231)]();else _0x42b9da[_0x3595c2(0x393)]==_0x3595c2(0xa01)&&_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4e4)][_0x3595c2(0x8aa)](_0x3595c2(0xa01));}}else _0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4e4)][_0x3595c2(0x8aa)](_0x42b9da[_0x3595c2(0x393)]);}}return;}_0x3595c2(0xa8b)in _0x42b9da&&(_0x435689[_0x3595c2(0x883)]=_0x42b9da[_0x3595c2(0xa8b)],_0x435689[_0x3595c2(0xc3f)](_0x570b33));_0x3595c2(0x18d)in _0x42b9da&&(_0x435689['pcs'][_0x570b33][_0x3595c2(0x18d)]=parseInt(_0x42b9da[_0x3595c2(0x18d)]));_0x3595c2(0x20f)in _0x42b9da&&_0x435689['limitAudioBitrate'](_0x570b33,_0x42b9da[_0x3595c2(0x20f)]);_0x3595c2(0xaa6)in _0x42b9da&&_0x435689[_0x3595c2(0x308)](_0x570b33,_0x42b9da[_0x3595c2(0xaa6)]);_0x3595c2(0x916)in _0x42b9da&&_0x435689[_0x3595c2(0x916)](_0x570b33,_0x42b9da[_0x3595c2(0x916)]);'targetAudioBitrate'in _0x42b9da&&_0x435689['targetAudioBitrate'](_0x570b33,_0x42b9da[_0x3595c2(0x6dd)]);if(_0x3595c2(0xbb2)in _0x42b9da){if(_0x3595c2(0xba7)in _0x42b9da){if(_0x42b9da['remote']===_0x435689[_0x3595c2(0xba7)]&&_0x435689[_0x3595c2(0xba7)]||_0x435689[_0x3595c2(0xba7)]===!![]){_0x435689[_0x3595c2(0xbb2)]();return;}}}if(_0x3595c2(0xa17)in _0x42b9da){if('remote'in _0x42b9da){if(_0x42b9da['remote']===_0x435689[_0x3595c2(0xba7)]&&_0x435689[_0x3595c2(0xba7)]||_0x435689[_0x3595c2(0xba7)]===!![]){_0x435689['hangup'](!![]);return;}}}if(_0x3595c2(0xc6e)in _0x42b9da){if(_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0){var _0xf73d8c={};if(_0x435689[_0x3595c2(0xc35)]['stats'])_0xf73d8c['whipOut']=_0x435689[_0x3595c2(0xc35)]['stats'];else for(var _0x5bb1a3 in _0x435689[_0x3595c2(0x76f)]){if(_0x5bb1a3===_0x570b33)continue;_0xf73d8c[_0x5bb1a3]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)];}var _0x2a1d7b={};_0x2a1d7b['remoteStats']=_0xf73d8c,_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0xba7)in _0x42b9da){if(_0x42b9da[_0x3595c2(0xba7)]===_0x435689[_0x3595c2(0xba7)]&&_0x435689[_0x3595c2(0xba7)]||_0x435689['remote']===!![]){var _0xf73d8c={};if(_0x435689['whipOut'][_0x3595c2(0x5c2)])_0xf73d8c['whipOut']=_0x435689['whipOut'][_0x3595c2(0x5c2)];else for(var _0x5bb1a3 in _0x435689['pcs']){if(_0x5bb1a3===_0x570b33)continue;_0xf73d8c[_0x5bb1a3]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats'];}var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x514)]=_0xf73d8c,_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}}else{var _0xf73d8c={};if(_0x435689[_0x3595c2(0xc35)][_0x3595c2(0x5c2)])_0xf73d8c[_0x3595c2(0xc35)]=_0x435689['whipOut'][_0x3595c2(0x5c2)];else for(var _0x5bb1a3 in _0x435689['pcs']){if(_0x5bb1a3===_0x570b33)continue;if(!_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)])continue;if(_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x2ba)])continue;if(_0x435689[_0x3595c2(0xb0b)]){if('scene'in _0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats']){if(_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats'][_0x3595c2(0xa34)]===![])continue;}else continue;}_0xf73d8c[_0x5bb1a3]={},_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)]['video_bitrate_kbps']&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0x489)]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0x489)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)]['nacks_per_second']&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0xc44)]=_0x435689['pcs'][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0xc44)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0x924)]&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0x924)]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0x924)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)]['scene']&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0xa34)]=_0x435689['pcs'][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0xa34)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0xc69)]&&(_0xf73d8c[_0x5bb1a3]['label']=_0x435689['pcs'][_0x5bb1a3]['label']),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0x3e7)]&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0x3e7)]=_0x435689['pcs'][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0x3e7)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0xb5c)]&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0xb5c)]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0xb5c)]);}var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x514)]=_0xf73d8c,_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}}}if('requestStatsContinuous'in _0x42b9da){clearInterval(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x720)]);if(_0x435689[_0x3595c2(0xc19)]['indexOf'](_0x2d50f3||_0x570b33)>=0x0){if(_0x42b9da['requestStatsContinuous']){_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x720)]=setInterval(function(_0x2ed6bd){var _0x32d976=_0x3595c2,_0x585bb7={};if(_0x435689[_0x32d976(0xc35)][_0x32d976(0x5c2)])_0x585bb7[_0x32d976(0xc35)]=_0x435689[_0x32d976(0xc35)]['stats'];else for(var _0x4cbf69 in _0x435689[_0x32d976(0x76f)]){if(_0x4cbf69===_0x2ed6bd)continue;if(!_0x435689[_0x32d976(0x76f)][_0x4cbf69]['stats'])continue;if(_0x435689[_0x32d976(0x76f)][_0x4cbf69]['guest'])continue;_0x585bb7[_0x4cbf69]=_0x435689[_0x32d976(0x76f)][_0x4cbf69][_0x32d976(0x5c2)];}var _0xad6cfe={};_0xad6cfe[_0x32d976(0x514)]=_0x585bb7,_0x435689[_0x32d976(0x896)](_0xad6cfe,_0x2ed6bd);},0xbb8,_0x570b33);var _0xf73d8c={};if(_0x435689[_0x3595c2(0xc35)][_0x3595c2(0x5c2)])_0xf73d8c[_0x3595c2(0xc35)]=_0x435689[_0x3595c2(0xc35)][_0x3595c2(0x5c2)];else for(var _0x5bb1a3 in _0x435689['pcs']){if(_0x5bb1a3===_0x570b33)continue;if(!_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)])continue;if(_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['guest'])continue;_0xf73d8c[_0x5bb1a3]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)];}var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x514)]=_0xf73d8c,_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}}else{if('remote'in _0x42b9da){if(_0x42b9da[_0x3595c2(0xba7)]===_0x435689['remote']&&_0x435689[_0x3595c2(0xba7)]||_0x435689[_0x3595c2(0xba7)]===!![]){if(_0x42b9da[_0x3595c2(0x22c)]){_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x720)]=setInterval(function(_0x5003f2){var _0x5c7a97=_0x3595c2,_0x1dbdd9={};if(_0x435689[_0x5c7a97(0xc35)]['stats'])_0x1dbdd9[_0x5c7a97(0xc35)]=_0x435689['whipOut']['stats'];else for(var _0x57d642 in _0x435689[_0x5c7a97(0x76f)]){if(_0x57d642===_0x5003f2)continue;if(!_0x435689[_0x5c7a97(0x76f)][_0x57d642][_0x5c7a97(0x5c2)])continue;if(_0x435689[_0x5c7a97(0x76f)][_0x57d642]['guest'])continue;_0x1dbdd9[_0x57d642]=_0x435689[_0x5c7a97(0x76f)][_0x57d642][_0x5c7a97(0x5c2)];}var _0x360841={};_0x360841[_0x5c7a97(0x514)]=_0x1dbdd9,_0x435689[_0x5c7a97(0x896)](_0x360841,_0x5003f2);},0xbb8,_0x570b33);var _0xf73d8c={};if(_0x435689[_0x3595c2(0xc35)]['stats'])_0xf73d8c[_0x3595c2(0xc35)]=_0x435689[_0x3595c2(0xc35)]['stats'];else for(var _0x5bb1a3 in _0x435689[_0x3595c2(0x76f)]){if(_0x5bb1a3===_0x570b33)continue;if(!_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats'])continue;if(_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x2ba)])continue;_0xf73d8c[_0x5bb1a3]=_0x435689['pcs'][_0x5bb1a3][_0x3595c2(0x5c2)];}var _0x2a1d7b={};_0x2a1d7b['remoteStats']=_0xf73d8c,_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}}}else{if(_0x42b9da[_0x3595c2(0x22c)]){_0x435689['pcs'][_0x570b33][_0x3595c2(0x720)]=setInterval(function(_0x1e7935){var _0x44156d=_0x3595c2,_0x56fb92={};if(_0x435689[_0x44156d(0xc35)][_0x44156d(0x5c2)])_0x56fb92[_0x44156d(0xc35)]=_0x435689[_0x44156d(0xc35)][_0x44156d(0x5c2)];else for(var _0x1d18a2 in _0x435689[_0x44156d(0x76f)]){if(_0x1d18a2===_0x1e7935)continue;if(!_0x435689[_0x44156d(0x76f)][_0x1d18a2]['stats'])continue;if(_0x435689[_0x44156d(0x76f)][_0x1d18a2]['guest'])continue;if(_0x435689['roomid']){if(_0x44156d(0xa34)in _0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0x5c2)]){if(_0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0x5c2)][_0x44156d(0xa34)]===![])continue;}else continue;}_0x56fb92[_0x1d18a2]={},_0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0x5c2)]['video_bitrate_kbps']&&(_0x56fb92[_0x1d18a2][_0x44156d(0x489)]=_0x435689['pcs'][_0x1d18a2][_0x44156d(0x5c2)]['video_bitrate_kbps']),_0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0x5c2)][_0x44156d(0xc44)]&&(_0x56fb92[_0x1d18a2]['nacks_per_second']=_0x435689['pcs'][_0x1d18a2][_0x44156d(0x5c2)][_0x44156d(0xc44)]),_0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0x5c2)][_0x44156d(0x924)]&&(_0x56fb92[_0x1d18a2][_0x44156d(0x924)]=_0x435689[_0x44156d(0x76f)][_0x1d18a2]['stats'][_0x44156d(0x924)]),_0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0x5c2)]['scene']&&(_0x56fb92[_0x1d18a2][_0x44156d(0xa34)]=_0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0x5c2)][_0x44156d(0xa34)]),_0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0xc69)]&&(_0x56fb92[_0x1d18a2][_0x44156d(0xc69)]=_0x435689[_0x44156d(0x76f)][_0x1d18a2][_0x44156d(0xc69)]),_0x435689[_0x44156d(0x76f)][_0x1d18a2]['stats'][_0x44156d(0x3e7)]&&(_0x56fb92[_0x1d18a2][_0x44156d(0x3e7)]=_0x435689['pcs'][_0x1d18a2][_0x44156d(0x5c2)]['resolution']),_0x435689[_0x44156d(0x76f)][_0x1d18a2]['stats'][_0x44156d(0xb5c)]&&(_0x56fb92[_0x1d18a2]['video_encoder']=_0x435689['pcs'][_0x1d18a2][_0x44156d(0x5c2)][_0x44156d(0xb5c)]);}var _0xb4de05={};_0xb4de05[_0x44156d(0x514)]=_0x56fb92,_0x435689[_0x44156d(0x896)](_0xb4de05,_0x1e7935);},0xbb8,_0x570b33);var _0xf73d8c={};if(_0x435689[_0x3595c2(0xc35)]['stats'])_0xf73d8c[_0x3595c2(0xc35)]=_0x435689[_0x3595c2(0xc35)][_0x3595c2(0x5c2)];else for(var _0x5bb1a3 in _0x435689[_0x3595c2(0x76f)]){if(_0x5bb1a3===_0x570b33)continue;if(!_0x435689['pcs'][_0x5bb1a3]['stats'])continue;if(_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x2ba)])continue;if(_0x435689['roomid']){if(_0x3595c2(0xa34)in _0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats']){if(_0x435689['pcs'][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0xa34)]===![])continue;}else continue;}_0xf73d8c[_0x5bb1a3]={},_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0x489)]&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0x489)]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0x489)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)]['nacks_per_second']&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0xc44)]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats'][_0x3595c2(0xc44)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats'][_0x3595c2(0x924)]&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0x924)]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats']['available_outgoing_bitrate_kbps']),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0xa34)]&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0xa34)]=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats'][_0x3595c2(0xa34)]),_0x435689['pcs'][_0x5bb1a3][_0x3595c2(0xc69)]&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0xc69)]=_0x435689['pcs'][_0x5bb1a3][_0x3595c2(0xc69)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3]['stats'][_0x3595c2(0x3e7)]&&(_0xf73d8c[_0x5bb1a3][_0x3595c2(0x3e7)]=_0x435689['pcs'][_0x5bb1a3]['stats'][_0x3595c2(0x3e7)]),_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0xb5c)]&&(_0xf73d8c[_0x5bb1a3]['video_encoder']=_0x435689[_0x3595c2(0x76f)][_0x5bb1a3][_0x3595c2(0x5c2)][_0x3595c2(0xb5c)]);}var _0x2a1d7b={};_0x2a1d7b['remoteStats']=_0xf73d8c,_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}}}}if(_0x3595c2(0x3f6)in _0x42b9da)try{_0x435689[_0x3595c2(0x5bd)](_0x570b33,_0x42b9da[_0x3595c2(0x3f6)]['w'],_0x42b9da[_0x3595c2(0x3f6)]['h'],_0x42b9da[_0x3595c2(0x3f6)]['s'],_0x42b9da[_0x3595c2(0x3f6)]['c']);}catch(_0x6c1300){errorlog(_0x6c1300);}_0x3595c2(0x6e4)in _0x42b9da&&(_0x42b9da['scene']?_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0?_0x435689['sendKeyFrameScenes']():errorlog(_0x3595c2(0x73d)):_0x435689[_0x3595c2(0xbb5)](_0x570b33));if('chat'in _0x42b9da){var _0x37fcd5=![],_0x435c41=![];_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0&&(_0x37fcd5=!![],_0x3595c2(0x75f)in _0x42b9da&&(_0x42b9da[_0x3595c2(0x75f)]==!![]&&(_0x435c41=!![]))),log(_0x3595c2(0x6fe)+_0x37fcd5),getChatMessage(_0x42b9da[_0x3595c2(0x8a1)],_0x435689[_0x3595c2(0x76f)][_0x570b33]['label'],_0x37fcd5,_0x435c41,_0x570b33);}_0x3595c2(0xb20)in _0x42b9da&&(typeof processTipMessage===_0x3595c2(0x5f9)&&processTipMessage(_0x42b9da[_0x3595c2(0xb20)],_0x570b33));if(_0x3595c2(0x367)in _0x42b9da){_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x367)]=parseInt(_0x42b9da[_0x3595c2(0x367)])||0x0;_0x570b33 in _0x435689[_0x3595c2(0xc6c)]&&(_0x435689['rpcs'][_0x570b33][_0x3595c2(0x367)]=_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x367)]);if(_0x435689[_0x3595c2(0x781)]){var _0xfbac8f=document[_0x3595c2(0x689)](_0x3595c2(0x329)+_0x570b33+'\x22]');log(_0xfbac8f),_0xfbac8f[0x0]&&(_0xfbac8f[0x0][_0x3595c2(0x39e)]=parseInt(_0x42b9da[_0x3595c2(0x367)])||0x0);}updateMixer();}_0x3595c2(0x61b)in _0x42b9da&&_0x435689[_0x3595c2(0x125)](_0x570b33,_0x42b9da['scale']);if(_0x435689[_0x3595c2(0x781)]&&_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x908)]&&'directorState'in _0x42b9da){log(_0x42b9da),_0x435689[_0x3595c2(0x232)]=_0x42b9da['directorState'];for(var _0x10ceca in _0x435689[_0x3595c2(0x232)]){syncSceneState(_0x10ceca),syncOtherState(_0x10ceca),syncLabelState(_0x10ceca);if(_0x435689[_0x3595c2(0xb9e)]&&_0x435689[_0x3595c2(0x232)][_0x10ceca]&&_0x435689[_0x3595c2(0x232)][_0x10ceca][_0x3595c2(0x309)]&&_0x435689[_0x3595c2(0x232)][_0x10ceca][_0x3595c2(0x309)]['remove-queue']){var _0x252a00=![];for(var _0x5dbf99 in _0x435689[_0x3595c2(0xc6c)]){if(_0x435689[_0x3595c2(0xc6c)][_0x5dbf99]['streamID']===_0x10ceca){_0x69d88c(_0x5dbf99),_0x435689[_0x3595c2(0x20e)](_0x5dbf99),_0x252a00=!![];break;}}!_0x252a00&&!_0x435689['pendingApprovalStreamIDs']['includes'](_0x10ceca)&&_0x435689['pendingApprovalStreamIDs']['push'](_0x10ceca);}else{if(_0x435689[_0x3595c2(0x232)][_0x10ceca]&&_0x435689[_0x3595c2(0x232)][_0x10ceca]['others']&&!_0x435689[_0x3595c2(0x232)][_0x10ceca]['others'][_0x3595c2(0x3e8)]){var _0x56fbaf=_0x435689[_0x3595c2(0x2db)][_0x3595c2(0x97c)](_0x10ceca);_0x56fbaf>-0x1&&_0x435689[_0x3595c2(0x2db)][_0x3595c2(0x20c)](_0x56fbaf,0x1);}}}pokeAPI(_0x3595c2(0x708),_0x42b9da['directorState']);for(var _0x28da98 in _0x435689['pcs']){_0x435689['pcs'][_0x28da98]['coDirector']&&_0x28da98!==_0x570b33&&_0x435689[_0x3595c2(0x896)]({'directorState':_0x42b9da[_0x3595c2(0x3e3)]},_0x28da98);}}if(_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)==-0x1){if(_0x3595c2(0x26c)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x26c),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('requestVideoRecord'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x2bd),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x3d8)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b['rejected']=_0x3595c2(0x3d8),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if('changeURL'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x55a),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('changeLabel'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x696),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x606)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x606),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x86f)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]='requestChangeGating',_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x7ef)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x7ef),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('requestChangeSubGain'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0xa43),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('requestChangeMicPanning'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0xc03),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0xaf7)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]='remoteVideoMuted',_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('requestChangeMicDelay'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x38b),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0xbdd)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]='lowerhand',_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0xbb2)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]='hangup',_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('displayMute'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b['rejected']='displayMute',_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x8f4)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x8f4),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('volume'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0xa7e),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x81d)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x81d),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x435)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x435),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x31c)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x31c),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('resumeClock'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0xa23),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x4f4)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b['rejected']=_0x3595c2(0x4f4),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0xb5f)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0xb5f),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x349)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x349),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x16c)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x16c),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x963)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]='pauseClock',_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x4d7)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x4d7),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x1cd)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x1cd),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('rotate'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0xc40),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if('refreshMicrophone'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x284),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0xbdc)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0xbdc),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x670)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b['rejected']='getConnectionMap',_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if('refreshVideo'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0x820),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if('refreshConnection'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]='refreshConnection',_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0xa4a)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x592)]=_0x3595c2(0xa4a),_0x435689['sendMessage'](_0x2a1d7b,_0x570b33);}else{if(_0x3595c2(0x726)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b['rejected']=_0x3595c2(0x726),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x570b33);}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}else{if(_0x3595c2(0x26c)in _0x42b9da){var _0x55d5b1=_0x435689['streamSrc'][_0x3595c2(0x253)]();_0x55d5b1[_0x3595c2(0xb04)]&&(_0x3595c2(0xb07)in _0x42b9da?applyAudioHack(_0x42b9da['keyname'],_0x42b9da[_0x3595c2(0x62d)],_0x42b9da[_0x3595c2(0xb07)]):applyAudioHack(_0x42b9da[_0x3595c2(0x651)],_0x42b9da[_0x3595c2(0x62d)]));}if('requestVideoRecord'in _0x42b9da){if(_0x42b9da[_0x3595c2(0x2bd)]){_0x42b9da['googleDriveRecord']&&(_0x435689[_0x3595c2(0x7e3)]={},_0x435689['gdrive'][_0x3595c2(0x2d1)]=_0x42b9da[_0x3595c2(0xb21)]);if(_0x435689['videoElement']){var _0x1e3a6e=0x1770;if(_0x42b9da[_0x3595c2(0x12f)])_0x1e3a6e=_0x42b9da['recordConfig'];else _0x42b9da[_0x3595c2(0x62d)]&&(_0x1e3a6e=parseInt(_0x42b9da['value']));recordLocalVideo(_0x3595c2(0x8ed),_0x1e3a6e,![],_0x42b9da['altUUID']||![]);}}else _0x435689['videoElement']&&recordLocalVideo(_0x3595c2(0xaef),![],![],_0x42b9da[_0x3595c2(0xc70)]||![]);}if(_0x3595c2(0x3d8)in _0x42b9da){_0x435689['order']==![]&&(_0x435689[_0x3595c2(0x367)]=0x0);_0x435689[_0x3595c2(0x367)]+=parseInt(_0x42b9da[_0x3595c2(0x3d8)])||0x0;var _0x2a1d7b={};_0x2a1d7b={},_0x2a1d7b[_0x3595c2(0x367)]=_0x435689[_0x3595c2(0x367)],_0x435689['sendPeers'](_0x2a1d7b),updateMixer();}if('requestChangeMicPanning'in _0x42b9da){var _0x299020=_0x435689[_0x3595c2(0xbe5)];if(_0x42b9da[_0x3595c2(0x62d)]===_0x3595c2(0x914))_0x435689[_0x3595c2(0xbe5)]=![];else{var _0x23812d=parseInt(_0x42b9da['value']);isNaN(_0x23812d)&&(_0x23812d=0x5a);if(_0x23812d<0x0)_0x23812d=0x0;if(_0x23812d>0xb4)_0x23812d=0xb4;_0x435689[_0x3595c2(0xbe5)]=_0x23812d;}if(_0x435689[_0x3595c2(0xbe5)]!==![])try{_0x435689['disableWebAudio']=![];}catch(_0xcccab6){}if(_0x299020===![]&&_0x435689['micPanning']!==![]||_0x299020!==![]&&_0x435689[_0x3595c2(0xbe5)]===![]){try{_0x435689[_0x3595c2(0x917)]['srcObject']=outboundAudioPipeline();}catch(_0x236374){errorlog(_0x236374);}senderAudioUpdate();}else _0x435689[_0x3595c2(0xbe5)]!==![]&&changeMicPanning(_0x435689[_0x3595c2(0xbe5)],_0x42b9da['track']);}'changeURL'in _0x42b9da&&changeURL(_0x42b9da[_0x3595c2(0x55a)]);if(_0x3595c2(0xc40)in _0x42b9da){if(_0x42b9da[_0x3595c2(0xc40)]===!![])_0x435689[_0x3595c2(0xc40)]===![]?_0x435689[_0x3595c2(0xc40)]=0x5a:_0x435689[_0x3595c2(0xc40)]+=0x5a,_0x435689['rotate']>=0x168&&(_0x435689['rotate']-=0x168),_0x435689[_0x3595c2(0xc40)]===0x0&&(_0x435689[_0x3595c2(0xc40)]=![]);else _0x42b9da[_0x3595c2(0xc40)]===!![]?_0x435689['rotate']=![]:_0x435689[_0x3595c2(0xc40)]=parseInt(_0x42b9da['rotate'])||![];updateForceRotate(),updateMixer();}'stopClock'in _0x42b9da&&stopClock();_0x3595c2(0xa23)in _0x42b9da&&resumeClock();_0x3595c2(0x4f4)in _0x42b9da&&setClock(_0x42b9da['setClock']);_0x3595c2(0xb5f)in _0x42b9da&&hideClock();_0x3595c2(0x349)in _0x42b9da&&showClock();_0x3595c2(0x16c)in _0x42b9da&&startClock();_0x3595c2(0x963)in _0x42b9da&&pauseClock();if(_0x3595c2(0x4d7)in _0x42b9da){if(_0x435689[_0x3595c2(0x4d7)]!==![]){if(_0x42b9da[_0x3595c2(0x4d7)]&&!_0x435689[_0x3595c2(0x4d7)])toggleClock(_0x42b9da[_0x3595c2(0x58b)]||![]);else!_0x42b9da[_0x3595c2(0x4d7)]&&_0x435689[_0x3595c2(0x4d7)]&&toggleClock(_0x42b9da[_0x3595c2(0x58b)]||![]);}}'requestUpload'in _0x42b9da&&toggleFileshare(_0x570b33);if(_0x3595c2(0x1cd)in _0x42b9da)try{_0x2d50f3?(_0x42b9da[_0x3595c2(0x1cd)]?_0x435689[_0x3595c2(0xb94)]=_0x42b9da[_0x3595c2(0x1cd)][_0x3595c2(0x9fe)](','):_0x435689['group_alt']=[],_0x435689[_0x3595c2(0x896)]({'group':_0x42b9da[_0x3595c2(0x1cd)],'altUUID':!![]})):(_0x42b9da['group']?_0x435689[_0x3595c2(0x1cd)]=_0x42b9da[_0x3595c2(0x1cd)][_0x3595c2(0x9fe)](','):_0x435689[_0x3595c2(0x1cd)]=[],_0x435689[_0x3595c2(0x896)]({'group':_0x42b9da['group']})),updateMixer(),pokeIframeAPI(_0x3595c2(0xa0d),_0x435689['group']);}catch(_0x1d4273){}if('changeLabel'in _0x42b9da){if('value'in _0x42b9da){if(typeof _0x42b9da[_0x3595c2(0x62d)]==_0x3595c2(0x332)){_0x435689[_0x3595c2(0xc69)]=sanitizeLabel(_0x42b9da['value']),log(_0x3595c2(0xa10)+_0x435689[_0x3595c2(0xc69)]);if(_0x435689['director']){var _0xfbac8f=getById(_0x3595c2(0x52e)+_0x570b33);if(_0x435689[_0x3595c2(0xc69)])_0xfbac8f['innerText']=_0x435689[_0x3595c2(0xc69)],_0xfbac8f[_0x3595c2(0xab1)][_0x3595c2(0xae4)](_0x3595c2(0x339));else _0x435689[_0x3595c2(0xb2f)]===(_0x2d50f3||_0x570b33)?(miniTranslate(_0xfbac8f['innerHTML'],_0x3595c2(0x716)),_0xfbac8f[_0x3595c2(0xab1)]['remove'](_0x3595c2(0x339))):(miniTranslate(_0xfbac8f['innerHTML'],_0x3595c2(0x3f5)),_0xfbac8f[_0x3595c2(0xab1)][_0x3595c2(0x701)](_0x3595c2(0x339)));}else _0x435689['showlabels']&&updateMixer();!_0x435689[_0x3595c2(0x781)]&&(_0x435689[_0x3595c2(0xc69)]?document[_0x3595c2(0x967)]=_0x435689['label']:document[_0x3595c2(0x967)]=location[_0x3595c2(0x879)]);var _0x39a57f=encodeURIComponent(_0x435689[_0x3595c2(0xc69)]);urlParams[_0x3595c2(0x1a5)]('l')?updateURL('l='+_0x39a57f,!![],![]):updateURL(_0x3595c2(0x49f)+_0x39a57f,!![],![]);var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x696)]=!![],_0x2a1d7b['value']=_0x435689[_0x3595c2(0xc69)],_0x435689[_0x3595c2(0x896)](_0x2a1d7b);}else{_0x435689[_0x3595c2(0xc69)]=![];var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x696)]=!![],_0x2a1d7b[_0x3595c2(0x62d)]=_0x435689[_0x3595c2(0xc69)],_0x435689[_0x3595c2(0x896)](_0x2a1d7b);if(_0x435689[_0x3595c2(0x781)]){var _0xfbac8f=getById(_0x3595c2(0x52e)+_0x570b33);_0x435689['directorUUID']===(_0x2d50f3||_0x570b33)?(miniTranslate(_0xfbac8f['innerHTML'],_0x3595c2(0x716)),_0xfbac8f[_0x3595c2(0xab1)][_0x3595c2(0xae4)]('addALabel')):(miniTranslate(_0xfbac8f[_0x3595c2(0x5be)],_0x3595c2(0x3f5)),_0xfbac8f[_0x3595c2(0xab1)][_0x3595c2(0x701)](_0x3595c2(0x339)));}else _0x435689[_0x3595c2(0xba3)]?(document['title']=location[_0x3595c2(0x879)],updateMixer()):document['title']=location[_0x3595c2(0x879)];}}}if(_0x3595c2(0x606)in _0x42b9da){if(_0x42b9da['keyname']=='low')changeLowEQ(parseFloat(_0x42b9da[_0x3595c2(0x62d)]),_0x42b9da[_0x3595c2(0xb26)]);else{if(_0x42b9da['keyname']==_0x3595c2(0x370))changeMidEQ(parseFloat(_0x42b9da[_0x3595c2(0x62d)]),_0x42b9da[_0x3595c2(0xb26)]);else _0x42b9da['keyname']==_0x3595c2(0xb31)&&changeHighEQ(parseFloat(_0x42b9da[_0x3595c2(0x62d)]),_0x42b9da[_0x3595c2(0xb26)]);}}if(_0x3595c2(0x86f)in _0x42b9da){var _0x408ec1=_0x435689[_0x3595c2(0x529)];if(_0x42b9da[_0x3595c2(0x62d)]===_0x3595c2(0x914))_0x435689[_0x3595c2(0x529)]=![],log(_0x3595c2(0x6bc));else _0x42b9da['value']===_0x3595c2(0x95a)?(_0x435689[_0x3595c2(0x529)]=!![],log(_0x3595c2(0xc06))):_0x435689[_0x3595c2(0x529)]=_0x42b9da[_0x3595c2(0x62d)];_0x435689[_0x3595c2(0x529)]!==_0x408ec1&&senderAudioUpdate();}if(_0x3595c2(0x7ef)in _0x42b9da){var _0x408ec1=_0x435689['compressor'];if(_0x42b9da[_0x3595c2(0x62d)]===_0x3595c2(0x914))_0x435689[_0x3595c2(0x486)]=![],log('noise\x20gate\x20off');else{if(_0x42b9da[_0x3595c2(0x62d)]==='1')_0x435689[_0x3595c2(0x486)]=0x1,log(_0x3595c2(0xc06));else _0x42b9da[_0x3595c2(0x62d)]==='2'?(_0x435689[_0x3595c2(0x486)]=0x2,log('noise\x20gate\x20on')):_0x435689[_0x3595c2(0x486)]=parseInt(_0x42b9da[_0x3595c2(0x62d)])||![];}_0x435689[_0x3595c2(0x486)]!==_0x408ec1&&senderAudioUpdate();}if(_0x3595c2(0xc03)in _0x42b9da){var _0x299020=_0x435689['micPanning'];if(_0x42b9da[_0x3595c2(0x62d)]===_0x3595c2(0x914))_0x435689[_0x3595c2(0xbe5)]=![];else{var _0x23812d=parseInt(_0x42b9da['value']);isNaN(_0x23812d)&&(_0x23812d=0x5a);if(_0x23812d<0x0)_0x23812d=0x0;if(_0x23812d>0xb4)_0x23812d=0xb4;_0x435689[_0x3595c2(0xbe5)]=_0x23812d;}if(_0x299020===![]&&_0x435689[_0x3595c2(0xbe5)]!==![]||_0x299020!==![]&&_0x435689[_0x3595c2(0xbe5)]===![]){try{_0x435689[_0x3595c2(0x917)][_0x3595c2(0x200)]=outboundAudioPipeline();}catch(_0x5ebb03){errorlog(_0x5ebb03);}senderAudioUpdate();}else _0x435689[_0x3595c2(0xbe5)]!==![]&&changeMicPanning(_0x435689['micPanning'],_0x42b9da['track']);}'requestChangeMicDelay'in _0x42b9da&&(_0x435689['micDelay']===![]?(_0x435689[_0x3595c2(0x977)]=parseInt(_0x42b9da['value'])||0x0,senderAudioUpdate()):(_0x435689[_0x3595c2(0x977)]=parseInt(_0x42b9da[_0x3595c2(0x62d)])||0x0,changeMicDelay(_0x435689[_0x3595c2(0x977)],_0x42b9da['track'])));_0x3595c2(0xa43)in _0x42b9da&&changeSubGain(parseFloat(_0x42b9da['value']),_0x42b9da[_0x3595c2(0xb07)]);_0x3595c2(0xbdd)in _0x42b9da&&(_0x435689[_0x3595c2(0x5a6)]&&lowerhand());if(_0x3595c2(0x66c)in _0x42b9da&&'mirrorGuestTarget'in _0x42b9da){if(_0x42b9da['mirrorGuestTarget']&&_0x42b9da[_0x3595c2(0x2d0)]===!![])_0x435689[_0x3595c2(0xb47)]=_0x42b9da['mirrorGuestState'],_0x435689['mirrorOutput']=_0x42b9da['mirrorGuestState'],applyMirror(_0x435689[_0x3595c2(0x945)]);else _0x42b9da[_0x3595c2(0x2d0)]&&_0x42b9da[_0x3595c2(0x2d0)]in _0x435689[_0x3595c2(0xc6c)]&&(_0x435689[_0x3595c2(0xc6c)][_0x42b9da[_0x3595c2(0x2d0)]]['mirrorState']=_0x42b9da['mirrorGuestState'],_0x435689['rpcs'][_0x42b9da[_0x3595c2(0x2d0)]][_0x3595c2(0x917)]&&applyMirrorGuest(_0x42b9da[_0x3595c2(0x66c)],_0x435689[_0x3595c2(0xc6c)][_0x42b9da[_0x3595c2(0x2d0)]][_0x3595c2(0x917)],_0x435689[_0x3595c2(0xc6c)][_0x42b9da[_0x3595c2(0x2d0)]][_0x3595c2(0x1a4)]));}if(_0x3595c2(0x2a3)in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x9d6)]=_0x570b33,_0x2a1d7b['audioOptions']=listAudioSettingsPrep(),sendMediaDevices(_0x2a1d7b['UUID']),_0x435689['sendMessage'](_0x2a1d7b,_0x2a1d7b[_0x3595c2(0x9d6)]);}if('getVideoSettings'in _0x42b9da){var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x9d6)]=_0x570b33,_0x2a1d7b[_0x3595c2(0x69d)]=listVideoSettingsPrep(),sendMediaDevices(_0x2a1d7b['UUID']),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x2a1d7b['UUID']);}_0x3595c2(0x3d3)in _0x42b9da&&changeAudioOutputDeviceById(_0x42b9da['changeSpeaker'],_0x570b33);_0x3595c2(0xbdc)in _0x42b9da&&changeAudioDeviceById(_0x42b9da[_0x3595c2(0xbdc)],_0x570b33);'refreshMicrophone'in _0x42b9da&&refreshMicrophoneDevice(_0x570b33);_0x3595c2(0x820)in _0x42b9da&&refreshVideoDevice(_0x570b33);if('refreshConnection'in _0x42b9da){for(var _0x45cab7 in _0x435689['pcs']){if(_0x435689[_0x3595c2(0x76f)][_0x45cab7]&&_0x435689[_0x3595c2(0x76f)][_0x45cab7][_0x3595c2(0x495)])try{_0x435689[_0x3595c2(0x76f)][_0x45cab7][_0x3595c2(0x495)](),log(_0x3595c2(0x6ee)+_0x45cab7);}catch(_0x60d05d){warnlog(_0x3595c2(0x3b1)+_0x60d05d);}}for(var _0x45cab7 in _0x435689[_0x3595c2(0xc6c)]){if(_0x435689[_0x3595c2(0xc6c)][_0x45cab7]&&_0x435689[_0x3595c2(0xc6c)][_0x45cab7][_0x3595c2(0x495)])try{_0x435689[_0x3595c2(0xc6c)][_0x45cab7][_0x3595c2(0x495)](),log(_0x3595c2(0xb6f)+_0x45cab7);}catch(_0x6e417d){warnlog('ICE\x20restart\x20failed\x20for\x20rpcs:\x20'+_0x6e417d);}}}'refreshAll'in _0x42b9da&&(refreshMicrophoneDevice(_0x570b33),setTimeout(function(){refreshVideoDevice(_0x570b33);},0x1f4),setTimeout(function(){var _0x44ba68=_0x3595c2;for(var _0x3c88c6 in _0x435689[_0x44ba68(0x76f)]){if(_0x435689[_0x44ba68(0x76f)][_0x3c88c6]&&_0x435689[_0x44ba68(0x76f)][_0x3c88c6][_0x44ba68(0x495)])try{_0x435689[_0x44ba68(0x76f)][_0x3c88c6][_0x44ba68(0x495)]();}catch(_0x23b21c){}}for(var _0x3c88c6 in _0x435689[_0x44ba68(0xc6c)]){if(_0x435689[_0x44ba68(0xc6c)][_0x3c88c6]&&_0x435689[_0x44ba68(0xc6c)][_0x3c88c6]['restartIce'])try{_0x435689['rpcs'][_0x3c88c6][_0x44ba68(0x495)]();}catch(_0x58e67f){}}},0x3e8));if(_0x3595c2(0xae7)in _0x42b9da){if(_0x435689[_0x3595c2(0x1c1)])log('Restarting\x20WHIP\x20connection\x20per\x20director\x20request'),_0x435689[_0x3595c2(0x1c1)]();else{if(_0x435689['whipOut']){log(_0x3595c2(0x7f5));try{_0x435689[_0x3595c2(0xc35)]['close'](),_0x435689[_0x3595c2(0xc35)]=null;}catch(_0x4cdcb2){warnlog(_0x4cdcb2);}}else log(_0x3595c2(0x839));}}if(_0x3595c2(0x726)in _0x42b9da){var _0x189be0=_0x42b9da[_0x3595c2(0x726)];if(_0x189be0){if(_0x435689[_0x3595c2(0x76f)][_0x189be0])log(_0x3595c2(0x6ea)+_0x189be0),_0x435689[_0x3595c2(0x974)](_0x189be0);else _0x435689[_0x3595c2(0xc6c)][_0x189be0]&&(log(_0x3595c2(0x14c)+_0x189be0),_0x435689['closeRPC'](_0x189be0));}}if(_0x3595c2(0x670)in _0x42b9da){var _0x156707=_0x3595c2(0xb1c);if(SafariVersion)_0x156707='Safari\x20'+SafariVersion;else{if(typeof getChromiumVersion===_0x3595c2(0x5f9)&&getChromiumVersion()>0x3c)_0x156707=_0x3595c2(0xb89)+getChromiumVersion();else{if(Firefox)_0x156707=_0x3595c2(0x6f2);else navigator[_0x3595c2(0x19a)]['indexOf'](_0x3595c2(0x6d5))>=0x0&&(_0x156707='Chrome\x20iOS');}}var _0x474b2f={'uuid':_0x435689[_0x3595c2(0x9d6)],'streamID':_0x435689['streamID'],'label':_0x435689[_0x3595c2(0xc69)]||_0x435689[_0x3595c2(0x885)]||_0x3595c2(0x9a5),'browser':_0x156707,'connections':[]};for(var _0x45cab7 in _0x435689[_0x3595c2(0x76f)]){if(_0x435689[_0x3595c2(0x76f)][_0x45cab7]){var _0x26aca9=_0x435689[_0x3595c2(0x76f)][_0x45cab7],_0x13939e={'peerUUID':_0x45cab7,'peerStreamID':_0x26aca9[_0x3595c2(0x885)]||_0x45cab7,'direction':'outgoing','state':_0x26aca9[_0x3595c2(0x8e8)]||'unknown','bandwidth':-0x1,'audioEnabled':!_0x435689[_0x3595c2(0xb62)],'videoEnabled':!_0x435689['videoMuted'],'nackCount':0x0,'pliCount':0x0,'candidateType':_0x26aca9[_0x3595c2(0x5c2)]?_0x26aca9['stats'][_0x3595c2(0x71a)]||'unknown':'unknown'};_0x26aca9['stats']&&(_0x13939e[_0x3595c2(0xa6a)]=_0x26aca9[_0x3595c2(0x5c2)]['nackCount']||0x0,_0x13939e[_0x3595c2(0x29b)]=_0x26aca9[_0x3595c2(0x5c2)][_0x3595c2(0x29b)]||0x0),_0x474b2f[_0x3595c2(0x376)][_0x3595c2(0x35c)](_0x13939e);}}for(var _0x45cab7 in _0x435689[_0x3595c2(0xc6c)]){if(_0x435689[_0x3595c2(0xc6c)][_0x45cab7]){var _0x1f62e7=_0x435689[_0x3595c2(0xc6c)][_0x45cab7],_0x185710='unknown';_0x1f62e7[_0x3595c2(0x5c2)]&&_0x1f62e7[_0x3595c2(0x5c2)]['Peer-to-Peer_Connection']&&(_0x185710=_0x1f62e7[_0x3595c2(0x5c2)][_0x3595c2(0x63c)][_0x3595c2(0x71a)]||_0x3595c2(0xbd0));var _0x13939e={'peerUUID':_0x45cab7,'peerStreamID':_0x1f62e7[_0x3595c2(0x885)]||_0x45cab7,'direction':_0x3595c2(0x6e9),'state':_0x1f62e7[_0x3595c2(0x8e8)]||_0x3595c2(0xbd0),'bandwidth':_0x1f62e7[_0x3595c2(0x263)]||-0x1,'audioEnabled':!_0x1f62e7['remoteMuteState'],'videoEnabled':!_0x1f62e7['videoMuted'],'nackCount':_0x1f62e7[_0x3595c2(0xa6a)]||0x0,'pliCount':_0x1f62e7[_0x3595c2(0x29b)]||0x0,'candidateType':_0x185710};_0x474b2f[_0x3595c2(0x376)][_0x3595c2(0x35c)](_0x13939e);}}_0x474b2f['requesterUUID']=_0x570b33,_0x435689[_0x3595c2(0x896)]({'connectionMap':_0x474b2f},_0x570b33),log(_0x3595c2(0x7ae)+_0x474b2f[_0x3595c2(0x376)]['length']+_0x3595c2(0xb55));}_0x3595c2(0x83b)in _0x42b9da&&changeVideoDeviceById(_0x42b9da[_0x3595c2(0x83b)],_0x570b33);_0x3595c2(0xc05)in _0x42b9da&&changeLowCut(parseFloat(_0x42b9da[_0x3595c2(0x62d)]),_0x42b9da['track']);_0x3595c2(0xc05)in _0x42b9da&&changeLowCut(parseFloat(_0x42b9da[_0x3595c2(0x62d)]),_0x42b9da[_0x3595c2(0xb26)]);if(_0x3595c2(0xbb2)in _0x42b9da){if(_0x435689[_0x3595c2(0xb2f)]){if(_0x42b9da[_0x3595c2(0x59f)]&&_0x435689['roomid'])try{var _0x330bfb=_0x3595c2(0x41a)+sanitizeRoomName(_0x435689['roomid'])+'_'+(_0x435689[_0x3595c2(0x21b)]||'');setStorage(_0x330bfb,!![],0x4),log(_0x3595c2(0xae9));}catch(_0x464737){errorlog('Failed\x20to\x20store\x20block:\x20'+_0x464737);}_0x435689[_0x3595c2(0xbb2)]();}}if('mute'in _0x42b9da){}if(_0x3595c2(0xa7e)in _0x42b9da){var _0x4d6e9d=parseInt(_0x42b9da['volume'])/0x64||0x0;_0x435689[_0x3595c2(0x13a)]=parseInt(_0x42b9da[_0x3595c2(0xa7e)])||0x0;try{for(var _0x48a886 in _0x435689['webAudios']){log(_0x3595c2(0xbfb)),_0x435689['webAudios'][_0x48a886][_0x3595c2(0x2c0)][_0x3595c2(0x5f4)][_0x3595c2(0x26d)](_0x4d6e9d,_0x435689[_0x3595c2(0xc4e)][_0x48a886][_0x3595c2(0x6f8)][_0x3595c2(0x1cc)]);}}catch(_0x215121){}updateVolume(!![]);}if('micIsolate'in _0x42b9da){if(_0x42b9da[_0x3595c2(0x99c)])_0x435689[_0x3595c2(0xc19)]['indexOf'](_0x2d50f3||_0x570b33)>=0x0&&(_0x435689[_0x3595c2(0x81d)][_0x3595c2(0x35c)](_0x570b33),_0x435689[_0x3595c2(0x424)]());else{var _0x58bb4e=_0x435689[_0x3595c2(0x81d)][_0x3595c2(0x97c)](_0x570b33);_0x58bb4e>-0x1&&(_0x435689[_0x3595c2(0x81d)][_0x3595c2(0x20c)](_0x58bb4e,0x1),_0x435689['applyIsolatedChat']());}}if(_0x3595c2(0xc53)in _0x42b9da){if(_0x42b9da[_0x3595c2(0xc53)])_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0&&(_0x435689[_0x3595c2(0xc53)]['push'](_0x570b33),_0x435689[_0x3595c2(0x561)]());else{var _0x58bb4e=_0x435689[_0x3595c2(0xc53)][_0x3595c2(0x97c)](_0x570b33);_0x58bb4e>-0x1&&(_0x435689[_0x3595c2(0xc53)][_0x3595c2(0x20c)](_0x58bb4e,0x1),_0x435689[_0x3595c2(0x561)]());}}_0x3595c2(0x8f4)in _0x42b9da&&(_0x42b9da['speakerMute']?(_0x435689[_0x3595c2(0x8f9)]=!![],_0x435689['directorSpeakerMute']()):(_0x435689[_0x3595c2(0x8f9)]=![],_0x435689[_0x3595c2(0x9e2)]()));_0x3595c2(0x37b)in _0x42b9da&&(_0x42b9da[_0x3595c2(0x37b)]?(_0x435689[_0x3595c2(0x310)]=!![],_0x435689[_0x3595c2(0xb58)]()):(_0x435689['directorDisplayMuted']=![],_0x435689[_0x3595c2(0xb58)]()));if(_0x3595c2(0xaf7)in _0x42b9da){_0x435689['remoteVideoMuted']=_0x42b9da[_0x3595c2(0xaf7)],toggleVideoMute(!![]);if(!_0x435689[_0x3595c2(0xc02)]){var _0x2a1d7b={};_0x2a1d7b['videoMuted']=_0x435689[_0x3595c2(0xaf7)],_0x435689['sendMessage'](_0x2a1d7b);}}_0x3595c2(0x44a)in _0x42b9da&&applyNewParams(_0x42b9da['changeParams']);}if(_0x435689['directorUUID']===(_0x2d50f3||_0x570b33)){if(_0x42b9da[_0x3595c2(0xaa1)]==='migrate'){warnlog(_0x3595c2(0x8e1));if(_0x3595c2(0x8fa)in _0x42b9da){_0x3595c2(0xc13)in _0x42b9da[_0x3595c2(0x8fa)]&&(_0x435689['roomenc']=_0x42b9da[_0x3595c2(0xc13)]);'broadcast'in _0x42b9da[_0x3595c2(0x8fa)]&&(_0x42b9da[_0x3595c2(0x8fa)]['broadcast']===!![]||_0x42b9da['transferSettings'][_0x3595c2(0x532)]===null?(_0x435689['broadcast']=null,_0x435689[_0x3595c2(0xaaa)]===![]&&(_0x435689['minipreview']=0x2),_0x435689['style']===![]&&(_0x435689[_0x3595c2(0x8d0)]=0x1),_0x435689['showList']===null&&(_0x435689[_0x3595c2(0x899)]=!![])):_0x435689[_0x3595c2(0x532)]=_0x42b9da['transferSettings'][_0x3595c2(0x532)],_0x42b9da[_0x3595c2(0x8fa)][_0x3595c2(0x725)]&&(_0x435689[_0x3595c2(0x532)]!==![]?_0x435689['broadcast']===null?updateURL(_0x3595c2(0x532),!![]):updateURL('broadcast='+_0x435689[_0x3595c2(0x532)],!![]):updateURL(_0x3595c2(0xabf),!![])));'roomid'in _0x42b9da[_0x3595c2(0x8fa)]&&(_0x435689[_0x3595c2(0xb0b)]=_0x42b9da[_0x3595c2(0x8fa)][_0x3595c2(0xb0b)],_0x42b9da['transferSettings'][_0x3595c2(0x725)]&&updateURL(_0x3595c2(0x9d1)+_0x435689['roomid'],!![]));_0x3595c2(0x8f5)in _0x42b9da['transferSettings']&&(_0x435689[_0x3595c2(0x8f5)]=_0x42b9da[_0x3595c2(0x8fa)][_0x3595c2(0x8f5)],_0x435689[_0x3595c2(0x8f5)]?!(_0x3595c2(0x7c3)in _0x42b9da[_0x3595c2(0x8fa)])&&(_0x435689['queueType']=0x2):_0x435689[_0x3595c2(0x7c3)]=![],_0x42b9da[_0x3595c2(0x8fa)][_0x3595c2(0x725)]&&(_0x435689[_0x3595c2(0x8f5)]?updateURL(_0x3595c2(0x8f5),!![]):updateURL(_0x3595c2(0x746),!![])));if(_0x3595c2(0x7c3)in _0x42b9da[_0x3595c2(0x8fa)])try{_0x435689['queue']=!![],_0x435689[_0x3595c2(0x7c3)]=parseInt(_0x42b9da[_0x3595c2(0x8fa)][_0x3595c2(0x7c3)])||0x3;}catch(_0x4443fd){errorlog(_0x4443fd);}_0x3595c2(0x7c0)in _0x42b9da[_0x3595c2(0x8fa)]&&(_0x435689[_0x3595c2(0x8f5)]&&(_0x435689[_0x3595c2(0x8f5)]=_0x435689[_0x3595c2(0x7c3)]||0x3,_0x42b9da[_0x3595c2(0x8fa)][_0x3595c2(0x725)]&&updateURL(_0x3595c2(0x746),!![])));}_0x435689['waitPage']&&_0x435689[_0x3595c2(0xb50)]&&(_0x435689[_0x3595c2(0xb50)]=![],_0x435689[_0x3595c2(0x170)]=![],updateMixer());}try{if(_0x3595c2(0xb41)in _0x42b9da&&_0x3595c2(0x52a)in _0x42b9da[_0x3595c2(0xb41)]){var _0x1fb1d7=_0x2d50f3||_0x570b33;if(_0x435689[_0x3595c2(0xb2f)]&&_0x1fb1d7===_0x435689[_0x3595c2(0xb2f)])for(var _0x5bff68=0x0;_0x5bff68<_0x42b9da[_0x3595c2(0xb41)]['addCoDirector'][_0x3595c2(0xb04)];_0x5bff68++){var _0x28da98=_0x42b9da[_0x3595c2(0xb41)][_0x3595c2(0x52a)][_0x5bff68]['toString']();if(!_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x49c)](_0x28da98)){_0x435689['directorList'][_0x3595c2(0x35c)](_0x28da98);var _0x2005e3=getById(_0x3595c2(0x852)+_0x28da98);_0x2005e3&&_0x2005e3['classList'][_0x3595c2(0x701)](_0x3595c2(0x56f));}_0x28da98 in _0x435689['pcs']&&_0x435689[_0x3595c2(0x76f)][_0x28da98][_0x3595c2(0xc12)]&&_0x435689[_0x3595c2(0x7c3)]==0x4&&_0x435689[_0x3595c2(0x503)](_0x28da98);if(_0x28da98 in _0x435689[_0x3595c2(0x76f)])try{var _0x2a1d7b={};_0x2a1d7b[_0x3595c2(0x9d6)]=_0x28da98,_0x2a1d7b[_0x3595c2(0x4c0)]=listAudioSettingsPrep(),sendMediaDevices(_0x2a1d7b['UUID']),_0x435689[_0x3595c2(0x896)](_0x2a1d7b,_0x2a1d7b[_0x3595c2(0x9d6)]);}catch(_0x4e4007){}}}}catch(_0x5dbd1b){errorlog(_0x5dbd1b);}if(_0x3595c2(0x7c5)in _0x42b9da)try{_0x435689[_0x3595c2(0x896)]({'cbid':_0x42b9da[_0x3595c2(0x7c5)]},_0x570b33);}catch(_0x2b0ebf){errorlog(_0x2b0ebf);}}if('requestVideoHack'in _0x42b9da){if(_0x435689['directorList'][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0||_0x435689[_0x3595c2(0xba7)]===!![]||_0x435689[_0x3595c2(0xba7)]&&_0x3595c2(0xba7)in _0x42b9da&&_0x42b9da['remote']===_0x435689[_0x3595c2(0xba7)])_0x3595c2(0x487)in _0x42b9da&&_0x42b9da['ctrl']?updateCameraConstraints(_0x42b9da[_0x3595c2(0x651)],_0x42b9da[_0x3595c2(0x62d)],!![],_0x570b33):updateCameraConstraints(_0x42b9da['keyname'],_0x42b9da[_0x3595c2(0x62d)],![],![]);else return;}if(_0x3595c2(0x647)in _0x42b9da){if(_0x435689['directorList'][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0||_0x435689[_0x3595c2(0xba7)]===!![]||_0x435689[_0x3595c2(0xba7)]&&'remote'in _0x42b9da&&_0x42b9da[_0x3595c2(0xba7)]===_0x435689[_0x3595c2(0xba7)])_0x435689[_0x3595c2(0xbde)](parseFloat(_0x42b9da[_0x3595c2(0x647)]),_0x42b9da[_0x3595c2(0x92b)]||![]);else return;}if(_0x3595c2(0x296)in _0x42b9da){if(_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0||_0x435689['remote']===!![]||_0x435689[_0x3595c2(0xba7)]&&_0x3595c2(0xba7)in _0x42b9da&&_0x42b9da[_0x3595c2(0xba7)]===_0x435689[_0x3595c2(0xba7)])_0x435689[_0x3595c2(0x7ed)](parseFloat(_0x42b9da['focus']),_0x42b9da[_0x3595c2(0x92b)]||![]);else return;}if('autofocus'in _0x42b9da){if(_0x435689[_0x3595c2(0xc19)]['indexOf'](_0x2d50f3||_0x570b33)>=0x0||_0x435689[_0x3595c2(0xba7)]===!![]||_0x435689[_0x3595c2(0xba7)]&&_0x3595c2(0xba7)in _0x42b9da&&_0x42b9da[_0x3595c2(0xba7)]===_0x435689['remote'])_0x435689['setRemoteAutofocus'](_0x42b9da[_0x3595c2(0x293)]);else return;}if(_0x3595c2(0x93c)in _0x42b9da){if(_0x435689[_0x3595c2(0xc19)][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0||_0x435689[_0x3595c2(0xba7)]===!![]||_0x435689['remote']&&'remote'in _0x42b9da&&_0x42b9da[_0x3595c2(0xba7)]===_0x435689[_0x3595c2(0xba7)])_0x435689['remotePan'](parseFloat(_0x42b9da[_0x3595c2(0x93c)]),_0x42b9da[_0x3595c2(0x92b)]||![]);else return;}if(_0x3595c2(0x402)in _0x42b9da){if(_0x435689['directorList'][_0x3595c2(0x97c)](_0x2d50f3||_0x570b33)>=0x0||_0x435689[_0x3595c2(0xba7)]===!![]||_0x435689[_0x3595c2(0xba7)]&&_0x3595c2(0xba7)in _0x42b9da&&_0x42b9da['remote']===_0x435689[_0x3595c2(0xba7)])_0x435689[_0x3595c2(0x728)](parseFloat(_0x42b9da['tilt']),_0x42b9da[_0x3595c2(0x92b)]||![]);else return;}if(_0x3595c2(0xbb9)in _0x42b9da){if(_0x435689[_0x3595c2(0xc19)]['indexOf'](_0x2d50f3||_0x570b33)>=0x0||_0x435689['remote']===!![]||_0x435689[_0x3595c2(0xba7)]&&_0x3595c2(0xba7)in _0x42b9da&&_0x42b9da['remote']===_0x435689[_0x3595c2(0xba7)])_0x435689[_0x3595c2(0x56d)](parseFloat(_0x42b9da[_0x3595c2(0xbb9)]),_0x42b9da['abs']||![]);else return;}if(_0x3595c2(0x3b9)in _0x42b9da){log(_0x3595c2(0x3b9));try{_0x435689[_0x3595c2(0x1f2)](_0x570b33,_0x42b9da[_0x3595c2(0x3b9)]);}catch(_0x183b6d){errorlog(_0x183b6d);}}_0x3595c2(0x638)in _0x42b9da&&playbackMIDI(_0x42b9da[_0x3595c2(0x638)],!![],_0x570b33);}catch(_0x1cecb5){errorlog(_0x1cecb5);}if(_0x3595c2(0x592)in _0x42b9da){if(_0x42b9da[_0x3595c2(0x592)]==_0x3595c2(0x6c1)){if(_0x435689[_0x3595c2(0xba7)])warnUser(getTranslation('invalid-remote-code'),0xbb8);else document['querySelector'](_0x3595c2(0x682))&&document[_0x3595c2(0x413)](_0x3595c2(0x682))[_0x3595c2(0x62d)]?warnUser(getTranslation('invalid-remote-code-obs'),0x1b58):warnUser(getTranslation('request-rejected-obs'),0x2710);getById(_0x3595c2(0xc1a))[_0x3595c2(0xab1)][_0x3595c2(0xae4)](_0x3595c2(0xc09));}else{if(_0x435689[_0x3595c2(0x781)])!_0x435689[_0x3595c2(0x326)]&&warnUser(_0x3595c2(0x1b4)+_0x42b9da['rejected']+')\x20failed\x20due\x20to\x20permissions\x20or\x20it\x20was\x20rejected\x20by\x20the\x20user',0x1388);else!_0x435689['cleanOutput']&&(_0x435689[_0x3595c2(0xba7)]?warnUser(_0x3595c2(0x24d),0x1388):warnUser(_0x3595c2(0x6e5),0x1388));}errorlog(_0x3595c2(0x169)+_0x42b9da['rejected']+_0x3595c2(0x1ed)+_0x435689[_0x3595c2(0x781)]),pokeIframeAPI('rejected',_0x42b9da[_0x3595c2(0x592)],_0x570b33);return;}else{if('approved'in _0x42b9da){log(_0x3595c2(0x360)+_0x42b9da[_0x3595c2(0xb96)]),pokeIframeAPI(_0x3595c2(0xb96),_0x42b9da['approved'],_0x570b33);return;}}if(_0x3595c2(0x2de)in _0x42b9da||_0x3595c2(0x57d)in _0x42b9da){log(_0x3595c2(0x412));_0x42b9da[_0x3595c2(0x2de)]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1da)]=_0x42b9da['audio']);if(_0x435689[_0x3595c2(0x265)]&&'allowwebp'in _0x42b9da&&_0x42b9da['allowwebp']!==![])_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x657)]=_0x42b9da[_0x3595c2(0x823)],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1b1)]=![],setTimeout(function(){makeImages(!![]);},0x3e8);else _0x42b9da[_0x3595c2(0x57d)]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33]['allowVideo']=_0x42b9da[_0x3595c2(0x57d)]);_0x3595c2(0x532)in _0x42b9da&&_0x42b9da[_0x3595c2(0x532)]!==![]&&(_0x435689['pcs'][_0x570b33]['allowBroadcast']=_0x42b9da['broadcast']);_0x3595c2(0x466)in _0x42b9da&&_0x42b9da['allowchunked']!==![]&&(_0x435689['pcs'][_0x570b33]['allowChunked']=_0x42b9da[_0x3595c2(0x466)]);if(_0x3595c2(0x2dc)in _0x42b9da&&_0x42b9da[_0x3595c2(0x2dc)]){_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x9ee)]=_0x42b9da[_0x3595c2(0x2dc)];try{_0x435689['videoElement']&&_0x435689['videoElement'][_0x3595c2(0x2d5)]&&_0x435689[_0x3595c2(0x917)]['syncDrawOnVideo']();}catch(_0x341090){errorlog(_0x341090);}}_0x3595c2(0xbd4)in _0x42b9da&&_0x42b9da[_0x3595c2(0xbd4)]!==![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4ba)]=_0x42b9da[_0x3595c2(0xbd4)]);const _0x5a575e=!('allowscreenmeshcast'in _0x42b9da&&_0x42b9da[_0x3595c2(0xa8a)]===![]||_0x3595c2(0x9aa)in _0x42b9da&&_0x42b9da['allowscreenwhipout']===![]);_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xb80)]=_0x5a575e;'widget'in _0x42b9da&&_0x42b9da[_0x3595c2(0x809)]!==![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x9f1)]=_0x42b9da['widget']);_0x3595c2(0x4f1)in _0x42b9da&&_0x42b9da[_0x3595c2(0x4f1)]!==![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x228)]=_0x42b9da[_0x3595c2(0x4f1)]);_0x3595c2(0x38c)in _0x42b9da&&_0x42b9da[_0x3595c2(0x38c)]!==![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x208)]=_0x42b9da['allowresources']);_0x3595c2(0x9bf)in _0x42b9da&&_0x42b9da[_0x3595c2(0x9bf)]!==![]&&(_0x435689['pcs'][_0x570b33][_0x3595c2(0xa83)]=_0x42b9da[_0x3595c2(0x9bf)]);_0x3595c2(0x3fa)in _0x42b9da&&_0x42b9da['allowscreen']!==![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x927)]=!![],_0x435689[_0x3595c2(0x76f)][_0x570b33]['allowScreenVideo']=!![]);_0x3595c2(0x800)in _0x42b9da&&_0x42b9da[_0x3595c2(0x800)]!==![]&&(_0x435689['pcs'][_0x570b33][_0x3595c2(0x53b)]=!![]);_0x3595c2(0x2aa)in _0x42b9da&&_0x42b9da[_0x3595c2(0x2aa)]!==![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x927)]=!![]);if(_0x435689['preferVideoCodec'])_0x435689[_0x3595c2(0x76f)][_0x570b33]['preferVideoCodec']=_0x435689[_0x3595c2(0x4f2)];else _0x3595c2(0x4f2)in _0x42b9da&&_0x42b9da[_0x3595c2(0x4f2)]!==![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4f2)]=_0x42b9da[_0x3595c2(0x4f2)]['toLowerCase']());if(_0x435689[_0x3595c2(0x74e)])_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x74e)]=_0x435689['preferAudioCodec'];else _0x3595c2(0x74e)in _0x42b9da&&_0x42b9da[_0x3595c2(0x74e)]!==![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x74e)]=_0x42b9da['preferAudioCodec'][_0x3595c2(0xa6f)]());_0x3595c2(0xa8a)in _0x42b9da&&_0x42b9da[_0x3595c2(0xa8a)]===![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x288)]=![]);_0x3595c2(0x9aa)in _0x42b9da&&_0x42b9da['allowscreenwhipout']===![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x288)]=![]);if(_0x3595c2(0x383)in _0x42b9da&&_0x42b9da[_0x3595c2(0x383)]===![])_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x43a)]=![];else{if('allowwhipout'in _0x42b9da&&_0x42b9da['allowwhipout']===![])_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x43a)]=![];else{if(_0x435689['meshcast']){if(_0x435689[_0x3595c2(0xc65)]==_0x3595c2(0x57d))_0x435689['pcs'][_0x570b33][_0x3595c2(0x1b1)]=![];else{if(_0x435689[_0x3595c2(0xc65)]=='audio')_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1da)]=![];else _0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1b1)]==![]?_0x435689['pcs'][_0x570b33][_0x3595c2(0x43a)]=![]:(_0x435689['pcs'][_0x570b33][_0x3595c2(0x1da)]=![],_0x435689['pcs'][_0x570b33][_0x3595c2(0x1b1)]=![]);}}else _0x435689[_0x3595c2(0x537)]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1da)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33]['allowVideo']=![]);}}_0x435689[_0x3595c2(0x792)]&&_0x435689['whipOutputScreen']&&_0x5a575e&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x927)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x53b)]=![]);_0x435689['whipoutSettings']&&_0x435689[_0x3595c2(0x50a)][_0x3595c2(0x8e5)]&&broadcastWhepSettings(_0x3595c2(0x596));_0x435689[_0x3595c2(0x792)]&&_0x435689['screenShareState']&&_0x435689['whipoutScreenSettings']&&_0x435689[_0x3595c2(0x853)][_0x3595c2(0x8e5)]&&_0x435689['whipoutScreenSettings'][_0x3595c2(0x838)]&&broadcastWhepSettings(_0x3595c2(0xadf));if(_0x435689[_0x3595c2(0x562)]){window[_0x3595c2(0x296)]();_0x435689[_0x3595c2(0x192)]&&playtone();var _0x3ac1f1=![];_0x570b33 in _0x435689['rpcs']&&_0x435689[_0x3595c2(0xc6c)][_0x570b33][_0x3595c2(0xc69)]&&(_0x3ac1f1=_0x435689[_0x3595c2(0xc6c)][_0x570b33][_0x3595c2(0xc69)]||_0x435689[_0x3595c2(0xc6c)][_0x570b33][_0x3595c2(0x885)]||![]);_0x3ac1f1=_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xc69)]||_0x3ac1f1||_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x885)]||_0x570b33||_0x3595c2(0x931);var _0x96222b=await confirmAlt(_0x3ac1f1+getTranslation(_0x3595c2(0x7d3)),!![]);if(!_0x96222b){try{log(_0x3595c2(0x928)),_0x435689['closePC'](_0x570b33);}catch(_0x59b266){}return;}}_0x3595c2(0x2ba)in _0x42b9da&&(_0x42b9da[_0x3595c2(0x2ba)]==!![]&&(_0x435689['pcs'][_0x570b33]['guest']=!![],_0x435689[_0x3595c2(0x192)]&&(playtone(![],_0x3595c2(0x60c)),showNotification(_0x3595c2(0x591),'')),pokeIframeAPI(_0x3595c2(0x66b),_0x42b9da[_0x3595c2(0x781)],_0x570b33)));_0x3595c2(0x337)in _0x42b9da&&(_0x42b9da[_0x3595c2(0x337)]===!![]&&(_0x435689['pcs'][_0x570b33][_0x3595c2(0x337)]=!![]));_0x3595c2(0xba7)in _0x42b9da&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xba7)]=_0x42b9da['remote']);_0x3595c2(0x5f1)in _0x42b9da&&(_0x42b9da[_0x3595c2(0x5f1)]==!![]&&(_0x435689['pcs'][_0x570b33]['limitAudio']=!![]));_0x3595c2(0x3b8)in _0x42b9da&&(_0x42b9da[_0x3595c2(0x3b8)]==!![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x9d4)]=!![]));_0x42b9da[_0x3595c2(0x517)]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33]['degradationPreference']=_0x42b9da['degrade']);if(_0x3595c2(0x2a6)in _0x42b9da)try{_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x2a6)]=_0x42b9da[_0x3595c2(0x2a6)],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x2a6)]&&setTimeout(function(_0x3ed89c){_0x435689['forcePLI'](_0x3ed89c);},0x1388,_0x570b33);}catch(_0x24b80e){warnlog(_0x24b80e);}_0x3595c2(0xb15)in _0x42b9da&&(_0x435689['pcs'][_0x570b33][_0x3595c2(0xb15)]=_0x42b9da[_0x3595c2(0xb15)]);if(_0x3595c2(0xb50)in _0x42b9da){const _0x5d2183=_0x42b9da[_0x3595c2(0xb50)];if(_0x5d2183&&_0x5d2183!==!![]&&_0x5d2183!==_0x3595c2(0x95a)&&_0x5d2183!==![]&&_0x5d2183!==_0x3595c2(0x914))_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xb50)]=_0x5d2183;else!_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xb50)]&&(_0x5d2183===!![]||_0x5d2183==='true')&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xb50)]=_0x5d2183);const _0x2e6af9=normalizeLayoutState(_0x5d2183);if(typeof _0x2e6af9!==_0x3595c2(0xba0))_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x59d)]=_0x2e6af9;else typeof _0x435689[_0x3595c2(0x76f)][_0x570b33]['layoutState']===_0x3595c2(0xba0)&&(_0x435689[_0x3595c2(0x76f)][_0x570b33]['layoutState']=![]);}if(_0x3595c2(0xa34)in _0x42b9da){if(_0x42b9da[_0x3595c2(0xa34)]!==![]){try{typeof _0x42b9da['scene']===_0x3595c2(0x332)?_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xa34)]=_0x42b9da[_0x3595c2(0xa34)][_0x3595c2(0x586)](/[\W]+/g,'_'):_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xa34)]=(parseInt(_0x42b9da[_0x3595c2(0xa34)])||0x0)+'',_0x435689[_0x3595c2(0x76f)][_0x570b33]['stats']['scene']=_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xa34)],updateSceneList(_0x435689['pcs'][_0x570b33][_0x3595c2(0xa34)]);}catch(_0x18fc42){errorlog(_0x18fc42);}_0x3595c2(0xb33)in _0x42b9da?_0x435689[_0x3595c2(0x76f)][_0x570b33]['showDirector']=_0x42b9da['showDirector']:_0x435689['pcs'][_0x570b33]['showDirector']=_0x435689['showDirector'];if(_0x435689[_0x3595c2(0x781)]){if(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xb33)]==![])_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1da)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1b1)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4ba)]=![],_0x435689['pcs'][_0x570b33][_0x3595c2(0x9ee)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33]['allowWidget']=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x43a)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33]['allowWebp']=![],_0x435689['pcs'][_0x570b33][_0x3595c2(0x927)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x53b)]=![];else{if(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xb33)]==0x1)_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4ba)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x9f1)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x9ee)]=![];else{if(_0x435689[_0x3595c2(0x76f)][_0x570b33]['showDirector']==0x2)_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1da)]=![],_0x435689['pcs'][_0x570b33]['allowScreenAudio']=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4ba)]=![],_0x435689['pcs'][_0x570b33][_0x3595c2(0x9f1)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33]['allowDrawing']=![];else{if(_0x435689[_0x3595c2(0x76f)][_0x570b33]['showDirector']==0x3)_0x435689['pcs'][_0x570b33]['allowAudio']=![],_0x435689['pcs'][_0x570b33]['allowVideo']=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x4ba)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x9f1)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x43a)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x657)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x9ee)]=![];else{if(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xb33)]==0x4){}}}}}broadcastSlotUpdate(_0x570b33);}_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xb15)]?pokeIframeAPI(_0x3595c2(0x327),_0x42b9da[_0x3595c2(0xa34)],_0x570b33):pokeIframeAPI(_0x3595c2(0x6a6),_0x42b9da[_0x3595c2(0xa34)],_0x570b33);}_0x435689[_0x3595c2(0x458)](_0x570b33);}else _0x42b9da['director']&&((iOS||iPad)&&(_0x435689[_0x3595c2(0x76f)][_0x570b33]['forceios']==!![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x2ba)]=!![])),_0x435689[_0x3595c2(0x192)]&&(playtone(![],'jointone'),showNotification('A\x20director\x20joined\x20the\x20room',_0x3595c2(0xa80))),_0x435689[_0x3595c2(0x458)](_0x570b33),pokeIframeAPI(_0x3595c2(0xba4),_0x42b9da['director'],_0x570b33));if(_0x435689['director'])'hidedirector'in _0x42b9da&&(_0x42b9da[_0x3595c2(0x250)]==!![]&&(_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x1da)]=![],_0x435689['pcs'][_0x570b33]['allowVideo']=![],_0x435689[_0x3595c2(0x76f)][_0x570b33]['allowIframe']=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x9f1)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33]['whipout']=![],_0x435689['pcs'][_0x570b33]['allowWebp']=![],_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0x927)]=![],_0x435689[_0x3595c2(0x76f)][_0x570b33]['allowScreenVideo']=![],_0x435689['pcs'][_0x570b33][_0x3595c2(0x9ee)]=![])),_0x435689[_0x3595c2(0x503)](_0x570b33);else{if(_0x435689['queueType']==0x3&&!_0x435689[_0x3595c2(0x781)])_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xc12)]=!![];else _0x435689[_0x3595c2(0x7c3)]==0x4&&!_0x435689[_0x3595c2(0x781)]?_0x435689['directorList'][_0x3595c2(0x97c)](_0x570b33)>=0x0?_0x435689[_0x3595c2(0x503)](_0x570b33):_0x435689[_0x3595c2(0x76f)][_0x570b33][_0x3595c2(0xc12)]=!![]:_0x435689['initialPublish'](_0x570b33);}}},_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x4d0)][_0x1535b5(0x13c)]=_0x2ede65=>_0x435689[_0x1535b5(0x3a0)](_0x2ede65,_0x28f225);function _0x56e5d1(){var _0x1a6e98=_0x1535b5;try{var _0xaab42f=document[_0x1a6e98(0x413)](_0x1a6e98(0x331));if(_0xaab42f)return{'type':_0x1a6e98(0x6eb),'target':_0x31be67(_0xaab42f)};var _0x2a5c74=document[_0x1a6e98(0x413)](_0x1a6e98(0xbe1));if(_0x2a5c74)return{'type':_0x1a6e98(0xa90),'target':_0x31be67(_0x2a5c74)};var _0x283d58=typeof getById==='function'?getById('highlightDirector'):document[_0x1a6e98(0x54c)](_0x1a6e98(0x768));if(_0x283d58){var _0x330e3b=_0x31be67(_0x283d58);if(_0x283d58[_0x1a6e98(0x992)]){if(_0x283d58[_0x1a6e98(0xab1)]&&_0x283d58[_0x1a6e98(0xab1)][_0x1a6e98(0x5b2)](_0x1a6e98(0xc5c)))return{'type':_0x1a6e98(0x6eb),'target':_0x330e3b};return{'type':_0x1a6e98(0xa90),'target':_0x330e3b};}}}catch(_0x2c9b8c){errorlog(_0x2c9b8c);}return null;}function _0x31be67(_0x14f349){var _0x2ab26a=_0x1535b5;if(!_0x14f349)return![];try{if(_0x14f349[_0x2ab26a(0xc6b)]&&_0x14f349['dataset']['sid'])return _0x14f349['dataset'][_0x2ab26a(0x952)];}catch(_0x263c0f){}if(_0x14f349&&_0x14f349['id']===_0x2ab26a(0x768)&&_0x435689&&_0x435689[_0x2ab26a(0x885)])return _0x435689[_0x2ab26a(0x885)];return![];}_0x435689[_0x1535b5(0x458)]=function(_0x34b542){var _0x7274cd=_0x1535b5;if(!(_0x435689[_0x7274cd(0x3e3)]||_0x435689[_0x7274cd(0xa34)]))return;try{var _0x502f8f={};_0x435689[_0x7274cd(0x76f)][_0x34b542]&&(_0x502f8f[_0x7274cd(0xb41)]=getDirectorSettings(_0x435689[_0x7274cd(0x76f)][_0x34b542][_0x7274cd(0xa34)]));log('TRYING\x20TO\x20SYNC\x20WITH\x20SENDING:\x20'+_0x34b542);var _0xe4470f=![];_0x435689[_0x7274cd(0xa1c)]&&_0x435689[_0x7274cd(0xa1c)][_0x7274cd(0x3d5)](_0x35af55=>{var _0x327a50=_0x7274cd;_0x35af55[_0x327a50(0x9d6)]===_0x34b542&&(_0xe4470f=!![]);});!_0xe4470f||_0x435689[_0x7274cd(0x76f)][_0x34b542]&&_0x435689[_0x7274cd(0x76f)][_0x34b542][_0x7274cd(0x908)]===!![]?_0x502f8f['directorState']=getDetailedState():warnlog(_0x7274cd(0x636));if(_0x435689[_0x7274cd(0x781)]){var _0xa4733=_0x56e5d1();if(_0xa4733&&_0x435689[_0x7274cd(0x76f)][_0x34b542]){var _0x3498a8=_0xa4733[_0x7274cd(0x5df)];if(_0x3498a8!==![]){var _0x459a4a=_0x435689[_0x7274cd(0x76f)][_0x34b542][_0x7274cd(0x59d)],_0x33a8e7=!![];try{if(typeof isAutoLayoutState===_0x7274cd(0x5f9))_0x33a8e7=isAutoLayoutState(_0x459a4a);else{if(typeof normalizeLayoutStateValue===_0x7274cd(0x5f9)){var _0x4b2a4b=normalizeLayoutStateValue(_0x459a4a);_0x33a8e7=_0x4b2a4b===![]||typeof _0x4b2a4b===_0x7274cd(0xba0)||_0x4b2a4b===null;}else _0x33a8e7=!_0x459a4a||_0x459a4a===![];}}catch(_0x5f4c31){errorlog(_0x5f4c31);}if(!_0x435689['pcs'][_0x34b542][_0x7274cd(0xb15)]&&_0x33a8e7){var _0x259308={};_0x259308[_0xa4733['type']]=_0x3498a8,_0x435689[_0x7274cd(0x896)](_0x259308,_0x34b542);}}}}Object[_0x7274cd(0x161)](_0x502f8f)[_0x7274cd(0xb04)]&&_0x435689[_0x7274cd(0xbd9)](_0x502f8f,_0x34b542);}catch(_0x380415){}},_0x435689['provideFileList']=function(_0xabe1a4){var _0x432012=_0x1535b5;log('session.provideFileList');if(!_0x435689[_0x432012(0x690)]||!_0x435689[_0x432012(0x690)]['length'])return;var _0x1c45c1={},_0x354f22=[];for(var _0x235fa2=0x0;_0x235fa2<_0x435689[_0x432012(0x690)][_0x432012(0xb04)];_0x235fa2++){(_0x435689[_0x432012(0x690)][_0x235fa2][_0x432012(0x147)]===![]||_0x435689[_0x432012(0x690)][_0x235fa2][_0x432012(0x147)]===_0xabe1a4)&&_0x354f22[_0x432012(0x35c)]({'id':_0x435689[_0x432012(0x690)][_0x235fa2]['id'],'name':_0x435689[_0x432012(0x690)][_0x235fa2][_0x432012(0x88c)],'size':_0x435689[_0x432012(0x690)][_0x235fa2][_0x432012(0x735)]});}_0x1c45c1[_0x432012(0x468)]=_0x354f22;if(_0xabe1a4 in _0x435689[_0x432012(0x76f)])_0x435689[_0x432012(0x896)](_0x1c45c1,_0xabe1a4);else _0xabe1a4 in _0x435689[_0x432012(0xc6c)]&&_0x435689[_0x432012(0x43f)](_0x1c45c1,_0xabe1a4);log(_0x1c45c1);},_0x435689[_0x1535b5(0x503)]=function(_0xc3dc34){var _0x33b637=_0x1535b5;log(_0x33b637(0x5a9)+_0xc3dc34);if(_0xc3dc34 in _0x435689['pcs'])_0x435689['pcs'][_0xc3dc34]['needsPublishing']=![];else{errorlog('UUID\x20not\x20found\x20in\x20pcs');return;}getSenders2(_0xc3dc34)[_0x33b637(0xb04)]&&errorlog(_0x33b637(0x185)+getSenders2(_0xc3dc34)[_0x33b637(0xb04)]);if(_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0x4ba)]===!![]){if(_0x435689['iframeSrc']){var _0x3631c7={};_0x3631c7[_0x33b637(0xafd)]=_0x435689[_0x33b637(0xafd)],_0x435689[_0x33b637(0x964)]&&_0x435689[_0x33b637(0x964)]['sendOnNewConnect']&&(_0x435689[_0x33b637(0xafd)][_0x33b637(0x8da)](_0x33b637(0xaa7))&&(_0x3631c7[_0x33b637(0xafd)]+=_0x33b637(0x9b2)+parseInt(Math[_0x33b637(0xa44)](_0x435689[_0x33b637(0x964)]['sendOnNewConnect'][_0x33b637(0x25c)]['t']))+'')),_0x435689['sendMessage'](_0x3631c7,_0xc3dc34);}}if(_0x435689[_0x33b637(0x76f)][_0xc3dc34]['allowWidget']===!![]){if(_0x435689['widget']&&_0x435689['director']){var _0x3631c7={};_0x3631c7[_0x33b637(0xabb)]=_0x435689[_0x33b637(0x809)],_0x435689[_0x33b637(0x896)](_0x3631c7,_0xc3dc34);}}_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0xa83)]===!![]&&_0x435689[_0x33b637(0xb60)](_0xc3dc34);_0x435689[_0x33b637(0x76f)][_0xc3dc34]['allowResources']===!![]&&_0x435689['createResourceChannel'](_0xc3dc34);let _0x8dccdf=![];if(_0x435689[_0x33b637(0x6a9)]&&_0x435689['pcs'][_0xc3dc34][_0x33b637(0x699)]){_0x435689[_0x33b637(0xa54)](_0xc3dc34);if(_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0x699)]!==0x2)return;_0x8dccdf=!![];}var _0x1ee91a=_0x435689[_0x33b637(0x226)]();log(_0x33b637(0x1f7)),log(_0x1ee91a[_0x33b637(0x81f)]());if(_0x435689[_0x33b637(0x50a)]&&_0x435689['pcs'][_0xc3dc34]['whipout']===null){_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0x43a)]=!![];var _0x3631c7={};_0x3631c7[_0x33b637(0xbf1)]=_0x435689['whipoutSettings'],_0x435689[_0x33b637(0x896)](_0x3631c7,_0xc3dc34),warnlog(_0x3631c7);}!_0x8dccdf&&(_0x435689[_0x33b637(0x76f)][_0xc3dc34]['allowScreenVideo']||_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0x927)])&&createSecondStream2(_0xc3dc34);var _0x10a003=![];!_0x8dccdf&&_0x1ee91a[_0x33b637(0x4df)]()['forEach'](async _0x512a25=>{var _0x2ae191=_0x33b637;try{if(_0x435689[_0x2ae191(0x76f)][_0xc3dc34][_0x2ae191(0x1b1)]===!![]){if(_0x512a25[_0x2ae191(0x2fe)]=='video'){if(_0x435689[_0x2ae191(0x76f)][_0xc3dc34][_0x2ae191(0x2ba)]===!![]&&_0x435689['roombitrate']===0x0)log(_0x2ae191(0x780));else{let _0x3791b7=_0x435689[_0x2ae191(0x76f)][_0xc3dc34][_0x2ae191(0x2e6)](_0x512a25,_0x1ee91a);if(_0x3791b7&&_0x435689[_0x2ae191(0x8e7)])try{setupSenderTransform(_0x3791b7,_0xc3dc34);}catch(_0x385251){errorlog(_0x385251);}warnlog(_0x2ae191(0x488)),_0x10a003=!![],setTimeout(function(_0x32fcd2){var _0x3366ce=_0x2ae191;try{_0x435689[_0x3366ce(0xb1a)](_0x32fcd2);}catch(_0xba15aa){warnlog(_0xba15aa);}},_0x435689['rampUpTime'],_0xc3dc34);}}}}catch(_0x2d60e9){errorlog(_0x2d60e9);}});if(_0x435689['mixMinus']){if(_0x435689[_0x33b637(0x427)]){var _0x268b37=createDirectorMixMinusForGuest(_0xc3dc34);_0x268b37&&(_0x1ee91a=_0x268b37,onGuestJoinedMixMinus(_0xc3dc34));}else _0x1ee91a=mixMinusAudio(_0xc3dc34);}_0x435689[_0x33b637(0x76f)][_0xc3dc34]['allowAudio']&&(_0x1ee91a[_0x33b637(0x253)]()['forEach'](_0x250f5e=>{var _0x28bcfa=_0x33b637;try{_0x250f5e[_0x28bcfa(0x2fe)]==_0x28bcfa(0x2de)&&(_0x435689[_0x28bcfa(0x76f)][_0xc3dc34]['addTrack'](_0x250f5e,_0x1ee91a),warnlog(_0x28bcfa(0x9e5)));}catch(_0x3cb6ac){errorlog(_0x3cb6ac);}}),log(_0x33b637(0x530)),_0x1ee91a[_0x33b637(0x253)]()[_0x33b637(0xb04)]&&(_0x435689[_0x33b637(0x781)]!==![]&&_0x435689[_0x33b637(0x186)](),log('starting\x20kicker'),_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0x3c8)]===!![]&&(warnlog('limiting\x20AudioEncoder'),setTimeout(_0x435689['limitAudioEncoder'],0x3e8,_0xc3dc34,0x7d00,0x0)),_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0x9d4)]===!![]&&setTimeout(_0x435689[_0x33b637(0xbe4)],0x3e8,_0xc3dc34)));if(_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0x68c)])setTimeout(_0x435689[_0x33b637(0x68c)],0x3e8,_0xc3dc34,_0x435689[_0x33b637(0x76f)][_0xc3dc34][_0x33b637(0x68c)]);else{if(_0x435689[_0x33b637(0x703)]&&SafariVersion){if(_0x435689['contentHint']==_0x33b637(0xc08))setTimeout(_0x435689[_0x33b637(0x68c)],0x3e8,_0xc3dc34,_0x33b637(0xb3d));else _0x435689[_0x33b637(0x703)]==_0x33b637(0x472)&&setTimeout(_0x435689[_0x33b637(0x68c)],0x3e8,_0xc3dc34,_0x33b637(0xaee));}}if(iOS||iPad){if(SafariVersion&&SafariVersion<=0xd){}else _0x10a003&&(setTimeout(function(_0x5c5fb1){var _0x112223=_0x33b637;_0x435689[_0x112223(0x125)](_0x5c5fb1,null,!![]);},0x7d0,_0xc3dc34),setTimeout(function(_0x5cb4ab){var _0x32903d=_0x33b637,_0x4525a9=_0x435689[_0x32903d(0x291)](_0x5cb4ab);!_0x4525a9&&_0x435689['setScale'](_0x5cb4ab,0x64,!![]);},0x1388,_0xc3dc34));}else setTimeout(function(_0x2dea5b){var _0xc0290f=_0x33b637;_0x435689[_0xc0290f(0x291)](_0x2dea5b);},0x3e8,_0xc3dc34);},_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x1f1)]=function(_0x12c313){var _0x430070=_0x1535b5;if(!(_0x28f225 in _0x435689['pcs']))return;try{if(this[_0x430070(0x9bb)]===_0x430070(0x94c))log('ICE\x20closed?');else{if(this[_0x430070(0x9bb)]===_0x430070(0x2b8))log(_0x430070(0x850));else{if(this[_0x430070(0x9bb)]==='failed')log(_0x430070(0x8b1));else this[_0x430070(0x9bb)]===_0x430070(0x5af)?log(_0x430070(0x65e)):log(this[_0x430070(0x9bb)]);}}}catch(_0x55c871){errorlog(_0x55c871);}},_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x46a)]=function(_0x17b152){var _0x57fbf7=_0x1535b5;switch(_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x8e8)]){case'connected':log('CONNECTEED!'),clearTimeout(_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x98f)]);if(_0x435689['qosEnabled']&&_0x435689[_0x57fbf7(0x8c9)]){_0x435689['qosData'][_0x57fbf7(0x176)]++;try{setTimeout(processPcsQosStats,0x1f4,_0x28f225);}catch(_0x5af0a8){}}if(_0x435689['security']){if(_0x435689['ws']['readyState']!==0x1){_0x435689['ws']['close']();break;}_0x435689['ws'][_0x57fbf7(0x757)](),setTimeout(function(){var _0x51fb1c=_0x57fbf7;_0x435689[_0x51fb1c(0x326)]!=!![]&&warnUser(getTranslation(_0x51fb1c(0xbbe)));},0x1);}break;case'disconnected':log(_0x57fbf7(0x219)),clearTimeout(_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x98f)]);try{const _0xc1c1a7=Date[_0x57fbf7(0x1ab)]();_0x435689[_0x57fbf7(0x76f)][_0x28f225]&&(_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x94e)]=undefined,_0x435689['pcs'][_0x28f225][_0x57fbf7(0x89a)]=_0xc1c1a7);try{_0x435689[_0x57fbf7(0x896)]({'ping':_0xc1c1a7},_0x28f225);}catch(_0x581950){warnlog(_0x581950);}setTimeout(function(_0x45730d,_0x1ce622){var _0x536b70=_0x57fbf7;try{if(!(_0x45730d in _0x435689['pcs']))return;if(_0x435689['pcs'][_0x45730d]['connectionState']!==_0x536b70(0x2b8))return;if(_0x435689[_0x536b70(0x76f)][_0x45730d]['lastPongToken']!==_0x1ce622){try{_0x435689[_0x536b70(0x36e)]&&_0x435689[_0x536b70(0x36e)](_0x435689[_0x536b70(0x76f)][_0x45730d]);}catch(_0x188088){warnlog(_0x188088);}if(_0x435689['pcs'][_0x45730d][_0x536b70(0x495)])try{_0x435689[_0x536b70(0x76f)][_0x45730d][_0x536b70(0x495)]();}catch(_0x384ac6){warnlog(_0x384ac6);}try{_0x435689[_0x536b70(0xa84)](_0x45730d,!![]);}catch(_0x3ca6ef){warnlog(_0x3ca6ef);}}}catch(_0x1c865d){errorlog(_0x1c865d);}},0xbb8,_0x28f225,_0xc1c1a7);}catch(_0x4941b1){errorlog(_0x4941b1);}_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x98f)]=setTimeout(function(_0x55a19d){var _0x46674e=_0x57fbf7;_0x55a19d in _0x435689['pcs']?(warnlog(_0x46674e(0x484)),_0x435689[_0x46674e(0x974)](_0x55a19d)):errorlog('\x20---\x20PC\x20TIMED\x20OUT\x20and\x20already\x20deleted.\x20shouldn\x27t\x20happen');},navigator[_0x57fbf7(0x2ef)]&&navigator[_0x57fbf7(0x2ef)]['toLowerCase']()['includes'](_0x57fbf7(0x1a8))?0x2710:0x1388,_0x28f225);break;case _0x57fbf7(0x35e):warnlog(_0x57fbf7(0xa86));_0x435689[_0x57fbf7(0x509)]&&_0x435689[_0x57fbf7(0x8c9)]&&_0x435689[_0x57fbf7(0x8c9)][_0x57fbf7(0x30e)]++;_0x435689[_0x57fbf7(0x76f)][_0x28f225]&&(_0x435689[_0x57fbf7(0x76f)][_0x28f225]['delayIceSend']=0x0,_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x98f)]&&(log(_0x57fbf7(0xb8c)),clearTimeout(_0x435689['pcs'][_0x28f225][_0x57fbf7(0x98f)])),_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x495)]?(log('ice\x20restart\x20real'),_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x495)](),_0x435689['qosEnabled']&&_0x435689[_0x57fbf7(0x8c9)]&&_0x435689[_0x57fbf7(0x8c9)]['iceRestarts']++):(log('fake\x20ice\x20restart\x20faked'),_0x435689[_0x57fbf7(0xa84)](_0x28f225,!![]),_0x435689[_0x57fbf7(0x509)]&&_0x435689[_0x57fbf7(0x8c9)]&&_0x435689['qosData'][_0x57fbf7(0x193)]++));break;case _0x57fbf7(0x94c):warnlog('pcs\x20RTC\x20CLOSED'),log(_0x57fbf7(0x1f8)),_0x435689[_0x57fbf7(0x974)](_0x28f225);break;default:log(_0x57fbf7(0x454)+_0x435689[_0x57fbf7(0x76f)][_0x28f225][_0x57fbf7(0x8e8)]),clearTimeout(_0x435689[_0x57fbf7(0x76f)][_0x28f225]['closeTimeout']);break;}},_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0x4ab)]=function(_0x219450){var _0x2ecf64=_0x1535b5;warnlog(_0x2ecf64(0x978)),log(_0x2ecf64(0x946)),_0x435689[_0x2ecf64(0x974)](_0x28f225);},_0x435689[_0x1535b5(0x76f)][_0x28f225][_0x1535b5(0xada)]=function _0x4b9b65(){var _0x28eed1=_0x1535b5;log(_0x28eed1(0x578));};},_0x435689['processDescription2']=function(_0x179af8){var _0x5e195c=_0x120577,_0x15d315=_0x179af8[_0x5e195c(0x9d6)];if(_0x179af8['description'][_0x5e195c(0x2dd)]==_0x5e195c(0xb78))_0x435689[_0x5e195c(0x93a)](_0x179af8),_0x435689['connectPeer'](_0x179af8);else try{if(!(_0x179af8['UUID']in _0x435689[_0x5e195c(0x76f)]))return;var _0x38d18a=_0x435689[_0x5e195c(0x4d8)];if(_0x435689[_0x5e195c(0x612)]&&_0x435689['pcs'][_0x179af8[_0x5e195c(0x9d6)]]['guest']==!![]&&_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]]['forceios']==![]){if(_0x38d18a===![]||_0x38d18a>_0x435689[_0x5e195c(0x77c)]){var _0x574078=Object[_0x5e195c(0x161)](_0x435689[_0x5e195c(0x76f)])[_0x5e195c(0xb04)];if(_0x435689[_0x5e195c(0xb79)])_0x38d18a=_0x435689['maxMobileBitrate'];else{if(_0x574078>0x4)_0x38d18a=_0x435689[_0x5e195c(0x62b)];else(iOS||iPad)&&SafariVersion&&SafariVersion<=0xd?_0x38d18a=_0x435689[_0x5e195c(0x62b)]:_0x38d18a=_0x435689[_0x5e195c(0x77c)];}}if(iOS||iPad){if(_0x38d18a!==![]){if(_0x435689['pcs'][_0x179af8['UUID']][_0x5e195c(0x89f)]===![])_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]]['setBitrate']=_0x38d18a,_0x179af8[_0x5e195c(0x8e6)]['sdp']=CodecsHandler[_0x5e195c(0x797)](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)],_0x5e195c(0x35f),_0x435689['preferredVideoErrorCorrection']),_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x197)](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)],{'min':parseInt(_0x38d18a/0xa)||0x1,'max':_0x38d18a});else _0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x89f)]>_0x38d18a&&(_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x9d8)]=_0x38d18a,_0x179af8['description'][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x797)](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)],_0x5e195c(0x35f),_0x435689[_0x5e195c(0x3f7)]),_0x179af8['description'][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x197)](_0x179af8['description'][_0x5e195c(0x795)],{'min':parseInt(_0x38d18a/0xa)||0x1,'max':_0x38d18a}));_0x38d18a=![];}}}else{if(_0x435689[_0x5e195c(0x76f)][_0x179af8['UUID']][_0x5e195c(0x2ba)]==!![])_0x38d18a!==![]?_0x435689[_0x5e195c(0x2c3)]!==![]&&(_0x435689['roombitrate']<_0x38d18a&&(_0x38d18a=_0x435689[_0x5e195c(0x2c3)])):_0x38d18a=_0x435689[_0x5e195c(0x2c3)],(iOS||iPad)&&_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x337)]&&(_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]]['encoder']=!![]);else{if(iOS||iPad){var _0x115bf9=0x0;for(var _0x30bfb4 in _0x435689[_0x5e195c(0x76f)]){_0x179af8['UUID']!==_0x30bfb4&&(_0x435689[_0x5e195c(0x76f)][_0x30bfb4][_0x5e195c(0x784)]===!![]&&(_0x115bf9+=0x1));}if(_0x115bf9>=0x3){if(_0x435689['pcs'][_0x179af8['UUID']][_0x5e195c(0x337)])_0x435689[_0x5e195c(0x76f)][_0x179af8['UUID']][_0x5e195c(0x784)]=!![],_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]&&_0x435689['pcs'][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]===_0x5e195c(0x6f3)&&(_0x179af8['description']['sdp']=CodecsHandler['preferCodec'](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)],'h264',_0x435689[_0x5e195c(0x3f7)]),log('Trying\x20to\x20set\x20'+_0x435689[_0x5e195c(0x76f)][_0x179af8['UUID']][_0x5e195c(0x4f2)]+_0x5e195c(0x872)));else _0x435689['pcs'][_0x179af8[_0x5e195c(0x9d6)]]['preferVideoCodec']&&_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]===_0x5e195c(0x4f5)?(_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x797)](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)],_0x5e195c(0x4f5),_0x435689['preferredVideoErrorCorrection']),log(_0x5e195c(0x679)+_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]+_0x5e195c(0x872)),_0x435689['pcs'][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x784)]=![]):(_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x797)](_0x179af8['description']['sdp'],_0x5e195c(0x35f),_0x435689[_0x5e195c(0x3f7)]),log('Setting\x20Codec\x20to\x20vp8'),_0x435689['pcs'][_0x179af8['UUID']][_0x5e195c(0x784)]=![]);}else _0x435689[_0x5e195c(0x76f)][_0x179af8['UUID']][_0x5e195c(0x4f2)]&&_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]!=='h264'?_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]===_0x5e195c(0x4f5)||_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]==='vp8'?(_0x179af8[_0x5e195c(0x8e6)]['sdp']=CodecsHandler['preferCodec'](_0x179af8['description']['sdp'],_0x435689['pcs'][_0x179af8[_0x5e195c(0x9d6)]]['preferVideoCodec'],_0x435689[_0x5e195c(0x3f7)]),log(_0x5e195c(0x679)+_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]+_0x5e195c(0x872)),_0x435689['pcs'][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x784)]=![]):_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x784)]=!![]:(_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x784)]=!![],_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]]['preferVideoCodec']&&_0x435689['pcs'][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]==='h264'&&(_0x179af8['description'][_0x5e195c(0x795)]=CodecsHandler['preferCodec'](_0x179af8['description'][_0x5e195c(0x795)],_0x5e195c(0x6f3),_0x435689[_0x5e195c(0x3f7)]),log(_0x5e195c(0x679)+_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]+_0x5e195c(0x872))));}else _0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]&&(_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x797)](_0x179af8['description'][_0x5e195c(0x795)],_0x435689[_0x5e195c(0x76f)][_0x179af8['UUID']][_0x5e195c(0x4f2)],_0x435689[_0x5e195c(0x3f7)]),log('Trying\x20to\x20set\x20'+_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x4f2)]+_0x5e195c(0x872)));}}try{if(_0x38d18a){var _0xcbd286=CodecsHandler[_0x5e195c(0xc63)](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]);log(_0x5e195c(0x42a)+_0xcbd286);_0x435689['pcs'][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x89f)]!==![]&&(_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x89f)]<_0x38d18a&&(_0x38d18a=![]));if(_0x38d18a===![])_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x9d8)]=_0xcbd286;else{if(_0xcbd286!==![]&&_0xcbd286>_0x38d18a){var _0x31be09=CodecsHandler[_0x5e195c(0x137)](_0x179af8['description']['sdp'])||0x0;_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x197)](_0x179af8['description']['sdp'],{'min':parseInt(_0x38d18a/0xa)||0x1,'max':parseInt(_0x38d18a+_0x31be09/0x400)}),_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x9d8)]=_0x38d18a;}else{if(_0xcbd286===![]){var _0x31be09=CodecsHandler[_0x5e195c(0x137)](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)])||0x0;_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]=CodecsHandler['setVideoBitrates'](_0x179af8['description'][_0x5e195c(0x795)],{'min':parseInt(_0x38d18a/0xa)||0x1,'max':parseInt(_0x38d18a+_0x31be09/0x400)});if(_0x435689[_0x5e195c(0x62c)]&&_0x435689['outboundVideoBitrate']>_0x38d18a)_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]]['setBitrate']=_0x38d18a;else _0x435689[_0x5e195c(0x62c)]?_0x435689[_0x5e195c(0x76f)][_0x179af8['UUID']][_0x5e195c(0x9d8)]=_0x435689[_0x5e195c(0x62c)]:_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x89f)]=0x9c4;}else _0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x9d8)]=_0xcbd286;}}}else{if(_0x435689[_0x5e195c(0x62c)]!==![]){var _0xcbd286=CodecsHandler['getVideoBitrates'](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]);log('BITRATE\x202:\x20'+_0xcbd286);if(_0xcbd286===![]){var _0x31be09=CodecsHandler[_0x5e195c(0x137)](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)])||0x0;_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x197)](_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)],{'min':parseInt(_0x435689[_0x5e195c(0x62c)]/0xa)||0x1,'max':parseInt(_0x435689[_0x5e195c(0x62c)]+_0x31be09/0x400)});}else _0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x9d8)]===![]&&(_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x9d8)]=_0xcbd286);}else _0x435689[_0x5e195c(0x76f)][_0x179af8['UUID']][_0x5e195c(0x9d8)]===![]&&(_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x9d8)]=CodecsHandler[_0x5e195c(0xc63)](_0x179af8['description']['sdp']),log(_0x5e195c(0xb63)+_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x9d8)]));}}catch(_0x5c40e1){warnlog(_0x5e195c(0x75d));}_0x435689['outboundAudioBitrate']&&(_0x179af8['description'][_0x5e195c(0x795)]=CodecsHandler[_0x5e195c(0x1e2)](_0x179af8['description'][_0x5e195c(0x795)],{'maxaveragebitrate':_0x435689['outboundAudioBitrate']*0x400,'cbr':_0x435689['cbr']}));if(_0x5e195c(0x767)in _0x179af8&&_0x179af8[_0x5e195c(0x767)]!=_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x767)]){errorlog(_0x5e195c(0xbd5));return;}_0x435689[_0x5e195c(0xb8f)]&&(_0x179af8['description']['sdp']=filterSDPLAN(_0x179af8[_0x5e195c(0x8e6)][_0x5e195c(0x795)])),_0x435689[_0x5e195c(0x961)]&&(_0x179af8[_0x5e195c(0x8e6)]['sdp']=filterStunOnly(_0x179af8[_0x5e195c(0x8e6)]['sdp'])),_0x435689[_0x5e195c(0x76f)][_0x179af8[_0x5e195c(0x9d6)]][_0x5e195c(0x2be)](_0x179af8['description'])['then']()['catch'](errorlog);}catch(_0x3f9524){errorlog(_0x3f9524);}},_0x435689[_0x120577(0x51f)]=function(_0x3f916a){var _0x3db0b9=_0x120577;_0x435689[_0x3db0b9(0x28c)]&&_0x3f916a[_0x3db0b9(0x153)]?_0x435689[_0x3db0b9(0x51b)](_0x3f916a[_0x3db0b9(0x8e6)],_0x3f916a[_0x3db0b9(0x153)])[_0x3db0b9(0x7b6)](function(_0xc3ccd1){var _0xa6190f=_0x3db0b9;try{_0x3f916a[_0xa6190f(0x8e6)]=JSON[_0xa6190f(0x48a)](_0xc3ccd1),_0x435689[_0xa6190f(0x6a7)](_0x3f916a);}catch(_0x5d78ad){errorlog(_0x5d78ad);}})['catch'](function(_0x48101f){var _0x3be64b=_0x3db0b9;errorlog(_0x3be64b(0x685),_0x48101f);}):_0x435689[_0x3db0b9(0x6a7)](_0x3f916a);},_0x435689[_0x120577(0xa9d)]=function(_0x3d3ce0){var _0x53f940=_0x120577;_0x435689[_0x53f940(0x28c)]&&_0x3d3ce0['vector']?_0x435689['decryptMessage'](_0x3d3ce0[_0x53f940(0x2c7)],_0x3d3ce0[_0x53f940(0x153)])['then'](function(_0x500a50){var _0x1348bc=_0x53f940;try{_0x3d3ce0['candidate']=JSON[_0x1348bc(0x48a)](_0x500a50),_0x435689['processIce2'](_0x3d3ce0);}catch(_0x572674){errorlog(_0x572674);}})[_0x53f940(0x501)](function(_0x3a5227){errorlog('Decryption\x20error:',_0x3a5227);}):_0x435689[_0x53f940(0x6b6)](_0x3d3ce0);},_0x435689['processIce2']=function(_0x2e4fc2){var _0x28183b=_0x120577;try{if(_0x435689['icefilter']){if(_0x2e4fc2[_0x28183b(0x2c7)][_0x28183b(0x2c7)][_0x28183b(0x97c)](_0x435689['icefilter'])===-0x1){log('dropped\x20candidate\x20due\x20to\x20filter'),log(_0x2e4fc2['candidate']);return;}else log('PASSED'),log(_0x2e4fc2['candidate']);}}catch(_0x14bec7){errorlog(_0x14bec7);}if(_0x2e4fc2[_0x28183b(0x2c7)]&&'candidate'in _0x2e4fc2[_0x28183b(0x2c7)]&&_0x2e4fc2[_0x28183b(0x2c7)][_0x28183b(0x2c7)]=='')return;try{if(_0x435689[_0x28183b(0xb8f)]){if(!filterIceLAN(_0x2e4fc2['candidate']))return;}if(_0x435689[_0x28183b(0x961)]){if(!filterStunOnly(event[_0x28183b(0x2c7)]))return;}}catch(_0xd614e2){errorlog(_0xd614e2);}if(_0x2e4fc2[_0x28183b(0x9d6)]in _0x435689[_0x28183b(0x76f)]&&_0x2e4fc2[_0x28183b(0x2dd)]==_0x28183b(0xba7)){log('PCS\x20WINS\x20ICE');if(_0x28183b(0x767)in _0x2e4fc2&&_0x435689[_0x28183b(0x76f)][_0x2e4fc2[_0x28183b(0x9d6)]][_0x28183b(0x767)]!=_0x2e4fc2[_0x28183b(0x767)]){errorlog(_0x28183b(0xc61));return;}_0x435689[_0x28183b(0x76f)][_0x2e4fc2[_0x28183b(0x9d6)]]['addIceCandidate'](_0x2e4fc2[_0x28183b(0x2c7)])[_0x28183b(0x7b6)]()[_0x28183b(0x501)](function(_0x3b96d2){var _0x2fae41=_0x28183b;warnlog(_0x2fae41(0x15e)+_0x3b96d2['message']);});}else{if(_0x2e4fc2[_0x28183b(0x9d6)]in _0x435689[_0x28183b(0xc6c)]&&_0x2e4fc2[_0x28183b(0x2dd)]=='local'){log(_0x28183b(0x2f6));if(_0x28183b(0x767)in _0x2e4fc2&&_0x435689[_0x28183b(0xc6c)][_0x2e4fc2[_0x28183b(0x9d6)]][_0x28183b(0x767)]!=_0x2e4fc2[_0x28183b(0x767)]){errorlog(_0x28183b(0xc61));return;}if(_0x435689['rpcs'][_0x2e4fc2[_0x28183b(0x9d6)]]===null)return;_0x435689[_0x28183b(0xc6c)][_0x2e4fc2[_0x28183b(0x9d6)]][_0x28183b(0x162)](_0x2e4fc2['candidate'])[_0x28183b(0x7b6)]()[_0x28183b(0x501)](function(_0x221531){var _0xc95286=_0x28183b;warnlog(_0xc95286(0x15e)+_0x221531[_0xc95286(0x973)]);});}else warnlog(_0x2e4fc2),errorlog(_0x28183b(0x494));}},_0x435689[_0x120577(0x7eb)]=function(_0x80f1c8){var _0x1bd2c8=_0x120577;if(_0x435689[_0x1bd2c8(0x28c)]&&_0x80f1c8['vector'])_0x435689['decryptMessage'](_0x80f1c8['candidates'],_0x80f1c8['vector'])[_0x1bd2c8(0x7b6)](function(_0x1dfa19){var _0x460564=_0x1bd2c8;_0x80f1c8[_0x460564(0x82b)]=JSON['parse'](_0x1dfa19);var _0x330043={};_0x330043[_0x460564(0x9d6)]=_0x80f1c8[_0x460564(0x9d6)],_0x330043[_0x460564(0x2dd)]=_0x80f1c8[_0x460564(0x2dd)];for(var _0x4f17ac=0x0;_0x4f17ac<_0x80f1c8[_0x460564(0x82b)][_0x460564(0xb04)];_0x4f17ac++){_0x330043[_0x460564(0x2c7)]=_0x80f1c8[_0x460564(0x82b)][_0x4f17ac],_0x435689[_0x460564(0x6b6)](_0x330043);}});else{var _0x3bcbbb={};_0x3bcbbb[_0x1bd2c8(0x9d6)]=_0x80f1c8['UUID'],_0x3bcbbb[_0x1bd2c8(0x2dd)]=_0x80f1c8[_0x1bd2c8(0x2dd)];for(var _0x63cd53=0x0;_0x63cd53<_0x80f1c8[_0x1bd2c8(0x82b)][_0x1bd2c8(0xb04)];_0x63cd53++){_0x3bcbbb[_0x1bd2c8(0x2c7)]=_0x80f1c8[_0x1bd2c8(0x82b)][_0x63cd53],_0x435689['processIce2'](_0x3bcbbb);}}},_0x435689[_0x120577(0x63d)]=async function(_0x285abd){var _0x386f4f=_0x120577;_0x386f4f(0xadf)in _0x285abd&&(_0x435689[_0x386f4f(0xc6c)][_0x285abd['UUID']][_0x386f4f(0x8bd)]=_0x285abd[_0x386f4f(0xadf)],log(_0x386f4f(0x617)),log(_0x285abd[_0x386f4f(0xadf)]));log(_0x285abd);_0x435689[_0x386f4f(0xab0)]&&_0x285abd[_0x386f4f(0x8e6)]&&_0x285abd['description'][_0x386f4f(0x795)]&&_0x285abd[_0x386f4f(0x8e6)][_0x386f4f(0x795)]['includes'](_0x386f4f(0xa5e))&&(_0x285abd['description'][_0x386f4f(0x795)]=_0x285abd[_0x386f4f(0x8e6)][_0x386f4f(0x795)][_0x386f4f(0x586)](_0x386f4f(0xa5e),''),warnlog(_0x386f4f(0x9d7)));_0x435689[_0x386f4f(0x9d5)]&&(_0x285abd['description'][_0x386f4f(0x795)]=CodecsHandler[_0x386f4f(0x419)](_0x285abd[_0x386f4f(0x8e6)][_0x386f4f(0x795)]));_0x435689['noREMB']&&(_0x285abd[_0x386f4f(0x8e6)][_0x386f4f(0x795)]=CodecsHandler[_0x386f4f(0x335)](_0x285abd['description'][_0x386f4f(0x795)]));_0x435689[_0x386f4f(0xa3a)]&&(log(_0x285abd[_0x386f4f(0x8e6)]['sdp']),_0x285abd[_0x386f4f(0x8e6)][_0x386f4f(0x795)]=CodecsHandler['disableNACK'](_0x285abd[_0x386f4f(0x8e6)][_0x386f4f(0x795)]));_0x435689[_0x386f4f(0xb8f)]&&(_0x285abd['description'][_0x386f4f(0x795)]=filterSDPLAN(_0x285abd['description'][_0x386f4f(0x795)]));_0x435689[_0x386f4f(0x961)]&&(_0x285abd[_0x386f4f(0x8e6)][_0x386f4f(0x795)]=filterStunOnly(_0x285abd[_0x386f4f(0x8e6)]['sdp']));try{if(_0x285abd['description']&&_0x285abd[_0x386f4f(0x8e6)][_0x386f4f(0x2dd)]===_0x386f4f(0xb78)){const _0x53971b=_0x435689['rpcs'][_0x285abd[_0x386f4f(0x9d6)]];if(_0x53971b&&_0x53971b[_0x386f4f(0x817)]&&_0x53971b['signalingState']!==_0x386f4f(0x805))try{_0x53971b[_0x386f4f(0x9de)]({'type':_0x386f4f(0x717)})[_0x386f4f(0x501)](warnlog);}catch(_0x158ba7){warnlog(_0x158ba7);}}}catch(_0x5ed94a){warnlog(_0x5ed94a);}_0x435689[_0x386f4f(0xc6c)][_0x285abd[_0x386f4f(0x9d6)]]['setRemoteDescription'](_0x285abd['description'])['then'](async function(){var _0x26c65f=_0x386f4f;if(_0x435689[_0x26c65f(0xc6c)][_0x285abd['UUID']]['remoteDescription']['type']==='offer'){if(_0x435689[_0x26c65f(0xc6c)][_0x285abd[_0x26c65f(0x9d6)]][_0x26c65f(0x817)]!==_0x26c65f(0xc68))return;_0x435689[_0x26c65f(0xc6c)][_0x285abd[_0x26c65f(0x9d6)]][_0x26c65f(0x870)]()[_0x26c65f(0x7b6)](function(_0x3ac11f){var _0x186d3a=_0x26c65f;log('creating\x20answer');if(_0x435689[_0x186d3a(0xc6c)][_0x285abd[_0x186d3a(0x9d6)]]['whip']){if(_0x435689['stereo']&&_0x435689[_0x186d3a(0x18f)]==0x4)_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler[_0x186d3a(0x1e2)](_0x3ac11f['sdp'],{'stereo':0x2},!![]);else _0x435689[_0x186d3a(0x18f)]&&!_0x435689[_0x186d3a(0x8c2)]&&_0x435689[_0x186d3a(0x18f)]!=0x3&&(_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler[_0x186d3a(0x1e2)](_0x3ac11f[_0x186d3a(0x795)],{'stereo':0x1},!![]));return _0x435689[_0x186d3a(0xc6c)][_0x285abd['UUID']][_0x186d3a(0x9de)](_0x3ac11f);}var _0x29281e=![];if(!_0x435689[_0x186d3a(0x781)]&&_0x435689['stereo']==0x5)_0x29281e={'stereo':0x1,'maxaveragebitrate':(_0x435689['audiobitrate']||_0x435689[_0x186d3a(0x2e0)])*0x400,'cbr':_0x435689['cbr'],'useinbandfec':_0x435689['noFEC']?0x0:0x1,'maxptime':_0x435689[_0x186d3a(0x7f6)],'minptime':_0x435689['minptime'],'ptime':_0x435689['ptime'],'dtx':_0x435689[_0x186d3a(0x741)]},log(_0x186d3a(0xc0d));else{if(_0x435689[_0x186d3a(0x8c2)]&&Firefox)_0x435689['audiobitrate']?_0x29281e={'stereo':0x0,'maxaveragebitrate':_0x435689[_0x186d3a(0xa3f)]*0x400,'cbr':_0x435689[_0x186d3a(0xb39)],'useinbandfec':_0x435689[_0x186d3a(0x3e0)]?0x0:0x1,'maxptime':_0x435689[_0x186d3a(0x7f6)],'minptime':_0x435689['minptime'],'ptime':_0x435689['ptime'],'dtx':_0x435689[_0x186d3a(0x741)]}:_0x29281e={'stereo':0x0,'useinbandfec':_0x435689[_0x186d3a(0x3e0)]?0x0:0x1,'maxptime':_0x435689[_0x186d3a(0x7f6)],'minptime':_0x435689[_0x186d3a(0x2f5)],'ptime':_0x435689[_0x186d3a(0xa71)],'dtx':_0x435689[_0x186d3a(0x741)]};else{if(_0x435689[_0x186d3a(0x18f)]==0x1||_0x435689[_0x186d3a(0x18f)]==0x2||_0x435689[_0x186d3a(0x18f)]==0x5)_0x29281e={'stereo':0x1,'maxaveragebitrate':(_0x435689['audiobitrate']||_0x435689[_0x186d3a(0x2e0)])*0x400,'cbr':_0x435689['cbr'],'useinbandfec':_0x435689[_0x186d3a(0x3e0)]?0x0:0x1,'maxptime':_0x435689[_0x186d3a(0x7f6)],'minptime':_0x435689[_0x186d3a(0x2f5)],'ptime':_0x435689['ptime'],'dtx':_0x435689[_0x186d3a(0x741)]},log(_0x186d3a(0xc0d));else{if(_0x435689[_0x186d3a(0x18f)]==0x4)_0x29281e={'stereo':0x2,'maxaveragebitrate':(_0x435689['audiobitrate']||_0x435689[_0x186d3a(0x2e0)])*0x400,'cbr':_0x435689[_0x186d3a(0xb39)],'useinbandfec':_0x435689['noFEC']?0x0:0x1,'maxptime':_0x435689[_0x186d3a(0x7f6)],'minptime':_0x435689[_0x186d3a(0x2f5)],'ptime':_0x435689[_0x186d3a(0xa71)],'dtx':_0x435689['dtx']};else{if(_0x435689[_0x186d3a(0xa3f)])_0x29281e={'maxaveragebitrate':_0x435689[_0x186d3a(0xa3f)]*0x400,'cbr':_0x435689['cbr'],'useinbandfec':_0x435689[_0x186d3a(0x3e0)]?0x0:0x1,'maxptime':_0x435689[_0x186d3a(0x7f6)],'minptime':_0x435689[_0x186d3a(0x2f5)],'ptime':_0x435689[_0x186d3a(0xa71)],'dtx':_0x435689[_0x186d3a(0x741)]};else{if(_0x435689[_0x186d3a(0x3e0)])_0x29281e={'useinbandfec':0x0,'maxptime':_0x435689[_0x186d3a(0x7f6)],'minptime':_0x435689['minptime'],'ptime':_0x435689['ptime'],'dtx':_0x435689['dtx']};else _0x435689[_0x186d3a(0x741)]&&(_0x29281e={'maxptime':_0x435689[_0x186d3a(0x7f6)],'minptime':_0x435689[_0x186d3a(0x2f5)],'ptime':_0x435689['ptime'],'dtx':_0x435689['dtx']});}}}}}_0x435689[_0x186d3a(0x18f)]===0x6&&(!_0x29281e?_0x29281e={'stereo':0x1}:_0x29281e[_0x186d3a(0x18f)]=0x1);_0x29281e&&(_0x3ac11f['sdp']=CodecsHandler['setOpusAttributes'](_0x3ac11f['sdp'],_0x29281e));if(_0x435689[_0x186d3a(0x844)])try{if(_0x435689[_0x186d3a(0x844)]===_0x186d3a(0x40b))_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler[_0x186d3a(0xa57)](_0x3ac11f[_0x186d3a(0x795)]);else{if(_0x435689[_0x186d3a(0x844)]===_0x186d3a(0x5c4)){if(_0x435689[_0x186d3a(0x8c2)])_0x3ac11f['sdp']=CodecsHandler[_0x186d3a(0x1b6)](_0x3ac11f[_0x186d3a(0x795)],_0x435689[_0x186d3a(0x148)]||0xbb80,![],_0x435689[_0x186d3a(0xa71)]);else _0x435689['stereo']?_0x3ac11f['sdp']=CodecsHandler['modifyDescPCM'](_0x3ac11f[_0x186d3a(0x795)],_0x435689[_0x186d3a(0x148)]||0x7d00,!![],_0x435689[_0x186d3a(0xa71)]):_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler[_0x186d3a(0x1b6)](_0x3ac11f[_0x186d3a(0x795)],_0x435689['sampleRate']||0xbb80,![],_0x435689[_0x186d3a(0xa71)]);}else _0x3ac11f[_0x186d3a(0x795)]=CodecsHandler['preferAudioCodec'](_0x3ac11f['sdp'],_0x435689['audioCodec'],_0x435689['redAudio'],_0x435689['fecAudio']);}}catch(_0x39cf49){errorlog(_0x39cf49),warnlog('couldn\x27t\x20set\x20preferred\x20audio\x20codec');}if(_0x435689[_0x186d3a(0x8df)]&&_0x435689['codecs']['length'])for(var _0x25bc33=_0x435689['codecs'][_0x186d3a(0xb04)]-0x1;_0x25bc33>=0x0;_0x25bc33--){try{_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler[_0x186d3a(0x797)](_0x3ac11f['sdp'],_0x435689[_0x186d3a(0x8df)][_0x25bc33],_0x435689[_0x186d3a(0xa68)]);}catch(_0x3210b3){errorlog(_0x3210b3);break;}}_0x435689[_0x186d3a(0x1a3)]&&(_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler[_0x186d3a(0x797)](_0x3ac11f['sdp'],_0x435689[_0x186d3a(0x1a3)],_0x435689['videoErrorCorrection']));try{var _0x5e50c4=![];try{_0x5e50c4=_0x3ac11f&&_0x3ac11f[_0x186d3a(0x795)]&&(_0x3ac11f['sdp'][_0x186d3a(0x41e)](/^m=video /mg)||[])[_0x186d3a(0xb04)]>0x1||![];}catch(_0x24671b){}var _0x5676b1=_0x435689[_0x186d3a(0xc6c)][_0x285abd[_0x186d3a(0x9d6)]]&&_0x435689[_0x186d3a(0xc6c)][_0x285abd['UUID']][_0x186d3a(0x8bd)]&&_0x435689[_0x186d3a(0xc6c)][_0x285abd[_0x186d3a(0x9d6)]][_0x186d3a(0x8bd)][_0x186d3a(0xb04)]>0x0||![],_0x2a0d1a=_0x435689[_0x186d3a(0xc6c)][_0x285abd[_0x186d3a(0x9d6)]]&&_0x435689[_0x186d3a(0xc6c)][_0x285abd['UUID']][_0x186d3a(0x36c)]||![];_0x435689[_0x186d3a(0x373)]&&!(_0x5e50c4||_0x5676b1||_0x2a0d1a)&&(log('h264profile\x20being\x20modified'),_0x3ac11f[_0x186d3a(0x795)]=_0x3ac11f[_0x186d3a(0x795)]['replace'](/42e01f/gi,_0x435689[_0x186d3a(0x373)]),_0x3ac11f['sdp']=_0x3ac11f[_0x186d3a(0x795)]['replace'](/42001f/gi,_0x435689['h264profile']),_0x3ac11f[_0x186d3a(0x795)]=_0x3ac11f['sdp']['replace'](/420029/gi,_0x435689[_0x186d3a(0x373)]),_0x3ac11f[_0x186d3a(0x795)]=_0x3ac11f[_0x186d3a(0x795)][_0x186d3a(0x586)](/42a01e/gi,_0x435689['h264profile']),_0x3ac11f[_0x186d3a(0x795)]=_0x3ac11f['sdp'][_0x186d3a(0x586)](/42a014/gi,_0x435689[_0x186d3a(0x373)]),_0x3ac11f[_0x186d3a(0x795)]=_0x3ac11f[_0x186d3a(0x795)]['replace'](/42a00b/gi,_0x435689[_0x186d3a(0x373)]),_0x3ac11f[_0x186d3a(0x795)]=_0x3ac11f['sdp'][_0x186d3a(0x586)](/640c1f/gi,_0x435689[_0x186d3a(0x373)]),_0x435689[_0x186d3a(0xc6c)][_0x285abd[_0x186d3a(0x9d6)]]&&(_0x435689[_0x186d3a(0xc6c)][_0x285abd[_0x186d3a(0x9d6)]][_0x186d3a(0x36c)]=!![]));}catch(_0x276255){errorlog(_0x276255);}_0x435689[_0x186d3a(0x9d5)]&&(_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler['disablePLI'](_0x3ac11f[_0x186d3a(0x795)]));_0x435689[_0x186d3a(0xbac)]&&(_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler[_0x186d3a(0x335)](_0x3ac11f[_0x186d3a(0x795)]));_0x435689['noNacks']&&(log(_0x3ac11f[_0x186d3a(0x795)]),_0x3ac11f[_0x186d3a(0x795)]=CodecsHandler[_0x186d3a(0x811)](_0x3ac11f[_0x186d3a(0x795)]));if(_0x435689[_0x186d3a(0xc6c)][_0x285abd['UUID']][_0x186d3a(0x358)])log(_0x186d3a(0x1bf)),_0x3ac11f[_0x186d3a(0x795)]=_0x128ce6(_0x3ac11f['sdp'],_0x435689[_0x186d3a(0xc6c)][_0x285abd[_0x186d3a(0x9d6)]][_0x186d3a(0x358)]);else _0x435689['bitrate']&&(log(_0x186d3a(0x1bf)),_0x3ac11f['sdp']=_0x128ce6(_0x3ac11f[_0x186d3a(0x795)],_0x435689[_0x186d3a(0xaa6)]));return _0x435689[_0x186d3a(0xb8f)]&&(_0x3ac11f[_0x186d3a(0x795)]=filterSDPLAN(_0x3ac11f[_0x186d3a(0x795)])),_0x435689[_0x186d3a(0x961)]&&(_0x3ac11f[_0x186d3a(0x795)]=filterStunOnly(_0x3ac11f[_0x186d3a(0x795)])),log(_0x3ac11f),_0x435689[_0x186d3a(0xc6c)][_0x285abd[_0x186d3a(0x9d6)]][_0x186d3a(0x9de)](_0x3ac11f);})[_0x26c65f(0x7b6)](function _0x34787d(){var _0x126a3d=_0x26c65f;log(_0x126a3d(0x4e2));if(_0x435689[_0x126a3d(0xc6c)][_0x285abd['UUID']][_0x126a3d(0x4fb)]){_0x435689[_0x126a3d(0xc6c)][_0x285abd[_0x126a3d(0x9d6)]]['whipCallback']&&_0x435689[_0x126a3d(0xc6c)][_0x285abd['UUID']]['whipCallback']();return;}var _0x187898={};_0x187898[_0x126a3d(0x9d6)]=_0x285abd[_0x126a3d(0x9d6)],_0x187898[_0x126a3d(0x8e6)]=filterDescriptionIpv6(_0x435689[_0x126a3d(0xc6c)][_0x285abd['UUID']][_0x126a3d(0x935)]),_0x187898[_0x126a3d(0x767)]=_0x435689['rpcs'][_0x285abd[_0x126a3d(0x9d6)]]['session'],_0x435689[_0x126a3d(0x28c)]&&_0x435689[_0x126a3d(0xc6c)][_0x285abd['UUID']][_0x126a3d(0x153)]?_0x435689[_0x126a3d(0x504)](JSON[_0x126a3d(0xc18)](_0x187898[_0x126a3d(0x8e6)]))[_0x126a3d(0x7b6)](function(_0x3e1f3b){var _0x1074aa=_0x126a3d;_0x187898['description']=_0x3e1f3b[0x0],_0x187898['vector']=_0x3e1f3b[0x1],_0x435689[_0x1074aa(0x9cb)](_0x187898);})[_0x126a3d(0x501)](errorlog):_0x435689[_0x126a3d(0x9cb)](_0x187898);})[_0x26c65f(0x501)](errorlog);}else _0x435689[_0x26c65f(0xc6c)][_0x285abd[_0x26c65f(0x9d6)]][_0x26c65f(0x67b)][_0x26c65f(0x2dd)]===_0x26c65f(0x34a)&&errorlog(_0x26c65f(0x825));})[_0x386f4f(0x501)](function(_0x33903c){var _0x235fc1=_0x386f4f;errorlog(_0x33903c),_0x285abd[_0x235fc1(0x8e6)]&&errorlog(_0x285abd['description'][_0x235fc1(0x795)]);});},_0x435689[_0x120577(0x6da)]=function(_0x54a4ac,_0x5ca702={}){var _0x37c476=_0x120577;const _0x4e19a6=_0x435689[_0x37c476(0xc6c)][_0x54a4ac],_0x42ef71=typeof _0x5ca702['base']===_0x37c476(0x91d)?_0x5ca702['base']:0xc8;if(!_0x4e19a6||!_0x4e19a6[_0x37c476(0x5c2)])return _0x42ef71;if(!(_0x42ef71>0x0))return 0x0;const _0x3d2b8b=_0x5ca702[_0x37c476(0x6fd)]==='audio'?_0x37c476(0x2de):_0x37c476(0x57d),_0x1640e6=_0x5ca702[_0x37c476(0x5bb)]||{},_0x4b2eaa=typeof _0x1640e6['min']===_0x37c476(0x91d)?_0x1640e6[_0x37c476(0x452)]:0x0,_0x18d68f=_0x3d2b8b===_0x37c476(0x2de)?0x1388:0x7530,_0x1c1d2c=typeof _0x1640e6[_0x37c476(0x50b)]==='number'?_0x1640e6[_0x37c476(0x50b)]:_0x18d68f;let _0x5f5801=_0x42ef71,_0x196545=_0x42ef71,_0x126f7=_0x42ef71,_0x37bb1d=_0x42ef71,_0x24ad94=![],_0x22d447=Infinity;const _0xbeb466=_0x3d2b8b===_0x37c476(0x2de)?_0x4e19a6[_0x37c476(0x5c2)]['chunked_mode_audio']:_0x4e19a6[_0x37c476(0x5c2)]['chunked_mode_video'];if(_0xbeb466){typeof _0xbeb466['buffer_buffer']==='number'&&Number[_0x37c476(0x7a3)](_0xbeb466[_0x37c476(0x4e9)])&&(_0x196545=_0xbeb466[_0x37c476(0x4e9)]);typeof _0xbeb466[_0x37c476(0x414)]===_0x37c476(0x91d)&&Number[_0x37c476(0x7a3)](_0xbeb466[_0x37c476(0x414)])&&(_0x126f7=_0xbeb466[_0x37c476(0x414)]);if(typeof _0xbeb466[_0x37c476(0x4d1)]===_0x37c476(0x91d)&&Number[_0x37c476(0x7a3)](_0xbeb466[_0x37c476(0x4d1)]))_0x37bb1d=_0xbeb466[_0x37c476(0x4d1)];else{const _0xe27847=typeof _0xbeb466[_0x37c476(0x4e9)]===_0x37c476(0x91d)?_0xbeb466['buffer_buffer']:_0x42ef71;_0x37bb1d=Math[_0x37c476(0x50b)](0x0,_0xe27847-_0x126f7);}_0x24ad94=!!_0xbeb466[_0x37c476(0xa62)],_0xbeb466[_0x37c476(0xa72)]&&Number['isFinite'](_0xbeb466[_0x37c476(0xa72)])&&(_0x22d447=Date[_0x37c476(0x1ab)]()-_0xbeb466[_0x37c476(0xa72)]);}_0x24ad94&&(_0x5f5801=Math[_0x37c476(0x50b)](_0x5f5801,_0x42ef71+Math['max'](0x50,_0x42ef71*0.25)));_0x22d447<0x1388&&(_0x5f5801=Math[_0x37c476(0x50b)](_0x5f5801,_0x42ef71+Math['max'](0x78,_0x42ef71*0.4)));if(_0x126f7<=_0x42ef71*0.3){const _0x1ef2bc=_0x42ef71-_0x126f7;_0x1ef2bc>0x14&&(_0x5f5801=Math[_0x37c476(0x50b)](_0x5f5801,_0x42ef71+Math[_0x37c476(0x50b)](0x3c,_0x1ef2bc*0.6)));}const _0x41f02c=_0x42ef71*0.3;if(_0x37bb1d<=_0x41f02c){const _0xc4f0a1=_0x41f02c-_0x37bb1d;_0xc4f0a1>0x0&&(_0x5f5801=Math['max'](_0x5f5801,_0x42ef71+Math['max'](0x3c,_0xc4f0a1*1.2)));}const _0x1cf9ab=_0x4e19a6['stats']['Peer-to-Peer_Connection']||_0x4e19a6[_0x37c476(0x5c2)][_0x37c476(0x2d3)];if(_0x435689[_0x37c476(0x9b7)]&&_0x1cf9ab&&_0x1cf9ab['Round_Trip_Time_ms']){const _0xaa701f=parseFloat(_0x1cf9ab[_0x37c476(0x429)]);Number['isFinite'](_0xaa701f)&&_0xaa701f>0x0&&(_0x5f5801=Math[_0x37c476(0x50b)](_0x5f5801,_0x42ef71+_0xaa701f/0x2));}Number['isFinite'](_0x4e19a6[_0x37c476(0x5c2)]['packetLoss'])&&_0x4e19a6[_0x37c476(0x5c2)][_0x37c476(0x1b5)]>0.03&&(_0x5f5801=Math[_0x37c476(0x50b)](_0x5f5801,_0x42ef71+_0x4e19a6[_0x37c476(0x5c2)][_0x37c476(0x1b5)]*0x3e8));Number[_0x37c476(0x7a3)](_0x4e19a6[_0x37c476(0x5c2)][_0x37c476(0x637)])&&_0x4e19a6['stats'][_0x37c476(0x637)]>0.02&&(_0x5f5801=Math[_0x37c476(0x50b)](_0x5f5801,_0x42ef71+_0x4e19a6[_0x37c476(0x5c2)][_0x37c476(0x637)]*0xfa0));if(!_0x24ad94&&_0x22d447>0x1388&&_0x196545>_0x42ef71){const _0x155d9d=Math[_0x37c476(0x50b)](0x14,_0x42ef71*0.1),_0x350ff6=Math[_0x37c476(0x50b)](_0x42ef71,_0x196545-_0x155d9d);_0x5f5801=Math[_0x37c476(0x50b)](_0x5f5801,_0x350ff6);}return _0x5f5801=Math[_0x37c476(0x50b)](_0x4b2eaa,Math[_0x37c476(0x452)](_0x5f5801,_0x1c1d2c)),Math[_0x37c476(0x5de)](_0x5f5801);},_0x435689['restartChunkedMode']=async function(_0x4bdd18){var _0x123636=_0x120577;errorlog(_0x123636(0xa36)),await new Promise(_0x2a154a=>setTimeout(_0x2a154a,0x7d0));try{_0x435689[_0x123636(0x641)]=null,await _0x435689[_0x123636(0x188)](_0x4bdd18),log(_0x123636(0x26a));}catch(_0x44bbe4){errorlog('Failed\x20to\x20restart\x20chunked\x20mode:',_0x44bbe4),!_0x435689[_0x123636(0x326)]&&warnUser(_0x123636(0xb49));}},_0x435689[_0x120577(0x226)]=function(){var _0x33f836=_0x120577;if(_0x435689[_0x33f836(0x917)]&&_0x435689[_0x33f836(0x917)]['srcObject'])return _0x435689[_0x33f836(0x917)][_0x33f836(0x200)];else return _0x435689[_0x33f836(0x917)]&&_0x435689[_0x33f836(0x917)][_0x33f836(0x4f7)]&&_0x435689[_0x33f836(0x857)]?_0x435689[_0x33f836(0x857)]:(log(_0x33f836(0xc66)),checkBasicStreamsExist(),_0x435689['videoElement']['srcObject']);};var _0x50bedc=0x0,_0x3cf488=0x0;_0x435689[_0x120577(0x188)]=async function(_0x44f5fb=null){var _0x22aada=_0x120577;if(_0x435689[_0x22aada(0x641)]!==null)return;else _0x435689[_0x22aada(0x641)]=![];!_0x44f5fb&&_0x435689[_0x22aada(0x5c2)][_0x22aada(0x835)]&&(_0x44f5fb=_0x435689[_0x22aada(0x5c2)][_0x22aada(0x835)]);let _0x5461a3=0x0;var _0xfae9b7=_0x435689['getLocalStream']()[_0x22aada(0x4df)]();if(!_0xfae9b7||!_0xfae9b7[_0x22aada(0xb04)]){warnlog(_0x22aada(0xc6a)),_0x435689[_0x22aada(0x641)]=null;return;}_0xfae9b7=_0xfae9b7[0x0];var _0x320cc3=new MediaStreamTrackProcessor(_0xfae9b7),_0x5616f5=_0x320cc3['readable'];const _0x3b0b08=_0x5616f5[_0x22aada(0xb5d)]();_0x3cf488+=0x1,_0x3b0b08[_0x22aada(0x680)]=_0x3cf488;var _0x1cf152=![],_0x3f07fd=-0x1,_0x1086f9=-0x1;let _0x19b1f2=![];const _0x529748=()=>{var _0x28640d=_0x22aada;if(_0x19b1f2)return;_0x19b1f2=!![];try{_0x3b0b08[_0x28640d(0x4ed)]();}catch(_0xcc5139){}};let _0x46ab0e;const _0x4b6478={'output':async _0x52f146=>{var _0x1997e7=_0x22aada;if(!_0x435689['chunkedRecorder']||!_0x435689['chunkedRecorder'][_0x1997e7(0x493)])return;else{if(_0x52f146['constructor']['name']=='EncodedVideoChunk'){let _0x4c513d=new Uint8Array(_0x52f146[_0x1997e7(0x893)]);_0x52f146[_0x1997e7(0x52c)](_0x4c513d);typeof _0x435689['chunkedRecorder'][_0x1997e7(0x873)]===_0x1997e7(0x5f9)?_0x435689['chunkedRecorder'][_0x1997e7(0x873)]({'media':_0x1997e7(0x57d),'frameType':_0x52f146[_0x1997e7(0x2dd)],'timestamp':_0x52f146[_0x1997e7(0x95f)]-_0x1086f9,'data':_0x4c513d}):(_0x435689['chunksQueue'][_0x1997e7(0x35c)]([_0x52f146['timestamp']-_0x1086f9,_0x52f146['type']]),_0x435689[_0x1997e7(0xb35)][_0x1997e7(0x35c)](_0x4c513d));_0x435689['chunkIframe']&&pokeIframeAPI(_0x1997e7(0x477),{'type':_0x52f146[_0x1997e7(0x2dd)],'ts':_0x52f146['timestamp']-_0x1086f9});try{await _0x435689[_0x1997e7(0x826)]['sendChunks'](_0x1997e7(0x57d));}catch(_0xb00ed9){errorlog(_0xb00ed9);}}}},'error':_0x244606=>{var _0x24aeac=_0x22aada;errorlog(_0x244606),_0x1cf152=!![],_0x435689[_0x24aeac(0x641)]=null;_0x435689[_0x24aeac(0x826)]&&(_0x435689['chunkedRecorder'][_0x24aeac(0x854)]=!![]);try{_0x3b0b08&&_0x3b0b08[_0x24aeac(0x16a)]()[_0x24aeac(0x501)](()=>{});}catch(_0x2a8e9c){}_0x529748();try{_0x46ab0e&&_0x46ab0e[_0x24aeac(0x203)]!=='closed'&&_0x46ab0e['close']();}catch(_0x3914d3){}_0x435689[_0x24aeac(0x9e0)](_0x44f5fb);}};_0x46ab0e=new VideoEncoder(_0x4b6478),_0x46ab0e[_0x22aada(0x9c2)]=_0x44f5fb,_0x46ab0e[_0x22aada(0xc1b)](_0x44f5fb),_0x435689[_0x22aada(0x5c2)][_0x22aada(0x835)]=_0x44f5fb,_0x435689[_0x22aada(0x826)][_0x22aada(0x474)]=_0x46ab0e;var _0x427f54,_0x5df204=new Promise((_0x4da7eb,_0xa35986)=>{_0x427f54=_0x4da7eb;});return _0x5df204['resolve']=_0x427f54,_0x3b0b08[_0x22aada(0x53c)]()[_0x22aada(0x7b6)](function _0xbf3b53({done:_0x5963b0,value:_0x176ad3}){var _0x4ca8b5=_0x22aada;if(_0x5963b0||_0x1cf152){try{_0x46ab0e[_0x4ca8b5(0x757)]();}catch(_0x512f4b){}_0x176ad3&&_0x176ad3[_0x4ca8b5(0x757)]();warnlog('frameReader.read().then(function'),_0x529748();return;}else{if(_0x46ab0e['state']==_0x4ca8b5(0x94c)){_0x176ad3&&_0x176ad3['close']();warnlog(_0x4ca8b5(0x989)),_0x529748();return;}}_0x1086f9==-0x1&&(_0x1086f9=_0x176ad3[_0x4ca8b5(0x95f)],_0x435689[_0x4ca8b5(0x5c2)][_0x4ca8b5(0x835)][_0x4ca8b5(0x433)]=Date[_0x4ca8b5(0x1ab)](),_0x5df204['resolve']());_0x3f07fd==_0x176ad3[_0x4ca8b5(0x95f)]&&(_0x176ad3['timestamp']+=0x1,warnlog('Timestamp\x20duplicated'));if(!_0x1cf152){_0x3f07fd=_0x176ad3['timestamp'],_0x5461a3++;if(_0x435689[_0x4ca8b5(0x826)]['needKeyFrame']){const _0xe649b1=_0x5461a3>=0x3c;_0xe649b1&&(_0x5461a3=0x0,_0x435689['chunkedRecorder'][_0x4ca8b5(0x854)]=![],warnlog('Keyframe\x20inserted'));try{_0x46ab0e[_0x4ca8b5(0xc25)](_0x176ad3,{'keyFrame':_0xe649b1});}catch(_0x35d9d8){errorlog(_0x35d9d8);}}else try{_0x46ab0e[_0x4ca8b5(0xc25)](_0x176ad3,{'keyFrame':![]});}catch(_0x1633fc){errorlog(_0x1633fc);}}_0x176ad3['close'](),_0x3b0b08[_0x4ca8b5(0x53c)]()[_0x4ca8b5(0x7b6)](_0xbf3b53);}),_0x435689[_0x22aada(0x641)]=!![],_0x5df204;},_0x435689[_0x120577(0x43e)]=async function(_0x1c6a1c){var _0x4c8fb5=_0x120577;if(_0x435689[_0x4c8fb5(0x3c9)]!==null)return;else _0x435689[_0x4c8fb5(0x3c9)]=![];!_0x1c6a1c&&_0x435689[_0x4c8fb5(0x5c2)][_0x4c8fb5(0xa38)]&&(_0x1c6a1c=_0x435689[_0x4c8fb5(0x5c2)][_0x4c8fb5(0xa38)]);var _0x1e1276=_0x435689[_0x4c8fb5(0x226)](),_0x2c4c6f=_0x1e1276[_0x4c8fb5(0x253)]();if(!_0x2c4c6f||!_0x2c4c6f[_0x4c8fb5(0xb04)]){_0x435689[_0x4c8fb5(0x3c9)]=null;return;}_0x2c4c6f=_0x2c4c6f[0x0];var _0x51cfcf=_0x2c4c6f[_0x4c8fb5(0x3ef)]();_0x1c6a1c[_0x4c8fb5(0x1c0)]>_0x51cfcf[_0x4c8fb5(0x136)]&&(_0x1c6a1c[_0x4c8fb5(0x1c0)]=_0x51cfcf[_0x4c8fb5(0x136)],_0x1c6a1c[_0x4c8fb5(0x4b2)]=_0x51cfcf[_0x4c8fb5(0x136)]);if(_0x1c6a1c[_0x4c8fb5(0x148)]!=_0x51cfcf[_0x4c8fb5(0x148)])try{_0x1e1276=outboundAudioPipeline();}catch(_0x5abd36){errorlog(_0x5abd36);}var _0x26d836=new MediaStreamTrackProcessor(_0x1e1276[_0x4c8fb5(0x253)]()[0x0]),_0x11a0f1=_0x26d836['readable'];const _0x923963=_0x11a0f1[_0x4c8fb5(0xb5d)]();var _0x3c00d2=![],_0x207c6f=-0x1,_0x5a71fc=-0x1;const _0x5b6b96={'output':async _0x571154=>{var _0x5cc69e=_0x4c8fb5;if(!_0x435689['chunkedRecorder']||!_0x435689[_0x5cc69e(0x826)][_0x5cc69e(0x493)])return;else{if(_0x571154[_0x5cc69e(0x31a)][_0x5cc69e(0x88c)]==_0x5cc69e(0xbc9)){let _0x553772=new Uint8Array(_0x571154[_0x5cc69e(0x893)]);_0x571154[_0x5cc69e(0x52c)](_0x553772);typeof _0x435689[_0x5cc69e(0x826)][_0x5cc69e(0x873)]===_0x5cc69e(0x5f9)?_0x435689[_0x5cc69e(0x826)][_0x5cc69e(0x873)]({'media':_0x5cc69e(0x2de),'frameType':_0x5cc69e(0x2de),'timestamp':_0x571154[_0x5cc69e(0x95f)]-_0x5a71fc,'data':_0x553772}):(_0x435689[_0x5cc69e(0xb35)][_0x5cc69e(0x35c)]([_0x571154['timestamp']-_0x5a71fc,_0x5cc69e(0x2de)]),_0x435689[_0x5cc69e(0xb35)]['push'](_0x553772));_0x435689[_0x5cc69e(0x60a)]&&pokeIframeAPI(_0x5cc69e(0x477),{'type':_0x5cc69e(0x2de),'ts':_0x571154[_0x5cc69e(0x95f)]-_0x5a71fc});try{await _0x435689[_0x5cc69e(0x826)][_0x5cc69e(0x493)]('audio');}catch(_0x1ac627){errorlog(_0x1ac627);if(!_0x435689[_0x5cc69e(0x826)]){}}}}},'error':_0x52867b=>{errorlog(_0x52867b);}};let _0x26f9df=new AudioEncoder(_0x5b6b96);_0x26f9df[_0x4c8fb5(0x9c2)]=_0x1c6a1c,_0x26f9df[_0x4c8fb5(0xc1b)](_0x1c6a1c),_0x435689[_0x4c8fb5(0x5c2)][_0x4c8fb5(0xa38)]={},_0x435689['stats'][_0x4c8fb5(0xa38)][_0x4c8fb5(0x1a3)]=_0x1c6a1c[_0x4c8fb5(0x1a3)],_0x435689['stats'][_0x4c8fb5(0xa38)]['numberOfChannels']=_0x1c6a1c[_0x4c8fb5(0x1c0)],_0x435689['stats'][_0x4c8fb5(0xa38)][_0x4c8fb5(0x148)]=_0x1c6a1c[_0x4c8fb5(0x148)],_0x435689[_0x4c8fb5(0x5c2)][_0x4c8fb5(0xa38)][_0x4c8fb5(0xaa6)]=_0x1c6a1c[_0x4c8fb5(0x5ee)][_0x4c8fb5(0xaa6)];var _0x322f43,_0x1f251a=new Promise((_0x820113,_0x2b67e0)=>{_0x322f43=_0x820113;});return _0x1f251a['resolve']=_0x322f43,_0x923963[_0x4c8fb5(0x53c)]()[_0x4c8fb5(0x7b6)](function _0x3f50e2({done:_0x5db055,value:_0x1dc4fa}){var _0x137c09=_0x4c8fb5;if(_0x5db055||_0x3c00d2){_0x26f9df[_0x137c09(0x757)]();_0x1dc4fa&&_0x1dc4fa['close']();_0x435689[_0x137c09(0x3c9)]=null;return;}else{if(_0x26f9df['state']==_0x137c09(0x94c)){_0x1dc4fa&&_0x1dc4fa[_0x137c09(0x757)]();_0x435689['chunkedAudioEnabled']=null;return;}}try{_0x5a71fc==-0x1&&(_0x5a71fc=_0x1dc4fa[_0x137c09(0x95f)],_0x435689['stats'][_0x137c09(0xa38)][_0x137c09(0x433)]=Date[_0x137c09(0x1ab)](),_0x1f251a[_0x137c09(0x55b)]());_0x207c6f==_0x1dc4fa['timestamp']&&(_0x1dc4fa['timestamp']+=0x1);if(!_0x3c00d2){_0x207c6f=_0x1dc4fa[_0x137c09(0x95f)];try{_0x26f9df[_0x137c09(0xc25)](_0x1dc4fa);}catch(_0x217136){errorlog(_0x217136);}}_0x1dc4fa[_0x137c09(0x757)](),_0x923963[_0x137c09(0x53c)]()[_0x137c09(0x7b6)](_0x3f50e2);}catch(_0x1642ed){errorlog(_0x1642ed),errorlog(_0x1dc4fa),errorlog(_0x5db055);}}),_0x435689['chunkedAudioEnabled']=!![],_0x1f251a;},_0x435689['getPCM']=function(_0x1709cc,_0x5a2f5a={}){var _0x32a811=_0x120577;warnlog(_0x32a811(0x32d));const _0x44c13a=new window[(_0x32a811(0x881))]({'sampleRate':_0x5a2f5a[_0x32a811(0x148)]||0xbb80}),_0x5ed1cf=_0x44c13a['createMediaStreamSource'](_0x1709cc),_0x2b37b4=0x800,_0x156c8d=(_0x44c13a[_0x32a811(0xb54)]||_0x44c13a[_0x32a811(0x858)])[_0x32a811(0x7f3)](_0x44c13a,_0x2b37b4,0x1,0x1);return _0x156c8d[_0x32a811(0x8bc)]=async function(_0xd437f0){var _0xe76983=_0x32a811,_0x3e669d=new Uint8Array(_0xd437f0[_0xe76983(0x72b)][_0xe76983(0x1fd)](0x0)[_0xe76983(0x237)]);_0x435689['chunksQueue']['push']([0x0,_0xe76983(0x5c4)]),_0x435689['chunksQueue'][_0xe76983(0x35c)](_0x3e669d);try{await _0x435689['chunkedRecorder'][_0xe76983(0x493)](_0xe76983(0x5c4));}catch(_0x496d18){errorlog(_0x496d18),!_0x435689[_0xe76983(0x826)]&&encoder[_0xe76983(0x757)]();}},_0x5ed1cf[_0x32a811(0x913)](_0x156c8d),_0x156c8d['connect'](_0x44c13a['destination']),_0x435689[_0x32a811(0x5c2)][_0x32a811(0xa38)]={},_0x435689[_0x32a811(0x3c9)]=!![],_0x156c8d;},_0x435689[_0x120577(0x181)]=async function(_0x5c3c34=![],_0x46b358=![]){var _0x580d81=_0x120577;if(!_0x435689[_0x580d81(0x826)]){warnlog(_0x580d81(0x7fb));var _0x206f75=null;_0x435689[_0x580d81(0x826)]={};const _0x1c670e=Number[_0x580d81(0x7a3)](_0x435689[_0x580d81(0x806)])?_0x435689['chunkchunksize']:parseInt(_0x435689[_0x580d81(0x806)])||0x4000,_0x3f7d76=Math[_0x580d81(0x50b)](0x800,Math[_0x580d81(0x452)](0x10000,_0x1c670e)),_0x597420=Number[_0x580d81(0x7a3)](_0x435689[_0x580d81(0xb67)])?_0x435689['chunkfec']:parseInt(_0x435689[_0x580d81(0xb67)])||0x0,_0xc161e4=_0x597420>=0x2?_0x597420:0x0,_0x438793=!!_0x435689['chunknack'],_0x3b9a98=_0x438793||_0xc161e4>=0x2,_0x13502b=Number[_0x580d81(0x7a3)](_0x435689[_0x580d81(0x151)])?_0x435689[_0x580d81(0x151)]:parseInt(_0x435689['chunkretry'])||0x0,_0x4be665=Number[_0x580d81(0x7a3)](_0x435689[_0x580d81(0x6c9)])?_0x435689[_0x580d81(0x6c9)]:parseInt(_0x435689[_0x580d81(0x6c9)])||0x0,_0x2a99ed=Number['isFinite'](_0x435689[_0x580d81(0x422)])?_0x435689[_0x580d81(0x422)]:0x1f4,_0x58b693=_0x4be665||Math[_0x580d81(0x50b)](0x1770,_0x13502b||0x0,_0x2a99ed*0x6),_0x35219b=0x4000000;_0x435689[_0x580d81(0x826)]['reliability']={'enabled':_0x3b9a98,'chunkSize':_0x3f7d76,'fecRate':_0xc161e4,'nack':_0x438793,'cache':new Map(),'cacheOrder':[],'cacheBytes':0x0,'maxCacheMs':_0x58b693,'maxCacheBytes':_0x35219b,'nextFrameId':Math[_0x580d81(0xb64)]()*0x7fffffff|0x0,'stats':{'fecParityBlocks':0x0,'retransmits':0x0}},_0x435689[_0x580d81(0x826)]['ensureReliabilityFrameId']=function(){var _0xd3738=_0x580d81;const _0x228bae=_0x435689[_0xd3738(0x826)][_0xd3738(0xbb1)];return _0x228bae[_0xd3738(0x856)]=_0x228bae[_0xd3738(0x856)]+0x1>>>0x0,_0x228bae[_0xd3738(0x856)]>0xfffffffe&&(_0x228bae[_0xd3738(0x856)]=0x1),_0x228bae[_0xd3738(0x856)];},_0x435689['chunkedRecorder']['pruneReliabilityCache']=function(){var _0x4ca0b4=_0x580d81;const _0x15a323=_0x435689[_0x4ca0b4(0x826)][_0x4ca0b4(0xbb1)];if(!_0x15a323[_0x4ca0b4(0x79a)])return;const _0xc26952=Date[_0x4ca0b4(0x1ab)]();while(_0x15a323['cacheOrder']['length']){const _0x4a0a3d=_0x15a323[_0x4ca0b4(0x766)][0x0],_0x10611a=_0x15a323['cache']['get'](_0x4a0a3d);if(!_0x10611a){_0x15a323[_0x4ca0b4(0x766)][_0x4ca0b4(0x57c)]();continue;}const _0x2ce590=_0xc26952-_0x10611a['createdAt'];if(_0x15a323['cacheBytes']>_0x15a323[_0x4ca0b4(0x30f)]||_0x2ce590>_0x15a323[_0x4ca0b4(0x7a4)])_0x15a323[_0x4ca0b4(0x3cc)][_0x4ca0b4(0x5cd)](_0x4a0a3d),_0x15a323[_0x4ca0b4(0x766)][_0x4ca0b4(0x57c)](),_0x15a323[_0x4ca0b4(0x201)]=Math[_0x4ca0b4(0x50b)](0x0,_0x15a323[_0x4ca0b4(0x201)]-_0x10611a[_0x4ca0b4(0x788)]);else break;}},_0x435689[_0x580d81(0x826)][_0x580d81(0x6ad)]=function(_0x4ca838){var _0x4bc4a3=_0x580d81;const _0x45d531=_0x435689['chunkedRecorder'][_0x4bc4a3(0xbb1)];if(!_0x45d531[_0x4bc4a3(0x79a)])return;_0x45d531[_0x4bc4a3(0x3cc)][_0x4bc4a3(0x50e)](_0x4ca838[_0x4bc4a3(0x8b9)],_0x4ca838),_0x45d531[_0x4bc4a3(0x766)]['push'](_0x4ca838[_0x4bc4a3(0x8b9)]),_0x45d531[_0x4bc4a3(0x201)]+=_0x4ca838[_0x4bc4a3(0x788)],_0x435689[_0x4bc4a3(0x826)][_0x4bc4a3(0x3ab)](),!_0x435689[_0x4bc4a3(0x5c2)]&&(_0x435689[_0x4bc4a3(0x5c2)]={}),_0x435689[_0x4bc4a3(0x5c2)][_0x4bc4a3(0xa56)]=_0x45d531[_0x4bc4a3(0x5c2)][_0x4bc4a3(0xa1d)];},_0x435689[_0x580d81(0x826)]['enqueueFrame']=function({media:_0x468f67,frameType:_0xb8658a,timestamp:_0x4a6f09,data:_0x11049b}){var _0x1b8e03=_0x580d81;if(!(_0x11049b instanceof Uint8Array))return;const _0x566e3a=_0x435689[_0x1b8e03(0x826)]['reliability'],_0x25aabb=Math[_0x1b8e03(0x50b)](0x1,Math[_0x1b8e03(0x452)](0x40000,_0x566e3a['chunkSize']||_0x11049b[_0x1b8e03(0x893)])),_0x3060ba=_0x435689[_0x1b8e03(0x826)][_0x1b8e03(0xbcf)](),_0x40886d=_0x435689['chunkedRecorder']['adaptation'];if(_0x468f67===_0x1b8e03(0x57d)&&_0xb8658a!=='key'&&_0x40886d&&(_0x40886d[_0x1b8e03(0xbda)]===_0x1b8e03(0x78b)||_0x40886d[_0x1b8e03(0xbda)]===_0x1b8e03(0x7d8))&&_0x40886d[_0x1b8e03(0x1eb)]>=0x1){_0x40886d[_0x1b8e03(0x1eb)]=Math['max'](0x0,_0x40886d[_0x1b8e03(0x1eb)]-0x1),_0x40886d[_0x1b8e03(0xb14)]=(_0x40886d[_0x1b8e03(0xb14)]||0x0)+0x1,_0x435689['stats'][_0x1b8e03(0x69e)]=_0x40886d['droppedFrames'];return;}const _0x2945e0=[],_0x32eba4=[];let _0x5ea528=0x0,_0x37bf4c=0x0;while(_0x5ea528<_0x11049b[_0x1b8e03(0x893)]){const _0x5e473e=Math[_0x1b8e03(0x452)](_0x11049b[_0x1b8e03(0x893)],_0x5ea528+Math[_0x1b8e03(0x50b)](0x1,_0x25aabb)),_0x923405=_0x11049b[_0x1b8e03(0xb0d)](_0x5ea528,_0x5e473e);_0x2945e0[_0x1b8e03(0x35c)](_0x923405),_0x32eba4[_0x1b8e03(0x35c)]({'index':_0x37bf4c,'size':_0x923405[_0x1b8e03(0x893)],'group':_0x566e3a['fecRate']>=0x2?Math[_0x1b8e03(0x8bb)](_0x37bf4c/_0x566e3a[_0x1b8e03(0x4ad)]):0x0}),_0x5ea528=_0x5e473e,_0x37bf4c+=0x1;}!_0x2945e0[_0x1b8e03(0xb04)]&&(_0x2945e0[_0x1b8e03(0x35c)](new Uint8Array(0x0)),_0x32eba4[_0x1b8e03(0x35c)]({'index':0x0,'size':0x0,'group':0x0}));const _0x58fad7=[],_0x2a7a15=[];if(_0x566e3a[_0x1b8e03(0x79a)]&&_0x566e3a[_0x1b8e03(0x4ad)]>=0x2){for(let _0x534be7=0x0;_0x534be7<_0x2945e0['length'];_0x534be7+=_0x566e3a[_0x1b8e03(0x4ad)]){const _0x1cace0=_0x2945e0[_0x1b8e03(0x3e2)](_0x534be7,_0x534be7+_0x566e3a[_0x1b8e03(0x4ad)]);if(!_0x1cace0[_0x1b8e03(0xb04)])continue;const _0x5b8ba6=_0x1cace0['reduce']((_0x4429de,_0x2a04ff)=>Math['max'](_0x4429de,_0x2a04ff['byteLength']),0x0),_0x2fdb5e=new Uint8Array(_0x5b8ba6);if(_0x5b8ba6)for(const _0x1a3968 of _0x1cace0){for(let _0x3725b2=0x0;_0x3725b2<_0x5b8ba6;_0x3725b2++){const _0xcdbc59=_0x3725b2<_0x1a3968[_0x1b8e03(0x893)]?_0x1a3968[_0x3725b2]:0x0;_0x2fdb5e[_0x3725b2]^=_0xcdbc59;}}_0x58fad7[_0x1b8e03(0x35c)](_0x2fdb5e),_0x2a7a15[_0x1b8e03(0x35c)]({'group':Math[_0x1b8e03(0x8bb)](_0x534be7/_0x566e3a[_0x1b8e03(0x4ad)]),'groupStart':_0x534be7,'groupSize':_0x1cace0['length'],'size':_0x5b8ba6});}_0x566e3a[_0x1b8e03(0x5c2)][_0x1b8e03(0xa1d)]+=_0x58fad7[_0x1b8e03(0xb04)];}const _0x3ac733={'version':0x1,'frameId':_0x3060ba,'media':_0x468f67,'frameType':_0xb8658a,'totalChunks':_0x2945e0['length'],'parityChunks':_0x58fad7[_0x1b8e03(0xb04)],'chunkSize':_0x25aabb,'fecRate':_0x566e3a[_0x1b8e03(0x4ad)],'nack':_0x566e3a[_0x1b8e03(0xa82)],'dataBytes':_0x11049b['byteLength'],'descriptors':_0x32eba4,'parityDescriptors':_0x2a7a15},_0x364df6=[_0x4a6f09,_0xb8658a,_0x3ac733];_0x435689[_0x1b8e03(0xb35)][_0x1b8e03(0x35c)](_0x364df6);for(const _0xb6de0b of _0x2945e0){_0x435689[_0x1b8e03(0xb35)][_0x1b8e03(0x35c)](_0xb6de0b);}for(const _0x1186ed of _0x58fad7){_0x435689[_0x1b8e03(0xb35)][_0x1b8e03(0x35c)](_0x1186ed);}_0x435689['chunkedRecorder'][_0x1b8e03(0x6ad)]({'frameId':_0x3060ba,'timestamp':_0x4a6f09,'media':_0x468f67,'frameType':_0xb8658a,'dataChunks':_0x2945e0,'parityChunks':_0x58fad7,'parityDescriptors':_0x2a7a15,'createdAt':Date[_0x1b8e03(0x1ab)](),'totalBytes':_0x11049b['byteLength']+_0x58fad7[_0x1b8e03(0x7c8)]((_0x51b2a9,_0x21b681)=>_0x51b2a9+_0x21b681['byteLength'],0x0)});},_0x435689[_0x580d81(0x826)]['handleNack']=function(_0x4a0f90,_0x51a50c){var _0x4f598b=_0x580d81;const _0x146a36=_0x435689[_0x4f598b(0x826)][_0x4f598b(0xbb1)];if(!_0x146a36||!_0x146a36[_0x4f598b(0x79a)])return;if(!_0x51a50c||typeof _0x51a50c['frameId']===_0x4f598b(0xba0))return;const _0x4ed98c=_0x51a50c[_0x4f598b(0x8b9)]>>>0x0,_0x97c161=_0x435689[_0x4f598b(0x1d2)][_0x4a0f90];if(!_0x97c161||_0x97c161[_0x4f598b(0x565)]!==_0x4f598b(0x379))return;const _0x59d568=_0x146a36[_0x4f598b(0x3cc)][_0x4f598b(0xa1b)](_0x4ed98c);if(!_0x59d568)return;const _0x1ebf0a=!!_0x51a50c['parity'];let _0x28866e=Number['isFinite'](_0x51a50c[_0x4f598b(0x572)])?parseInt(_0x51a50c[_0x4f598b(0x572)]):-0x1,_0x13bab4=Number['isFinite'](_0x51a50c[_0x4f598b(0xa16)])?parseInt(_0x51a50c[_0x4f598b(0xa16)]):-0x1,_0x35fac3=null,_0x3dd833=null;_0x1ebf0a?(_0x13bab4<0x0&&(_0x13bab4=Math[_0x4f598b(0x50b)](0x0,_0x28866e)),_0x35fac3=_0x59d568[_0x4f598b(0x635)][_0x13bab4],_0x3dd833=_0x59d568[_0x4f598b(0x2a7)][_0x13bab4]||null):_0x35fac3=_0x59d568[_0x4f598b(0x8d4)][_0x28866e];if(!_0x35fac3)return;const _0x295e07=_0x59d568[_0x4f598b(0x2a7)]||[],_0x5835b7={'version':0x1,'frameId':_0x59d568[_0x4f598b(0x8b9)],'media':_0x59d568['media'],'frameType':_0x59d568[_0x4f598b(0x3ba)],'totalChunks':_0x59d568[_0x4f598b(0x8d4)][_0x4f598b(0xb04)],'parityChunks':_0x59d568[_0x4f598b(0x635)][_0x4f598b(0xb04)],'chunkSize':_0x146a36[_0x4f598b(0x1e0)],'fecRate':_0x146a36[_0x4f598b(0x4ad)],'nack':_0x146a36[_0x4f598b(0xa82)],'resend':!![],'parity':_0x1ebf0a,'chunkIndex':_0x1ebf0a?_0x13bab4:_0x28866e,'group':_0x1ebf0a&&_0x3dd833?_0x3dd833[_0x4f598b(0x1cd)]:_0x146a36[_0x4f598b(0x4ad)]>=0x2?Math[_0x4f598b(0x8bb)](Math[_0x4f598b(0x50b)](_0x28866e,0x0)/_0x146a36['fecRate']):0x0,'groupSize':_0x1ebf0a&&_0x3dd833?_0x3dd833['groupSize']:_0x146a36[_0x4f598b(0x4ad)]>=0x2?Math[_0x4f598b(0x452)](_0x146a36['fecRate'],_0x59d568[_0x4f598b(0x8d4)]['length']-Math[_0x4f598b(0x8bb)](Math[_0x4f598b(0x50b)](_0x28866e,0x0)/_0x146a36[_0x4f598b(0x4ad)])*_0x146a36['fecRate']):_0x59d568[_0x4f598b(0x8d4)][_0x4f598b(0xb04)]};try{const _0x76937e=JSON['stringify']([_0x59d568[_0x4f598b(0x95f)],_0x59d568[_0x4f598b(0x3ba)],_0x5835b7,0x0]);_0x97c161[_0x4f598b(0xac5)](_0x76937e),_0x97c161['send'](_0x35fac3),_0x146a36[_0x4f598b(0x5c2)][_0x4f598b(0x430)]+=0x1,!_0x435689[_0x4f598b(0x5c2)]&&(_0x435689[_0x4f598b(0x5c2)]={}),_0x435689[_0x4f598b(0x5c2)][_0x4f598b(0x2b9)]=(_0x435689[_0x4f598b(0x5c2)][_0x4f598b(0x2b9)]||0x0)+0x1,_0x435689[_0x4f598b(0x5c2)][_0x4f598b(0x16e)]=(_0x435689[_0x4f598b(0x5c2)][_0x4f598b(0x16e)]||0x0)+0x1;}catch(_0x2d56e1){errorlog(_0x2d56e1);}},_0x435689[_0x580d81(0xaf6)]=_0x5c3c34||![],_0x435689[_0x580d81(0xaf6)]&&(_0x435689[_0x580d81(0x826)][_0x580d81(0xa12)]=_0x46b358),_0x435689[_0x580d81(0x826)][_0x580d81(0x493)]=async function(_0x4dae07=_0x580d81(0x34f)){var _0x2b0a71=_0x580d81;if(_0x206f75)return;_0x206f75=!![];var _0x3cdbc2=_0x4dae07;const _0x28fbf5=_0x435689[_0x2b0a71(0x826)][_0x2b0a71(0x247)]=_0x435689[_0x2b0a71(0x826)]['viewerHealth']||{};for(const _0x2541e4 in _0x28fbf5){(!_0x435689[_0x2b0a71(0x1d2)][_0x2541e4]||!_0x435689['pcs'][_0x2541e4])&&delete _0x28fbf5[_0x2541e4];}const _0x2ad024={0x0:0x0,0x1:0x0,0x2:0x0};let _0x19b858=0x0;function _0x5f40b6(_0x4feb53){var _0x1c16e4=_0x2b0a71;if(!_0x4feb53)return 0x2;if(_0x4feb53[_0x1c16e4(0xa34)]!==![]&&_0x4feb53[_0x1c16e4(0xa34)]!==undefined&&_0x4feb53['scene']!==null)return 0x0;if(_0x4feb53[_0x1c16e4(0x2ed)]!==null&&_0x4feb53[_0x1c16e4(0x2ed)]!==undefined&&_0x4feb53[_0x1c16e4(0x2ed)]!==![])return 0x0;if(_0x4feb53['guest']===!![]||_0x4feb53[_0x1c16e4(0x15d)]===!![])return 0x2;return 0x1;}function _0x7ebc7f(_0x36e1fa){switch(_0x36e1fa){case 0x0:return 0x180000;case 0x1:return 0x100000;default:return 0xc0000;}}function _0x50dc71(_0x1546c1,_0x7d7444){return!_0x28fbf5[_0x1546c1]&&(_0x28fbf5[_0x1546c1]={'priority':_0x7d7444,'skipped':0x0,'sent':0x0,'buffered':0x0,'stalled':![],'consecutiveHigh':0x0}),_0x28fbf5[_0x1546c1]['priority']=_0x7d7444,_0x28fbf5[_0x1546c1];}function _0x4e6e2d(_0x2014ab,_0x414ee2,_0x34e7c4={}){var _0x42a8d6=_0x2b0a71,_0xd4ef32=_0x435689[_0x42a8d6(0x1d2)][_0x2014ab],_0x5db0f8=_0x435689[_0x42a8d6(0x76f)][_0x2014ab];if(!_0xd4ef32||!_0x5db0f8||_0xd4ef32[_0x42a8d6(0x565)]!==_0x42a8d6(0x379))return![];var _0x43e07f=_0x34e7c4[_0x42a8d6(0x8c3)]===!![];!_0x5db0f8[_0x42a8d6(0x5c2)]&&(_0x5db0f8['stats']={});var _0x5f3dbd=_0x5f40b6(_0x5db0f8),_0x405940=_0x50dc71(_0x2014ab,_0x5f3dbd);!_0xd4ef32[_0x42a8d6(0x8a9)]&&(_0xd4ef32['keyframeSent']=![]);!_0xd4ef32[_0x42a8d6(0x211)]&&(_0xd4ef32['audioHeaderSent']=![]);!_0xd4ef32[_0x42a8d6(0x1dc)]&&(_0xd4ef32[_0x42a8d6(0x1dc)]=![]);if(_0x3cdbc2==='delta'&&!_0xd4ef32[_0x42a8d6(0x8a9)])return warnlog(_0x42a8d6(0x32c)),_0x435689[_0x42a8d6(0x826)]&&(_0x435689[_0x42a8d6(0x826)][_0x42a8d6(0x854)]=!![]),![];if(!_0x43e07f&&(_0x3cdbc2===_0x42a8d6(0x304)||_0x3cdbc2===_0x42a8d6(0x770)||_0x3cdbc2==='video')&&!_0xd4ef32['keyframeSent'])return warnlog(_0x42a8d6(0x32c)),_0x435689[_0x42a8d6(0x826)]&&(_0x435689[_0x42a8d6(0x826)][_0x42a8d6(0x854)]=!![]),![];if(!_0x43e07f&&(_0x3cdbc2==='audio'||_0x3cdbc2===_0x42a8d6(0x5c4))&&!_0xd4ef32[_0x42a8d6(0x211)])return warnlog(_0x42a8d6(0x40c)),![];if(!_0xd4ef32['detailsSent']){if(_0x435689[_0x42a8d6(0xaf6)])try{var _0x13cde6={..._0x435689['chunkedDetails']};_0x13cde6[_0x42a8d6(0x95f)]=Date[_0x42a8d6(0x1ab)](),_0xd4ef32[_0x42a8d6(0xac5)](JSON[_0x42a8d6(0xc18)](_0x13cde6)),_0xd4ef32['detailsSent']=!![];}catch(_0x104025){return _0x405940[_0x42a8d6(0x99e)]=(_0x405940[_0x42a8d6(0x99e)]||0x0)+0x1,_0x405940[_0x42a8d6(0x869)]=!![],_0x435689[_0x42a8d6(0x826)][_0x42a8d6(0x854)]=!![],![];}else _0xd4ef32[_0x42a8d6(0x1dc)]=!![];}var _0x4e5a72=_0xd4ef32[_0x42a8d6(0x953)]||0x0;_0x4e5a72>_0x19b858&&(_0x19b858=_0x4e5a72);_0x4e5a72>_0x2ad024[_0x5f3dbd]&&(_0x2ad024[_0x5f3dbd]=_0x4e5a72);_0x5db0f8[_0x42a8d6(0x5c2)][_0x42a8d6(0x953)]=_0x4e5a72;var _0x5df9a8=_0x3cdbc2==='video'||_0x3cdbc2===_0x42a8d6(0x770),_0x37ee1a=_0x7ebc7f(_0x5f3dbd);_0x5f3dbd===0x0&&(_0x4e5a72>_0x37ee1a*1.5?_0x405940[_0x42a8d6(0x6ab)]=(_0x405940[_0x42a8d6(0x6ab)]||0x0)+0x1:_0x405940[_0x42a8d6(0x6ab)]=0x0);if(_0x5df9a8&&_0x5f3dbd>0x0&&_0x4e5a72>_0x37ee1a)return _0x405940['skipped']=(_0x405940['skipped']||0x0)+0x1,_0x405940[_0x42a8d6(0x25e)]=(_0x405940[_0x42a8d6(0x25e)]||0x0)+0x1,_0x405940[_0x42a8d6(0x697)]=_0x4e5a72,_0x405940['consecutiveHigh']>=0x4&&(_0x405940[_0x42a8d6(0x869)]=!![],_0x435689[_0x42a8d6(0x826)][_0x42a8d6(0x854)]=!![]),![];try{_0xd4ef32[_0x42a8d6(0xac5)](_0x414ee2);}catch(_0x1bb5cb){return _0x405940[_0x42a8d6(0x99e)]=(_0x405940[_0x42a8d6(0x99e)]||0x0)+0x1,_0x405940[_0x42a8d6(0x869)]=!![],_0x435689[_0x42a8d6(0x826)][_0x42a8d6(0x854)]=!![],![];}if(_0x43e07f){if(_0x3cdbc2==_0x42a8d6(0x304)||_0x3cdbc2==_0x42a8d6(0x57d))_0xd4ef32[_0x42a8d6(0x8a9)]=!![];else(_0x3cdbc2==_0x42a8d6(0x2de)||_0x3cdbc2==_0x42a8d6(0x5c4))&&(_0xd4ef32[_0x42a8d6(0x211)]=!![]);}return _0x4e5a72=_0xd4ef32['bufferedAmount']||0x0,_0x4e5a72>_0x19b858&&(_0x19b858=_0x4e5a72),_0x4e5a72>_0x2ad024[_0x5f3dbd]&&(_0x2ad024[_0x5f3dbd]=_0x4e5a72),_0x5db0f8[_0x42a8d6(0x5c2)][_0x42a8d6(0x953)]=_0x4e5a72,_0x405940['buffered']=_0x4e5a72,_0x405940[_0x42a8d6(0x380)]=(_0x405940['sent']||0x0)+0x1,_0x405940[_0x42a8d6(0xa60)]=Date[_0x42a8d6(0x1ab)](),_0x405940[_0x42a8d6(0x25e)]=Math[_0x42a8d6(0x50b)](0x0,(_0x405940[_0x42a8d6(0x25e)]||0x0)-0x1),_0x405940[_0x42a8d6(0x869)]=![],!![];}while(_0x435689['chunksQueue'][_0x2b0a71(0xb04)]){if(!Object['keys'](_0x435689['chunkedTransferChannels'])[_0x2b0a71(0xb04)]){_0x435689[_0x2b0a71(0xb35)]=[],_0x206f75=null,_0x435689[_0x2b0a71(0x5c2)][_0x2b0a71(0x72e)]=0x0;return;}_0x435689['stats'][_0x2b0a71(0x72e)]=_0x435689[_0x2b0a71(0xb35)][_0x2b0a71(0xb04)],_0x19b858=0x0,_0x2ad024[0x0]=0x0,_0x2ad024[0x1]=0x0,_0x2ad024[0x2]=0x0;var _0x3e8274=_0x435689['chunksQueue'][_0x2b0a71(0x57c)]();if(Array[_0x2b0a71(0x45b)](_0x3e8274)){_0x3cdbc2=_0x3e8274[0x1];const _0xb29af1=_0x3e8274['slice']();_0xb29af1[_0x2b0a71(0x35c)](_0x435689[_0x2b0a71(0xb35)][_0x2b0a71(0xb04)]);var _0x1d7521=JSON[_0x2b0a71(0xc18)](_0xb29af1);for(var _0x1170f9 in _0x435689[_0x2b0a71(0x1d2)]){if(!_0x435689[_0x2b0a71(0x1d2)][_0x1170f9])continue;if((_0x3cdbc2==_0x2b0a71(0x304)||_0x3cdbc2=='delta'||_0x3cdbc2=='video')&&!_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x1b1)])continue;if((_0x3cdbc2==_0x2b0a71(0x2de)||_0x3cdbc2==_0x2b0a71(0x5c4))&&!_0x435689['pcs'][_0x1170f9]['allowAudio'])continue;if(!_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0x8a9)]&&_0x3cdbc2=='delta'){warnlog(_0x2b0a71(0x32c));continue;}try{if(_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0x565)]===_0x2b0a71(0x379)){if(!_0x435689['chunkedTransferChannels'][_0x1170f9][_0x2b0a71(0x1dc)]){if(_0x435689['chunkedDetails']){var _0x2262b9={..._0x435689[_0x2b0a71(0xaf6)]};_0x2262b9[_0x2b0a71(0x95f)]=Date[_0x2b0a71(0x1ab)](),_0x435689['chunkedTransferChannels'][_0x1170f9][_0x2b0a71(0xac5)](JSON[_0x2b0a71(0xc18)](_0x2262b9)),_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0x1dc)]=!![];}else continue;}_0x435689['chunkedTransferChannels'][_0x1170f9]['send'](_0x1d7521);if(_0x3cdbc2==_0x2b0a71(0x304)||_0x3cdbc2=='video')_0x435689['chunkedTransferChannels'][_0x1170f9][_0x2b0a71(0x8a9)]=!![];else(_0x3cdbc2==_0x2b0a71(0x2de)||_0x3cdbc2==_0x2b0a71(0x5c4))&&(_0x435689['chunkedTransferChannels'][_0x1170f9]['audioHeaderSent']=!![]);_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x5c2)]['bufferedAmount']=_0x435689['chunkedTransferChannels'][_0x1170f9][_0x2b0a71(0x953)],_0x19b858<_0x435689[_0x2b0a71(0x76f)][_0x1170f9]['stats'][_0x2b0a71(0x953)]&&(_0x19b858=_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x5c2)][_0x2b0a71(0x953)]);}}catch(_0x15818a){}}}else{if(_0x3e8274[_0x2b0a71(0x893)]>0x40000){for(var _0x1170f9 in _0x435689[_0x2b0a71(0x1d2)]){if(!_0x435689[_0x2b0a71(0x1d2)][_0x1170f9])continue;if((_0x3cdbc2==_0x2b0a71(0x304)||_0x3cdbc2==_0x2b0a71(0x770)||_0x3cdbc2=='video')&&!_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x1b1)])continue;if((_0x3cdbc2==_0x2b0a71(0x2de)||_0x3cdbc2==_0x2b0a71(0x5c4))&&!_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x1da)])continue;if((_0x3cdbc2==_0x2b0a71(0x304)||_0x3cdbc2==_0x2b0a71(0x770)||_0x3cdbc2==_0x2b0a71(0x57d))&&!_0x435689['chunkedTransferChannels'][_0x1170f9][_0x2b0a71(0x8a9)]){warnlog(_0x2b0a71(0x32c));continue;}else{if(!_0x435689[_0x2b0a71(0x1d2)][_0x1170f9]['audioHeaderSent']&&(_0x3cdbc2==_0x2b0a71(0x2de)||_0x3cdbc2=='pcm')){warnlog(_0x2b0a71(0x40c));continue;}}try{if(_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0x565)]===_0x2b0a71(0x379)){if(!_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0x1dc)]){if(_0x435689['chunkedDetails']){var _0x2262b9={..._0x435689[_0x2b0a71(0xaf6)]};_0x2262b9[_0x2b0a71(0x95f)]=Date[_0x2b0a71(0x1ab)](),_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0xac5)](JSON[_0x2b0a71(0xc18)](_0x2262b9)),_0x435689['chunkedTransferChannels'][_0x1170f9]['detailsSent']=!![];}else continue;}_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0xac5)](_0x3e8274['slice'](0x0,0x40000)),_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x5c2)][_0x2b0a71(0x953)]=_0x435689[_0x2b0a71(0x1d2)][_0x1170f9]['bufferedAmount'],_0x19b858<_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x5c2)][_0x2b0a71(0x953)]&&(_0x19b858=_0x435689['pcs'][_0x1170f9][_0x2b0a71(0x5c2)][_0x2b0a71(0x953)]);}}catch(_0x179f1d){}}_0x435689['chunksQueue'][_0x2b0a71(0x4a0)](_0x3e8274[_0x2b0a71(0x3e2)](0x40000));}else for(var _0x1170f9 in _0x435689[_0x2b0a71(0x1d2)]){if(!_0x435689['chunkedTransferChannels'][_0x1170f9])continue;if((_0x3cdbc2==_0x2b0a71(0x304)||_0x3cdbc2==_0x2b0a71(0x770)||_0x3cdbc2==_0x2b0a71(0x57d))&&!_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x1b1)])continue;if((_0x3cdbc2==_0x2b0a71(0x2de)||_0x3cdbc2==_0x2b0a71(0x5c4))&&!_0x435689['pcs'][_0x1170f9][_0x2b0a71(0x1da)])continue;try{if(_0x435689['chunkedTransferChannels'][_0x1170f9]['readyState']==='open'){if(!_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0x1dc)]){if(_0x435689[_0x2b0a71(0xaf6)]){var _0x2262b9={..._0x435689[_0x2b0a71(0xaf6)]};_0x2262b9['timestamp']=Date[_0x2b0a71(0x1ab)](),_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0xac5)](JSON[_0x2b0a71(0xc18)](_0x2262b9)),_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0x1dc)]=!![];}else continue;}_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0xac5)](_0x3e8274);}_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x5c2)][_0x2b0a71(0x953)]=_0x435689[_0x2b0a71(0x1d2)][_0x1170f9][_0x2b0a71(0x953)],_0x19b858<_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x5c2)][_0x2b0a71(0x953)]&&(_0x19b858=_0x435689[_0x2b0a71(0x76f)][_0x1170f9][_0x2b0a71(0x5c2)][_0x2b0a71(0x953)]);}catch(_0x2703ec){}}}_0x435689['stats']['maxBufferSize']=_0x19b858;}_0x206f75=null,_0x435689[_0x2b0a71(0x5c2)]['chunkedInQueue']=0x0;};}for(var _0x5e06d3 in _0x435689[_0x580d81(0x76f)]){if(_0x435689[_0x580d81(0x1d2)][_0x5e06d3]){if(_0x435689[_0x580d81(0xaf6)]){var _0x32b814={..._0x435689[_0x580d81(0xaf6)]};_0x32b814[_0x580d81(0x95f)]=Date['now']();if(_0x5c3c34)try{_0x435689['chunkedTransferChannels'][_0x5e06d3]['send'](JSON[_0x580d81(0xc18)](_0x32b814)),_0x435689[_0x580d81(0x1d2)][_0x5e06d3]['detailsSent']=!![];}catch(_0x51985b){}else{if(!_0x435689[_0x580d81(0x1d2)][_0x5e06d3][_0x580d81(0x1dc)])try{_0x435689[_0x580d81(0x1d2)][_0x5e06d3]['send'](JSON['stringify'](_0x32b814)),_0x435689[_0x580d81(0x1d2)][_0x5e06d3][_0x580d81(0x1dc)]=!![];}catch(_0x3ba932){}}}}else{var _0x52af07=_0x580d81(0x6a9);_0x435689['chunkedTransferChannels'][_0x5e06d3]=_0x435689[_0x580d81(0x76f)][_0x5e06d3]['createDataChannel'](_0x52af07,{'ordered':!![]}),_0x435689[_0x580d81(0x1d2)][_0x5e06d3][_0x580d81(0x273)]=_0x580d81(0x4de),_0x435689['chunkedTransferChannels'][_0x5e06d3][_0x580d81(0x33c)]=_0x580d81(0x4fd),_0x435689[_0x580d81(0x1d2)][_0x5e06d3][_0x580d81(0x609)]=![],_0x435689[_0x580d81(0x1d2)][_0x5e06d3][_0x580d81(0x1dc)]=![],_0x435689[_0x580d81(0x1d2)][_0x5e06d3][_0x580d81(0x285)]=null,_0x435689['chunkedTransferChannels'][_0x5e06d3]['keyframeSent']=![],_0x435689[_0x580d81(0x1d2)][_0x5e06d3][_0x580d81(0x211)]=![],_0x435689[_0x580d81(0x1d2)][_0x5e06d3]['onopen']=()=>{var _0x480207=_0x580d81;log('RETRANSMIT\x20chunkedtransfer\x20OPEN');if(_0x435689[_0x480207(0xaf6)]){var _0x1560d7={..._0x435689['chunkedDetails']};_0x1560d7['timestamp']=Date[_0x480207(0x1ab)](),_0x435689[_0x480207(0x1d2)][_0x5e06d3][_0x480207(0xac5)](JSON['stringify'](_0x1560d7)),_0x435689['chunkedTransferChannels'][_0x5e06d3][_0x480207(0x1dc)]=!![];}},_0x435689[_0x580d81(0x1d2)][_0x5e06d3]['onclose']=()=>{var _0xd81e1c=_0x580d81;try{var _0x3824bc=_0x435689[_0xd81e1c(0x569)][_0xd81e1c(0x97c)](_0x435689[_0xd81e1c(0x1d2)][_0x5e06d3]);_0x3824bc>-0x1&&_0x435689['hostedTransfers'][_0xd81e1c(0x20c)](_0x3824bc,0x1);}catch(_0x4440ed){errorlog(_0x4440ed);}log('re-Transfer\x20ended'),_0x435689['chunkedTransferChannels'][_0x5e06d3]=null,delete _0x435689[_0xd81e1c(0x1d2)][_0x5e06d3];var _0x5b9696=![];for(var _0x576c30=0x0;_0x576c30<_0x435689['hostedTransfers']['length'];_0x576c30++){if('contentType'in _0x435689['hostedTransfers'][_0x576c30]&&_0x435689[_0xd81e1c(0x569)][_0x576c30][_0xd81e1c(0x273)]==_0xd81e1c(0x4de)){_0x5b9696=!![];break;}}},_0x435689[_0x580d81(0x1d2)][_0x5e06d3][_0x580d81(0x13c)]=_0x142a9f=>{var _0x2256a5=_0x580d81;if(_0x142a9f['data'])try{var _0x308c39=JSON[_0x2256a5(0x48a)](_0x142a9f[_0x2256a5(0x36f)]);if(_0x308c39['kf'])_0x435689[_0x2256a5(0x826)][_0x2256a5(0xa12)]?(_0x435689[_0x2256a5(0x826)]['upstreamChannel'][_0x2256a5(0xac5)](JSON[_0x2256a5(0xc18)]({'kf':!![]})),warnlog(_0x2256a5(0x9ac))):errorlog(_0x2256a5(0x83d));else _0x308c39[_0x2256a5(0x2dd)]===_0x2256a5(0xa82)&&_0x435689['chunkedRecorder']&&typeof _0x435689[_0x2256a5(0x826)][_0x2256a5(0x1b2)]===_0x2256a5(0x5f9)&&_0x435689['chunkedRecorder']['handleNack'](_0x5e06d3,_0x308c39);}catch(_0x16b147){}},_0x435689['hostedTransfers']['push'](_0x435689['chunkedTransferChannels'][_0x5e06d3]);}}await _0x435689['chunkedRecorder'][_0x580d81(0x493)]();};function _0x25a646(_0x3e3296,_0x3c6ff7=0x2){var _0x5bd8d3=_0x120577;let _0x3857af=Number[_0x5bd8d3(0x7a3)](_0x3e3296)?Math[_0x5bd8d3(0x8bb)](_0x3e3296):_0x3c6ff7;_0x3857af<_0x3c6ff7&&(_0x3857af=_0x3c6ff7);const _0x3156d5=_0x3857af%_0x3c6ff7;return _0x3156d5!==0x0&&(_0x3857af-=_0x3156d5),_0x3857af<_0x3c6ff7&&(_0x3857af=_0x3c6ff7),_0x3857af;}async function _0x5d7961(_0x2d5398=0x500,_0x5a0728=0x2d0,_0x16fb80=0x1e){var _0x25cbbe=_0x120577,_0xbb2765=[_0x25cbbe(0xa19),_0x25cbbe(0xb7e),_0x25cbbe(0x35f),_0x25cbbe(0x391)],_0x17e3c4=[_0x25cbbe(0x7ab),_0x25cbbe(0xbf3)],_0x1c56ba=[];if(_0x435689[_0x25cbbe(0xb53)]){var _0x49a657=[],_0x4ed3ed='keep';for(var _0x31972c of _0xbb2765){for(var _0x31893d of _0x17e3c4){_0x49a657['push']({'codec':_0x31972c,'alpha':_0x4ed3ed,'hardwareAcceleration':_0x31893d,'width':_0x2d5398,'height':_0x5a0728,'bitrate':0x1e8480,'bitrateMode':_0x25cbbe(0xa64),'framerate':_0x16fb80,'latencyMode':_0x25cbbe(0x9f7)});}}for(var _0x3bd6d3=0x0;_0x3bd6d3<_0x49a657[_0x25cbbe(0xb04)];_0x3bd6d3++){var _0x1b8ad9=await VideoEncoder[_0x25cbbe(0x2a4)](_0x49a657[_0x3bd6d3]);_0x1b8ad9&&_0x1b8ad9[_0x25cbbe(0x1af)]&&_0x1c56ba['push'](_0x1b8ad9);}!_0x1c56ba[_0x25cbbe(0xb04)]&&(!_0x435689[_0x25cbbe(0x326)]&&warnUser('Notice:\x20Alpha\x20chunked-mode\x20encoding\x20is\x20not\x20supported\x20by\x20this\x20browser.\x0a\x0aThe\x20vidoe\x20encoder\x20is\x20falling\x20back\x20to\x20non-alpha\x20mode',0x1770));}if(!_0x1c56ba['length']){var _0x49a657=[],_0x4ed3ed=_0x25cbbe(0xac7);for(var _0x31972c of _0xbb2765){for(var _0x31893d of _0x17e3c4){_0x49a657[_0x25cbbe(0x35c)]({'codec':_0x31972c,'alpha':_0x4ed3ed,'hardwareAcceleration':_0x31893d,'width':_0x2d5398,'height':_0x5a0728,'bitrate':0x1e8480,'bitrateMode':'constant','framerate':_0x16fb80,'latencyMode':'realtime'});}}for(var _0x3bd6d3=0x0;_0x3bd6d3<_0x49a657['length'];_0x3bd6d3++){var _0x1b8ad9=await VideoEncoder['isConfigSupported'](_0x49a657[_0x3bd6d3]);_0x1b8ad9&&_0x1b8ad9[_0x25cbbe(0x1af)]&&_0x1c56ba['push'](_0x1b8ad9);}}return _0x1c56ba;}_0x435689[_0x120577(0xa54)]=async function(_0x349405=null){var _0x5b7671=_0x120577;if(_0x349405&&!_0x435689['pcs'][_0x349405][_0x5b7671(0x699)])return;!_0x435689[_0x5b7671(0x641)]&&_0x435689['chunkedRecorder']&&_0x435689['chunkedRecorder']['configVideo']&&await _0x435689[_0x5b7671(0x188)](_0x435689['stats'][_0x5b7671(0x835)]);!_0x435689[_0x5b7671(0x3c9)]&&_0x435689['chunkedRecorder']&&_0x435689[_0x5b7671(0x826)][_0x5b7671(0xa39)]&&await _0x435689['webCodecAudio'](_0x435689['chunkedRecorder'][_0x5b7671(0xa39)]);if(_0x349405){if(_0x349405 in _0x435689[_0x5b7671(0x1d2)]){warnlog(_0x5b7671(0xb38));return;}else _0x435689[_0x5b7671(0x1d2)][_0x349405]=null;}if(!_0x435689['chunkedRecorder']){var _0x331da7=_0x435689['getLocalStream'](),_0x545320=_0x435689['chunked'],_0x572387=null;_0x435689['maxvideobitrate']&&_0x435689[_0x5b7671(0x4d8)]<_0x545320&&(_0x545320=_0x435689[_0x5b7671(0x4d8)]);var _0x5a0086={'codec':'vp09.00.10.08','width':0x780,'height':0x438,'bitrate':parseInt(_0x545320*0x3e8),'frameRate':0x1e,'latencyMode':_0x5b7671(0x9f7)},_0x50a4c1=_0x331da7['getVideoTracks']();if(_0x50a4c1[_0x5b7671(0xb04)]){var _0x5b0135=_0x50a4c1[0x0][_0x5b7671(0x3ef)]();_0x5b0135['width']&&(_0x5a0086[_0x5b7671(0x3dd)]=_0x5b0135[_0x5b7671(0x3dd)]),_0x5b0135['height']&&(_0x5a0086['height']=_0x5b0135[_0x5b7671(0x348)]),_0x5b0135[_0x5b7671(0xb4a)]&&(_0x5a0086['frameRate']=_0x5b0135[_0x5b7671(0xb4a)]);}else _0x5a0086=![];if(_0x545320<0x259){var _0x531eda=_0x5a0086[_0x5b7671(0x3dd)]*_0x5a0086[_0x5b7671(0x348)]/(0x280*0x168);if(_0x531eda>=0x2)_0x5a0086['width']=parseInt(_0x5a0086[_0x5b7671(0x3dd)]/0x2),_0x5a0086[_0x5b7671(0x348)]=parseInt(_0x5a0086['height']/0x2);else _0x531eda>=1.5&&(_0x5a0086['width']=parseInt(_0x5a0086[_0x5b7671(0x3dd)]/1.5),_0x5a0086[_0x5b7671(0x348)]=parseInt(_0x5a0086[_0x5b7671(0x348)]/1.5));}_0x5a0086[_0x5b7671(0x3dd)]=_0x25a646(_0x5a0086[_0x5b7671(0x3dd)],0x2),_0x5a0086['height']=_0x25a646(_0x5a0086['height'],0x2),_0x5a0086[_0x5b7671(0xb4a)]=Math[_0x5b7671(0x50b)](0x1,Math['round'](_0x5a0086[_0x5b7671(0xb4a)]||0x1e));try{var _0x586283=await _0x5d7961(_0x5a0086['width'],_0x5a0086[_0x5b7671(0x348)],_0x5a0086[_0x5b7671(0xb4a)]);_0x586283&&_0x586283[_0x5b7671(0xb04)]&&(_0x5a0086[_0x5b7671(0x1a3)]=_0x586283[0x0]['config'][_0x5b7671(0x1a3)],_0x5a0086[_0x5b7671(0xb53)]=_0x586283[0x0][_0x5b7671(0x9c2)][_0x5b7671(0xb53)]),log(_0x586283);}catch(_0x5c0875){errorlog(_0x5c0875);}warnlog(_0x5a0086);_0x5a0086[_0x5b7671(0x3dd)]==_0x5a0086['height']&&(_0x5a0086[_0x5b7671(0x3dd)]=0x280,_0x5a0086[_0x5b7671(0x348)]=0x280);var _0x51b589={'codec':'opus','numberOfChannels':0x2,'channels':0x2,'sampleRate':0xbb80,'bitrate':0xfa00,'tuning':{'bitrate':0xfa00}};if(_0x545320>0xbb8)_0x51b589={'codec':_0x5b7671(0x471),'numberOfChannels':0x2,'channels':0x2,'sampleRate':0xbb80,'tuning':{'bitrate':0x1f400}};else _0x545320<0x259&&(_0x51b589={'codec':_0x5b7671(0x471),'numberOfChannels':0x2,'channels':0x2,'sampleRate':0xbb80,'tuning':{'bitrate':0x7d00}});_0x435689[_0x5b7671(0x5c4)]&&(_0x51b589={'codec':'pcm','numberOfChannels':0x2,'channels':0x2,'sampleRate':0xbb80});!_0x331da7['getAudioTracks']()[_0x5b7671(0xb04)]&&(_0x51b589=![]);if(!_0x51b589&&!_0x5a0086){warnlog('no\x20video/audio\x20config');return;}warnlog(_0x5b7671(0x499)),_0x435689['chunkedRecorder']={},_0x435689['chunkedRecorder']['needKeyFrame']=!![],_0x435689[_0x5b7671(0x826)][_0x5b7671(0x27c)]=_0x5a0086||![],_0x435689[_0x5b7671(0x826)][_0x5b7671(0xa39)]=_0x51b589||![],_0x435689[_0x5b7671(0x826)]['chunkRates']=[],_0x435689[_0x5b7671(0x5c2)][_0x5b7671(0x26b)]=_0x435689[_0x5b7671(0x6a9)];function _0x458819(){var _0x45a92f=_0x5b7671;const _0x585081='room123';let _0x5402f=![];var _0x51e9d3=new WebSocket(_0x45a92f(0x742)+_0x585081+_0x45a92f(0x156));_0x51e9d3[_0x45a92f(0x37c)]=null,_0x51e9d3[_0x45a92f(0x33c)]=_0x45a92f(0x4fd),_0x51e9d3[_0x45a92f(0xada)]=()=>{var _0x15b1cf=_0x45a92f;console['log'](_0x15b1cf(0xc1f));if(_0x435689['chunkedAudioEnabled']&&_0x435689[_0x15b1cf(0x641)]){let _0x2e9f45={'timestamp':Date[_0x15b1cf(0x1ab)](),'type':_0x15b1cf(0x3ae),'realTimeVideo':_0x435689[_0x15b1cf(0x5c2)][_0x15b1cf(0x835)][_0x15b1cf(0x433)]||0x0,'realTimeAudio':_0x435689[_0x15b1cf(0x5c2)][_0x15b1cf(0xa38)][_0x15b1cf(0x433)]||0x0,'size':0x5af3107a3fff,'configVideo':_0x435689[_0x15b1cf(0x826)][_0x15b1cf(0x27c)],'configAudio':_0x435689[_0x15b1cf(0x826)][_0x15b1cf(0xa39)],'recordType':_0x435689['chunked'],'filename':_0x58b188+_0x15b1cf(0x984),'id':_0x58b188};log(_0x2e9f45),_0x51e9d3['sendHeader'](_0x2e9f45),_0x5402f=!![];}else{if(_0x435689['chunkedAudioEnabled']){let _0x298ad2={'timestamp':Date['now'](),'type':'chunkedtransfer','realTimeAudio':_0x435689[_0x15b1cf(0x5c2)][_0x15b1cf(0xa38)][_0x15b1cf(0x433)]||0x0,'size':0x5af3107a3fff,'configAudio':_0x435689[_0x15b1cf(0x826)][_0x15b1cf(0xa39)],'recordType':_0x435689['chunked'],'filename':_0x58b188+_0x15b1cf(0x984),'id':_0x58b188};log(_0x298ad2),_0x51e9d3[_0x15b1cf(0xc30)](_0x298ad2),_0x5402f=!![];}else{if(_0x435689[_0x15b1cf(0x641)]){let _0x4d86b8={'timestamp':Date[_0x15b1cf(0x1ab)](),'type':_0x15b1cf(0x3ae),'realTimeVideo':_0x435689['stats'][_0x15b1cf(0x835)][_0x15b1cf(0x433)]||0x0,'size':0x5af3107a3fff,'configVideo':_0x435689[_0x15b1cf(0x826)][_0x15b1cf(0x27c)],'recordType':_0x435689[_0x15b1cf(0x6a9)],'filename':_0x58b188+_0x15b1cf(0x984),'id':_0x58b188};log(_0x4d86b8),_0x51e9d3[_0x15b1cf(0xc30)](_0x4d86b8),_0x5402f=!![];}}}console[_0x15b1cf(0x2c6)](_0x15b1cf(0x654)),_0x435689['chunkedRecorder']&&_0x435689[_0x15b1cf(0x826)]['sendChunks']&&_0x435689[_0x15b1cf(0x826)]['sendChunks'](),_0x5402f&&_0x14de0f();},_0x51e9d3[_0x45a92f(0x933)]=function(_0xc3e37a){var _0x4478ca=_0x45a92f;if(!_0x5402f)return;if(Array[_0x4478ca(0x45b)](_0xc3e37a))_0x51e9d3[_0x4478ca(0xc30)](_0xc3e37a);else{if(typeof _0xc3e37a===_0x4478ca(0x31f))_0x51e9d3[_0x4478ca(0x65c)](_0xc3e37a);else return _0x4478ca(0x283);}},_0x51e9d3[_0x45a92f(0xc30)]=function(_0x398e1b){var _0x40f87b=_0x45a92f;try{const _0x20fec2=JSON[_0x40f87b(0xc18)](_0x398e1b),_0x4dfd43=new TextEncoder()[_0x40f87b(0xc25)](_0x20fec2),_0x20ddad=new Uint8Array([0x0]),_0xd5b89=new Uint8Array(_0x20ddad[_0x40f87b(0xb04)]+_0x4dfd43[_0x40f87b(0xb04)]);_0xd5b89['set'](_0x20ddad,0x0),_0xd5b89[_0x40f87b(0x50e)](_0x4dfd43,_0x20ddad[_0x40f87b(0xb04)]),this['send'](_0xd5b89);}catch(_0x3049cc){errorlog(_0x3049cc);}},_0x51e9d3[_0x45a92f(0x65c)]=function(_0x31d21e){var _0x2572bc=_0x45a92f;try{const _0x44f0e6=new Uint8Array([0x1]),_0x224bb1=new Uint8Array(_0x44f0e6[_0x2572bc(0xb04)]+_0x31d21e[_0x2572bc(0x893)]);_0x224bb1['set'](_0x44f0e6,0x0),_0x224bb1[_0x2572bc(0x50e)](new Uint8Array(_0x31d21e),_0x44f0e6['length']),this[_0x2572bc(0xac5)](_0x224bb1);}catch(_0xf16803){errorlog(_0xf16803);}},_0x51e9d3['onmessage']=function(_0x1f8271){var _0x46a77b=_0x45a92f;const _0x9ad188=new Uint8Array(_0x1f8271[_0x46a77b(0x36f)]),_0x2ed2f3=_0x9ad188[0x0];if(_0x2ed2f3===0x3){const _0x5650da=new DataView(_0x9ad188['buffer'])[_0x46a77b(0x44e)](0x1,!![]),_0x38bfee=new DataView(_0x9ad188['buffer'])[_0x46a77b(0x44e)](0x5,!![]),_0x4691e1=_0x9ad188[0x9]===0x1;console[_0x46a77b(0x2c6)](_0x46a77b(0xa89)+_0x5650da),console[_0x46a77b(0x2c6)]('New\x20viewers:\x20'+_0x38bfee),_0x4691e1&&(console[_0x46a77b(0x2c6)](_0x46a77b(0x13b)),_0x435689[_0x46a77b(0x826)][_0x46a77b(0x854)]=!![]);}},_0x51e9d3[_0x45a92f(0x980)]=function(){var _0x4ea446=_0x45a92f;const _0x4cd955=new Uint8Array([0x2]);this[_0x4ea446(0xac5)](_0x4cd955);};function _0x14de0f(){var _0x2234cf=_0x45a92f;_0x51e9d3['readyState']===0x1&&(_0x51e9d3[_0x2234cf(0x980)](),clearTimeout(_0x51e9d3[_0x2234cf(0x37c)]),_0x51e9d3[_0x2234cf(0x37c)]=setTimeout(_0x14de0f,0x1388));}return _0x51e9d3[_0x45a92f(0x4ab)]=()=>{var _0x3643ad=_0x45a92f;console[_0x3643ad(0x2c6)](_0x3643ad(0x840)),_0x51e9d3[_0x3643ad(0x37c)]&&clearTimeout(_0x51e9d3[_0x3643ad(0x37c)]),_0x435689['chunkedRecorder'][_0x3643ad(0xbc8)]=![];},_0x51e9d3['onerror']=_0x85f271=>{var _0x3332d4=_0x45a92f;console['error'](_0x3332d4(0x48e),_0x85f271);},_0x51e9d3;}_0x435689[_0x5b7671(0x826)][_0x5b7671(0xbc8)]=![];function _0x269fae(){var _0xe3d815=_0x5b7671;let _0x2d4f8b=-0x1;for(let _0x257fc4=_0x435689[_0xe3d815(0xb35)][_0xe3d815(0xb04)]-0x1;_0x257fc4>=0x0;_0x257fc4--){const _0x66f13f=_0x435689['chunksQueue'][_0x257fc4];if(Array['isArray'](_0x66f13f)&&_0x66f13f[0x1]==='key'){_0x2d4f8b=_0x257fc4;break;}}if(_0x2d4f8b>0x0){const _0xd7f0f5=_0x2d4f8b;_0x435689[_0xe3d815(0xb35)]=_0x435689[_0xe3d815(0xb35)][_0xe3d815(0x3e2)](_0x2d4f8b),console['log'](_0xe3d815(0x1fb)+_0xd7f0f5+_0xe3d815(0x8b6));}else _0x2d4f8b===-0x1&&console[_0xe3d815(0x2c6)]('No\x20keyframe\x20found\x20in\x20queue,\x20keeping\x20all\x20chunks');}_0x435689[_0x5b7671(0x826)][_0x5b7671(0x493)]=async function(_0x421c21='null'){var _0x19e07c=_0x5b7671;if(_0x572387)return;_0x572387=!![];const _0x2f273d=0x1f4;if(_0x435689[_0x19e07c(0xb35)]['length']>_0x2f273d){const _0x465e71=_0x435689[_0x19e07c(0xb35)][_0x19e07c(0xb04)];let _0xfc6980=_0x435689['chunksQueue']['slice'](-_0x2f273d),_0x47203e=_0xfc6980[_0x19e07c(0x3de)](_0x5933a3=>Array[_0x19e07c(0x45b)](_0x5933a3)&&typeof _0x5933a3[0x1]===_0x19e07c(0x332));if(_0x47203e===-0x1)console['warn']('Chunked\x20queue\x20overflow,\x20dropping\x20all\x20entries\x20(no\x20metadata\x20for\x20alignment)'),_0xfc6980=[];else _0x47203e>0x0&&(_0xfc6980=_0xfc6980[_0x19e07c(0x3e2)](_0x47203e));const _0x275aa8=_0x465e71-_0xfc6980['length'];console[_0x19e07c(0x2cd)](_0x19e07c(0xbdb)+_0x275aa8+'\x20oldest\x20chunks'),_0x435689[_0x19e07c(0xb35)]=_0xfc6980;}if(_0x435689[_0x19e07c(0x453)]){!_0x435689[_0x19e07c(0x826)][_0x19e07c(0xbc8)]&&(_0x435689[_0x19e07c(0x826)]['wss']=_0x458819());if(_0x435689[_0x19e07c(0x826)][_0x19e07c(0xbc8)]){if(_0x435689['chunkedRecorder'][_0x19e07c(0xbc8)][_0x19e07c(0x565)]===0x1)while(_0x435689[_0x19e07c(0xb35)][_0x19e07c(0xb04)]){try{_0x435689[_0x19e07c(0x826)][_0x19e07c(0xbc8)][_0x19e07c(0x933)](_0x435689[_0x19e07c(0xb35)][_0x19e07c(0x57c)]());}catch(_0x32253c){break;}}else _0x435689[_0x19e07c(0xb35)][_0x19e07c(0xb04)]>0x3e8?(console[_0x19e07c(0x2c6)]('Chunkcast\x20queue\x20too\x20large,\x20clearing\x20to\x20last\x20keyframe'),_0x269fae()):console[_0x19e07c(0x2c6)]('Chunkcast\x20WebSocket\x20not\x20ready,\x20queuing\x20chunks');_0x572387=null;return;}}var _0x3caf2d=_0x421c21;const _0x3163b2=_0x435689['chunkedRecorder'][_0x19e07c(0x247)]=_0x435689[_0x19e07c(0x826)]['viewerHealth']||{};for(const _0x5c1a28 in _0x3163b2){(!_0x435689[_0x19e07c(0x1d2)][_0x5c1a28]||!_0x435689[_0x19e07c(0x76f)][_0x5c1a28])&&delete _0x3163b2[_0x5c1a28];}const _0x36ae00={0x0:0x0,0x1:0x0,0x2:0x0};let _0x52db22=0x0;function _0xf3cb73(_0x22717f){var _0x266179=_0x19e07c;if(!_0x22717f)return 0x2;if(_0x22717f[_0x266179(0xa34)]!==![]&&_0x22717f[_0x266179(0xa34)]!==undefined&&_0x22717f[_0x266179(0xa34)]!==null)return 0x0;if(_0x22717f[_0x266179(0x2ed)]!==null&&_0x22717f['sceneDisplay']!==undefined&&_0x22717f[_0x266179(0x2ed)]!==![])return 0x0;if(_0x22717f[_0x266179(0x2ba)]===!![]||_0x22717f[_0x266179(0x15d)]===!![])return 0x2;return 0x1;}function _0x452101(_0x1533d2){switch(_0x1533d2){case 0x0:return 0x180000;case 0x1:return 0x100000;default:return 0xc0000;}}function _0x2733a2(_0x1e5363,_0x362737){return!_0x3163b2[_0x1e5363]&&(_0x3163b2[_0x1e5363]={'priority':_0x362737,'skipped':0x0,'sent':0x0,'buffered':0x0,'stalled':![],'consecutiveHigh':0x0}),_0x3163b2[_0x1e5363]['priority']=_0x362737,_0x3163b2[_0x1e5363];}function _0xbc532d(_0x2b8074,_0x3fdd38,_0x475dcc={}){var _0x10da44=_0x19e07c,_0x114386=_0x435689['chunkedTransferChannels'][_0x2b8074],_0x1003e3=_0x435689[_0x10da44(0x76f)][_0x2b8074];if(!_0x114386||!_0x1003e3||_0x114386[_0x10da44(0x565)]!=='open')return![];var _0x506146=_0x475dcc[_0x10da44(0x8c3)]===!![];!_0x1003e3[_0x10da44(0x5c2)]&&(_0x1003e3[_0x10da44(0x5c2)]={});var _0x536506=_0xf3cb73(_0x1003e3),_0x2e0095=_0x2733a2(_0x2b8074,_0x536506);!_0x114386[_0x10da44(0x8a9)]&&(_0x114386['keyframeSent']=![]);!_0x114386['audioHeaderSent']&&(_0x114386[_0x10da44(0x211)]=![]);!_0x114386[_0x10da44(0x1dc)]&&(_0x114386[_0x10da44(0x1dc)]=![]);if(_0x3caf2d===_0x10da44(0x770)&&!_0x114386[_0x10da44(0x8a9)])return warnlog('Waiting\x20for\x20keyframe\x20/\x20header\x20before\x20sending\x20delta\x20/\x20raw\x20video\x20data'),_0x435689['chunkedRecorder']&&(_0x435689['chunkedRecorder'][_0x10da44(0x854)]=!![]),![];if(!_0x506146&&(_0x3caf2d==='key'||_0x3caf2d===_0x10da44(0x770)||_0x3caf2d===_0x10da44(0x57d))&&!_0x114386['keyframeSent'])return warnlog('Waiting\x20for\x20keyframe\x20/\x20header\x20before\x20sending\x20delta\x20/\x20raw\x20video\x20data'),_0x435689[_0x10da44(0x826)]&&(_0x435689['chunkedRecorder'][_0x10da44(0x854)]=!![]),![];if(!_0x506146&&(_0x3caf2d===_0x10da44(0x2de)||_0x3caf2d===_0x10da44(0x5c4))&&!_0x114386[_0x10da44(0x211)])return warnlog(_0x10da44(0x40c)),![];if(!_0x114386[_0x10da44(0x1dc)]){if(_0x435689[_0x10da44(0xaf6)])try{var _0x24da7a={..._0x435689['chunkedDetails']};_0x24da7a[_0x10da44(0x95f)]=Date[_0x10da44(0x1ab)](),_0x114386[_0x10da44(0xac5)](JSON['stringify'](_0x24da7a)),_0x114386['detailsSent']=!![];}catch(_0x20a971){return _0x2e0095[_0x10da44(0x99e)]=(_0x2e0095[_0x10da44(0x99e)]||0x0)+0x1,_0x2e0095[_0x10da44(0x869)]=!![],_0x435689['chunkedRecorder'][_0x10da44(0x854)]=!![],![];}else _0x114386['detailsSent']=!![];}var _0x4dbf6d=_0x114386[_0x10da44(0x953)]||0x0;_0x4dbf6d>_0x52db22&&(_0x52db22=_0x4dbf6d);_0x4dbf6d>_0x36ae00[_0x536506]&&(_0x36ae00[_0x536506]=_0x4dbf6d);_0x1003e3[_0x10da44(0x5c2)][_0x10da44(0x953)]=_0x4dbf6d;var _0x28f545=_0x3caf2d==='video'||_0x3caf2d==='delta',_0x497c1c=_0x452101(_0x536506);_0x536506===0x0&&(_0x4dbf6d>_0x497c1c*1.5?_0x2e0095['highPriorityPressure']=(_0x2e0095[_0x10da44(0x6ab)]||0x0)+0x1:_0x2e0095[_0x10da44(0x6ab)]=0x0);if(_0x28f545&&_0x536506>0x0&&_0x4dbf6d>_0x497c1c)return _0x2e0095[_0x10da44(0xc23)]=(_0x2e0095[_0x10da44(0xc23)]||0x0)+0x1,_0x2e0095['consecutiveHigh']=(_0x2e0095[_0x10da44(0x25e)]||0x0)+0x1,_0x2e0095[_0x10da44(0x697)]=_0x4dbf6d,_0x2e0095['consecutiveHigh']>=0x4&&(_0x2e0095['stalled']=!![],_0x435689[_0x10da44(0x826)][_0x10da44(0x854)]=!![]),![];try{_0x114386[_0x10da44(0xac5)](_0x3fdd38);}catch(_0x254617){return _0x2e0095[_0x10da44(0x99e)]=(_0x2e0095['sendErrors']||0x0)+0x1,_0x2e0095[_0x10da44(0x869)]=!![],_0x435689[_0x10da44(0x826)][_0x10da44(0x854)]=!![],![];}if(_0x506146){if(_0x3caf2d==_0x10da44(0x304)||_0x3caf2d==_0x10da44(0x57d))_0x114386['keyframeSent']=!![];else(_0x3caf2d=='audio'||_0x3caf2d==_0x10da44(0x5c4))&&(_0x114386['audioHeaderSent']=!![]);}return _0x4dbf6d=_0x114386[_0x10da44(0x953)]||0x0,_0x4dbf6d>_0x52db22&&(_0x52db22=_0x4dbf6d),_0x4dbf6d>_0x36ae00[_0x536506]&&(_0x36ae00[_0x536506]=_0x4dbf6d),_0x1003e3[_0x10da44(0x5c2)]['bufferedAmount']=_0x4dbf6d,_0x2e0095[_0x10da44(0x697)]=_0x4dbf6d,_0x2e0095['sent']=(_0x2e0095['sent']||0x0)+0x1,_0x2e0095[_0x10da44(0xa60)]=Date[_0x10da44(0x1ab)](),_0x2e0095[_0x10da44(0x25e)]=Math['max'](0x0,(_0x2e0095['consecutiveHigh']||0x0)-0x1),_0x2e0095['stalled']=![],!![];}while(_0x435689[_0x19e07c(0xb35)][_0x19e07c(0xb04)]){if(!Object[_0x19e07c(0x161)](_0x435689[_0x19e07c(0x1d2)])['length']){_0x269fae(),_0x572387=null,_0x435689['stats'][_0x19e07c(0x72e)]=_0x435689['chunksQueue'][_0x19e07c(0xb04)],_0x435689[_0x19e07c(0x826)]['chunkRates']=[];return;}_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x72e)]=_0x435689['chunksQueue']['length'],_0x52db22=0x0,_0x36ae00[0x0]=0x0,_0x36ae00[0x1]=0x0,_0x36ae00[0x2]=0x0;var _0x5b3345=_0x435689[_0x19e07c(0xb35)][_0x19e07c(0x57c)]();if(Array[_0x19e07c(0x45b)](_0x5b3345)){_0x3caf2d=_0x5b3345[0x1];const _0x337974=_0x435689[_0x19e07c(0xb35)][_0x19e07c(0xb04)],_0x2f83e4=_0x5b3345,_0xe0a52c=_0x5b3345[_0x19e07c(0x3e2)]();_0xe0a52c[_0x19e07c(0x35c)](_0x337974);const _0x325a30=JSON[_0x19e07c(0xc18)](_0xe0a52c),_0x288ae3=_0x3caf2d==_0x19e07c(0x304)||_0x3caf2d==_0x19e07c(0x57d)||_0x3caf2d==_0x19e07c(0x2de)||_0x3caf2d==_0x19e07c(0x5c4);let _0x1f30f8=![],_0x3f6602=![],_0xf66152=![];for(var _0x5ee3d3 in _0x435689['chunkedTransferChannels']){var _0x307492=_0x435689[_0x19e07c(0x1d2)][_0x5ee3d3];if(!_0x307492){_0xf66152=!![];continue;}var _0x3b6755=_0x435689[_0x19e07c(0x76f)][_0x5ee3d3];if(!_0x3b6755)continue;if((_0x3caf2d==_0x19e07c(0x304)||_0x3caf2d=='delta'||_0x3caf2d==_0x19e07c(0x57d))&&!_0x3b6755[_0x19e07c(0x1b1)])continue;if((_0x3caf2d=='audio'||_0x3caf2d==_0x19e07c(0x5c4))&&(!_0x3b6755[_0x19e07c(0x1da)]||_0x3b6755[_0x19e07c(0x699)]==0x2))continue;_0x1f30f8=!![],_0xbc532d(_0x5ee3d3,_0x325a30,{'metadata':!![]})&&(_0x3f6602=!![]);}if(!_0x3f6602&&(_0x1f30f8||_0xf66152)&&_0x288ae3){_0x435689[_0x19e07c(0x826)]&&(_0x435689[_0x19e07c(0x826)][_0x19e07c(0x854)]=!![]);_0x435689[_0x19e07c(0xb35)][_0x19e07c(0x4a0)](_0x2f83e4),_0x435689['stats'][_0x19e07c(0x72e)]=_0x435689[_0x19e07c(0xb35)][_0x19e07c(0xb04)],_0x572387=null;return;}}else{if(_0x5b3345['byteLength']>0x40000){const _0x80df35=_0x5b3345,_0x127157=_0x80df35[_0x19e07c(0x3e2)](0x0,0x40000);let _0x3f14c7=![],_0x147d38=![],_0x165ea3=![];for(var _0x5ee3d3 in _0x435689[_0x19e07c(0x1d2)]){var _0x307492=_0x435689['chunkedTransferChannels'][_0x5ee3d3];if(!_0x307492){_0x165ea3=!![];continue;}var _0x3b6755=_0x435689['pcs'][_0x5ee3d3];if(!_0x3b6755)continue;if((_0x3caf2d==_0x19e07c(0x304)||_0x3caf2d==_0x19e07c(0x770)||_0x3caf2d==_0x19e07c(0x57d))&&!_0x3b6755[_0x19e07c(0x1b1)])continue;if((_0x3caf2d==_0x19e07c(0x2de)||_0x3caf2d==_0x19e07c(0x5c4))&&(!_0x3b6755[_0x19e07c(0x1da)]||_0x3b6755[_0x19e07c(0x699)]==0x2))continue;_0x3f14c7=!![],_0xbc532d(_0x5ee3d3,_0x127157)&&(_0x147d38=!![]);}const _0x303740=_0x3caf2d=='key'||_0x3caf2d==_0x19e07c(0x57d)||_0x3caf2d=='audio'||_0x3caf2d==_0x19e07c(0x5c4);if(!_0x147d38&&(_0x3f14c7||_0x165ea3)&&_0x303740){_0x435689[_0x19e07c(0x826)]&&(_0x435689[_0x19e07c(0x826)][_0x19e07c(0x854)]=!![]);_0x435689[_0x19e07c(0xb35)][_0x19e07c(0x4a0)](_0x80df35),_0x435689['stats'][_0x19e07c(0x72e)]=_0x435689[_0x19e07c(0xb35)]['length'],_0x572387=null;return;}_0x147d38&&(_0x435689['chunksQueue'][_0x19e07c(0x4a0)](_0x80df35[_0x19e07c(0x3e2)](0x40000)),_0x435689[_0x19e07c(0x826)]['chunkRates'][_0x19e07c(0x35c)]({'bufferSize':_0x52db22,'byteLength':0x40000,'timestamp':Date[_0x19e07c(0x1ab)]()}));}else{const _0x41f523=_0x5b3345;let _0x52538a=![],_0x841d87=![],_0x17270d=![];for(var _0x5ee3d3 in _0x435689[_0x19e07c(0x1d2)]){var _0x307492=_0x435689[_0x19e07c(0x1d2)][_0x5ee3d3];if(!_0x307492){_0x17270d=!![];continue;}var _0x3b6755=_0x435689[_0x19e07c(0x76f)][_0x5ee3d3];if(!_0x3b6755)continue;if((_0x3caf2d=='key'||_0x3caf2d=='delta'||_0x3caf2d=='video')&&!_0x3b6755[_0x19e07c(0x1b1)])continue;if((_0x3caf2d==_0x19e07c(0x2de)||_0x3caf2d=='pcm')&&(!_0x3b6755[_0x19e07c(0x1da)]||_0x3b6755[_0x19e07c(0x699)]==0x2))continue;_0x52538a=!![],_0xbc532d(_0x5ee3d3,_0x5b3345)&&(_0x841d87=!![]);}const _0x5e4382=_0x3caf2d=='key'||_0x3caf2d==_0x19e07c(0x57d)||_0x3caf2d==_0x19e07c(0x2de)||_0x3caf2d=='pcm';if(!_0x841d87&&(_0x52538a||_0x17270d)&&_0x5e4382){_0x435689[_0x19e07c(0x826)]&&(_0x435689[_0x19e07c(0x826)][_0x19e07c(0x854)]=!![]);_0x435689[_0x19e07c(0xb35)][_0x19e07c(0x4a0)](_0x41f523),_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x72e)]=_0x435689[_0x19e07c(0xb35)][_0x19e07c(0xb04)],_0x572387=null;return;}_0x841d87&&_0x435689['chunkedRecorder'][_0x19e07c(0xa63)][_0x19e07c(0x35c)]({'bufferSize':_0x52db22,'byteLength':_0x5b3345['byteLength'],'timestamp':Date[_0x19e07c(0x1ab)]()});}}_0x435689[_0x19e07c(0x826)][_0x19e07c(0xa63)]=_0x435689[_0x19e07c(0x826)][_0x19e07c(0xa63)][_0x19e07c(0x3e2)](-0x3e8);let _0x2c0c95=0x0,_0x46568c=0x0,_0xc23942=0x0;for(let _0x4e1589=_0x435689[_0x19e07c(0x826)]['chunkRates'][_0x19e07c(0xb04)]-0x1;_0x4e1589>0x0;_0x4e1589--){if(_0xc23942>_0x435689[_0x19e07c(0x422)]*0x2){_0x435689[_0x19e07c(0x826)][_0x19e07c(0xa63)][_0x19e07c(0x20c)](_0x4e1589-0x1,0x1);continue;}const _0xcc6aea=_0x435689[_0x19e07c(0x826)][_0x19e07c(0xa63)][_0x4e1589-0x1],_0x45e1c8=_0x435689[_0x19e07c(0x826)]['chunkRates'][_0x4e1589];_0x46568c+=_0x45e1c8[_0x19e07c(0x658)]-_0xcc6aea[_0x19e07c(0x658)],_0x2c0c95+=_0x45e1c8['byteLength'],_0xc23942+=_0x45e1c8[_0x19e07c(0x95f)]-_0xcc6aea[_0x19e07c(0x95f)];}let _0xb78dbb=_0x2c0c95-_0x46568c,_0x2cffc3=0x0,_0x284def=0x0;if(_0xc23942>0x0){const _0x37f4fc=_0xc23942/0x3e8;_0x37f4fc>0x0&&(_0x2cffc3=_0x2c0c95/_0x37f4fc*0x8/0x3e8,_0x284def=_0xb78dbb/_0x37f4fc*0x8/0x3e8);}_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x357)]=parseInt(0x8*_0x52db22/Math['max'](_0x284def,0x1))+_0x19e07c(0x42b)+_0x435689[_0x19e07c(0x422)];let _0x4f2647=_0x284def>0x0?0x8*_0x52db22/_0x284def/_0x435689[_0x19e07c(0x422)]:0x0;_0x435689[_0x19e07c(0x5c2)]['bufferFullness']=_0x4f2647,_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0xa49)]=parseInt(_0x2cffc3)+_0x19e07c(0x97e)+parseInt(_0x284def);if(!_0x435689[_0x19e07c(0x826)]['adaptation']){const _0xa3318d=Math[_0x19e07c(0x50b)](0xc8,_0x435689[_0x19e07c(0x6a9)]*0.2),_0x29f537=Number[_0x19e07c(0x7a3)](parseFloat(_0x435689[_0x19e07c(0x57a)]))?parseFloat(_0x435689[_0x19e07c(0x57a)]):_0xa3318d,_0x58e392=Number['isFinite'](parseFloat(_0x435689[_0x19e07c(0x915)]))?parseFloat(_0x435689[_0x19e07c(0x915)]):_0x435689[_0x19e07c(0x6a9)];_0x435689[_0x19e07c(0x826)][_0x19e07c(0x7d5)]={'target':_0x435689[_0x19e07c(0x6a9)],'floor':_0x29f537,'ceiling':_0x58e392,'lastChange':0x0,'mode':(_0x435689['chunkadapt']||_0x19e07c(0xaa6))['toLowerCase'](),'frameDropBudget':0x0,'maxFrameDrop':Number[_0x19e07c(0x7a3)](parseInt(_0x435689[_0x19e07c(0x2d8)]))?Math[_0x19e07c(0x50b)](0x0,parseInt(_0x435689['chunkadaptmaxdrop'])):0x6,'threshold':Number[_0x19e07c(0x7a3)](parseFloat(_0x435689['chunkadaptthreshold']))?parseFloat(_0x435689[_0x19e07c(0x4c2)]):0x17c,'interval':Number[_0x19e07c(0x7a3)](parseInt(_0x435689['chunkadaptinterval']))?Math['max'](0xc8,parseInt(_0x435689['chunkadaptinterval'])):0x320};}const _0x358a94=_0x435689[_0x19e07c(0x826)]['adaptation'];_0x358a94[_0x19e07c(0xbda)]=(_0x435689[_0x19e07c(0xc11)]||_0x358a94['mode']||_0x19e07c(0xaa6))[_0x19e07c(0xa6f)]();Number['isFinite'](parseFloat(_0x435689[_0x19e07c(0x57a)]))&&(_0x358a94[_0x19e07c(0x8bb)]=parseFloat(_0x435689[_0x19e07c(0x57a)]));Number['isFinite'](parseFloat(_0x435689['chunkadaptceil']))?_0x358a94['ceiling']=parseFloat(_0x435689['chunkadaptceil']):_0x358a94[_0x19e07c(0x798)]=_0x435689[_0x19e07c(0x6a9)];Number[_0x19e07c(0x7a3)](parseInt(_0x435689['chunkadaptmaxdrop']))&&(_0x358a94[_0x19e07c(0xa0f)]=Math[_0x19e07c(0x50b)](0x0,parseInt(_0x435689[_0x19e07c(0x2d8)])));Number['isFinite'](parseFloat(_0x435689['chunkadaptthreshold']))&&(_0x358a94[_0x19e07c(0x8e9)]=parseFloat(_0x435689['chunkadaptthreshold']));Number[_0x19e07c(0x7a3)](parseInt(_0x435689['chunkadaptinterval']))&&(_0x358a94[_0x19e07c(0x410)]=Math[_0x19e07c(0x50b)](0xc8,parseInt(_0x435689['chunkadaptinterval'])));_0x358a94[_0x19e07c(0x8bb)]=Math[_0x19e07c(0x50b)](0x0,Math[_0x19e07c(0x452)](_0x358a94[_0x19e07c(0x8bb)],_0x358a94[_0x19e07c(0x798)]));const _0x590768=Date[_0x19e07c(0x1ab)]();let _0x1fd4d7=_0x358a94[_0x19e07c(0x5df)],_0x3abe36=![],_0x1fdeea=![],_0x23c1e8=![],_0x463dab=![],_0x5ad92f=0x0;for(const _0x4e7fe6 in _0x3163b2){const _0x43609c=_0x3163b2[_0x4e7fe6];_0x43609c['priority']===0x0&&(_0x43609c[_0x19e07c(0x869)]||(_0x43609c[_0x19e07c(0x6ab)]||0x0)>0x3)&&(_0x3abe36=!![]),_0x43609c[_0x19e07c(0x3bb)]>0x0&&(_0x5ad92f+=_0x43609c['skipped']||0x0);}_0x23c1e8=_0x36ae00[0x0]>_0x452101(0x0),_0x1fdeea=_0x36ae00[0x1]>_0x452101(0x1),_0x463dab=_0x36ae00[0x2]>_0x452101(0x2);const _0xec2983=_0x358a94[_0x19e07c(0xbda)]||_0x19e07c(0xaa6),_0x13ef75=_0xec2983===_0x19e07c(0xaa6)||_0xec2983===_0x19e07c(0x7d8),_0x3ee2f1=_0xec2983==='framerate'||_0xec2983===_0x19e07c(0x7d8);_0x3ee2f1&&(_0x358a94['frameDropBudget']=Math[_0x19e07c(0x50b)](0x0,_0x358a94[_0x19e07c(0x1eb)]-0.2));if(_0x3abe36||_0x23c1e8||_0x4f2647>1.25){_0x13ef75&&(_0x1fd4d7=Math['max'](_0x358a94[_0x19e07c(0x8bb)],_0x358a94['target']*0.82));if(_0x3ee2f1){const _0xe39c01=_0xec2983===_0x19e07c(0x7d8)?0x1:0x2;_0x358a94['frameDropBudget']=Math[_0x19e07c(0x452)](_0x358a94['maxFrameDrop'],_0x358a94[_0x19e07c(0x1eb)]+_0xe39c01);}}else{if(_0x1fdeea||_0x4f2647>0x1)_0x13ef75&&(_0x1fd4d7=Math[_0x19e07c(0x50b)](_0x358a94[_0x19e07c(0x8bb)],_0x358a94[_0x19e07c(0x5df)]*0.9)),_0x3ee2f1&&(_0x358a94[_0x19e07c(0x1eb)]=Math[_0x19e07c(0x452)](_0x358a94[_0x19e07c(0xa0f)],_0x358a94[_0x19e07c(0x1eb)]+0x1));else!_0x3abe36&&!_0x23c1e8&&_0x4f2647<0.35&&_0x284def>0x0&&(_0x13ef75&&(_0x1fd4d7=Math[_0x19e07c(0x452)](_0x358a94['ceiling'],_0x358a94[_0x19e07c(0x5df)]*1.08)),_0x3ee2f1&&(_0x358a94[_0x19e07c(0x1eb)]=Math[_0x19e07c(0x50b)](0x0,_0x358a94['frameDropBudget']-0x1)));}_0x13ef75&&Math['abs'](_0x1fd4d7-_0x358a94[_0x19e07c(0x5df)])>_0x358a94['target']*0.05&&_0x590768-_0x358a94['lastChange']>_0x358a94['interval']&&(_0x358a94[_0x19e07c(0x5df)]=Math[_0x19e07c(0x50b)](_0x358a94[_0x19e07c(0x8bb)],Math[_0x19e07c(0x452)](_0x1fd4d7,_0x358a94[_0x19e07c(0x798)])),_0x358a94[_0x19e07c(0x78e)]=_0x590768);_0x3abe36&&(_0x435689[_0x19e07c(0x826)][_0x19e07c(0x854)]=!![]);_0x435689['stats'][_0x19e07c(0x26b)]=Math[_0x19e07c(0x50b)](_0x358a94[_0x19e07c(0x8bb)],Math['min'](_0x358a94[_0x19e07c(0x5df)],_0x358a94[_0x19e07c(0x798)])),_0x358a94[_0x19e07c(0x1eb)]=Math[_0x19e07c(0x50b)](0x0,Math[_0x19e07c(0x452)](_0x358a94['maxFrameDrop'],_0x358a94[_0x19e07c(0x1eb)])),_0x435689[_0x19e07c(0x5c2)]['chunkedFrameDropBudget']=Math[_0x19e07c(0x5de)](_0x358a94[_0x19e07c(0x1eb)]),_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x604)]=_0x358a94[_0x19e07c(0xbda)],_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x26b)]=Math[_0x19e07c(0x5de)](_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x26b)]),_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0xadb)]=_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x26b)];const _0x22fc6a=_0x284def>0x0?_0x284def/0x8:0x0,_0x4c87fd={'high':_0x22fc6a?Math['round'](_0x36ae00[0x0]/_0x22fc6a):0x0,'medium':_0x22fc6a?Math[_0x19e07c(0x5de)](_0x36ae00[0x1]/_0x22fc6a):0x0,'low':_0x22fc6a?Math[_0x19e07c(0x5de)](_0x36ae00[0x2]/_0x22fc6a):0x0};_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x712)]=_0x4c87fd,_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x68a)]={};for(const _0x4a70b5 in _0x3163b2){const _0x373e4e=_0x3163b2[_0x4a70b5];_0x435689['stats'][_0x19e07c(0x68a)][_0x4a70b5]={'priority':_0x373e4e[_0x19e07c(0x3bb)],'buffered':parseInt(_0x373e4e[_0x19e07c(0x697)]||0x0),'skipped':_0x373e4e[_0x19e07c(0xc23)]||0x0,'stalled':!!_0x373e4e[_0x19e07c(0x869)],'pressure':_0x373e4e['highPriorityPressure']||0x0};}_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x3a4)]=_0x5ad92f;try{_0x435689[_0x19e07c(0x826)]&&_0x435689['chunkedRecorder'][_0x19e07c(0x474)]&&(_0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)][_0x19e07c(0x203)]==_0x19e07c(0x94c)&&(console['log'](_0x19e07c(0xb90)),delete _0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)],_0x435689[_0x19e07c(0x641)]=null,await _0x435689[_0x19e07c(0x188)]()),_0x435689['chunkedRecorder']&&_0x435689['chunkedRecorder'][_0x19e07c(0x474)]&&_0x435689[_0x19e07c(0x826)]['videoEncoder'][_0x19e07c(0xc1b)]&&_0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)][_0x19e07c(0x9c2)]&&(_0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)][_0x19e07c(0x9c2)][_0x19e07c(0xaa6)]&&_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x26b)]&&(_0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)][_0x19e07c(0x9c2)][_0x19e07c(0xaa6)]=_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x26b)]*0x3e8),_0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)][_0x19e07c(0x9c2)][_0x19e07c(0x5ee)]&&_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x26b)]&&(_0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)][_0x19e07c(0x9c2)][_0x19e07c(0x5ee)][_0x19e07c(0xaa6)]=_0x435689['stats'][_0x19e07c(0x26b)]*0x3e8),_0x435689[_0x19e07c(0x826)]['videoEncoder'][_0x19e07c(0xc1b)](_0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)][_0x19e07c(0x9c2)])),_0x435689['stats'][_0x19e07c(0x26b)]=parseInt(_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x26b)])),_0x435689['chunkedRecorder']&&_0x435689[_0x19e07c(0x826)][_0x19e07c(0x527)]&&(_0x435689[_0x19e07c(0x826)]['audioEncoder'][_0x19e07c(0x203)]==_0x19e07c(0x94c)&&(console['log']('Video\x20encdoder\x20closed'),delete _0x435689[_0x19e07c(0x826)][_0x19e07c(0x527)],_0x435689[_0x19e07c(0x3c9)]=null,await _0x435689[_0x19e07c(0x43e)]()),_0x435689[_0x19e07c(0x826)]&&_0x435689[_0x19e07c(0x826)][_0x19e07c(0x527)]&&_0x435689[_0x19e07c(0x826)][_0x19e07c(0x527)][_0x19e07c(0xc1b)]&&_0x435689[_0x19e07c(0x826)][_0x19e07c(0x527)]['config']&&_0x435689['chunkedRecorder'][_0x19e07c(0x527)][_0x19e07c(0xc1b)](_0x435689[_0x19e07c(0x826)]['audioEncoder'][_0x19e07c(0x9c2)]));}catch(_0x25c443){errorlog(_0x25c443);if(_0x435689['chunkedTransferChannels'])for(var _0x5ee3d3 in _0x435689[_0x19e07c(0x1d2)]){_0x435689['chunkedTransferChannels'][_0x5ee3d3]['close']();_0x5ee3d3 in _0x435689['chunkedTransferChannels']&&delete _0x435689[_0x19e07c(0x1d2)][_0x5ee3d3];_0x435689[_0x19e07c(0x641)]=null,_0x435689[_0x19e07c(0x3c9)]=null;if(_0x435689[_0x19e07c(0x826)]&&_0x435689[_0x19e07c(0x826)]['videoEncoder']){try{_0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)]['close']();}catch(_0x9a3699){}delete _0x435689[_0x19e07c(0x826)][_0x19e07c(0x474)],await _0x435689[_0x19e07c(0x188)]();}if(_0x435689[_0x19e07c(0x826)]&&_0x435689[_0x19e07c(0x826)][_0x19e07c(0x527)])try{_0x435689['chunkedRecorder'][_0x19e07c(0x527)][_0x19e07c(0x757)](),delete _0x435689[_0x19e07c(0x826)][_0x19e07c(0x527)];}catch(_0x28d57d){}setTimeout(function(_0xb55273){var _0x27c6ea=_0x19e07c;_0x435689[_0x27c6ea(0xa54)](_0xb55273);},0x3e8,_0x5ee3d3);}_0x572387=null;return;}}_0x572387=null,_0x435689[_0x19e07c(0x5c2)][_0x19e07c(0x72e)]=0x0;},_0x435689[_0x5b7671(0x826)][_0x5b7671(0x27c)]&&(_0x435689[_0x5b7671(0x826)][_0x5b7671(0xa15)]=_0x435689[_0x5b7671(0x188)](_0x435689[_0x5b7671(0x826)][_0x5b7671(0x27c)])),_0x435689['chunkedRecorder'][_0x5b7671(0xa39)]&&(_0x435689['chunkedRecorder'][_0x5b7671(0xa39)][_0x5b7671(0x1a3)]==_0x5b7671(0x5c4)?_0x435689[_0x5b7671(0x634)](_0x331da7,_0x435689['chunkedRecorder'][_0x5b7671(0xa39)]):_0x435689['chunkedRecorder']['audioPromise']=_0x435689[_0x5b7671(0x43e)](_0x435689['chunkedRecorder'][_0x5b7671(0xa39)])),_0x331da7[_0x5b7671(0x77b)]=function(_0x248332){warnlog('STREAM\x20ENDED'),log(_0x248332);};}else warnlog(_0x5b7671(0xacb));_0x435689[_0x5b7671(0x826)][_0x5b7671(0xa15)]&&(await _0x435689['chunkedRecorder'][_0x5b7671(0xa15)],delete _0x435689[_0x5b7671(0x826)][_0x5b7671(0xa15)]);_0x435689[_0x5b7671(0x826)][_0x5b7671(0x51e)]&&(await _0x435689[_0x5b7671(0x826)][_0x5b7671(0x51e)],delete _0x435689[_0x5b7671(0x826)][_0x5b7671(0x51e)]);if(!_0x349405)return;var _0x58b188='chunked';if(_0x349405 in _0x435689[_0x5b7671(0x76f)]){if(!_0x435689[_0x5b7671(0x1d2)][_0x349405])_0x435689[_0x5b7671(0x1d2)][_0x349405]=_0x435689[_0x5b7671(0x76f)][_0x349405][_0x5b7671(0x644)](_0x58b188,{'ordered':!![]});else{errorlog(_0x5b7671(0x418));return;}}else{warnlog(_0x5b7671(0xad2));return;}_0x435689['chunkedTransferChannels'][_0x349405]['contentType']=_0x5b7671(0x4de),_0x435689[_0x5b7671(0x1d2)][_0x349405][_0x5b7671(0x33c)]='arraybuffer',_0x435689[_0x5b7671(0x1d2)][_0x349405][_0x5b7671(0x609)]=![],_0x435689[_0x5b7671(0x1d2)][_0x349405][_0x5b7671(0xada)]=()=>{var _0x328097=_0x5b7671;log('chunkedtransfer\x20OPEN');if(_0x435689['chunkedAudioEnabled']&&_0x435689[_0x328097(0x641)]&&_0x435689[_0x328097(0x76f)][_0x349405][_0x328097(0x1da)]&&!(_0x435689[_0x328097(0x76f)][_0x349405][_0x328097(0x699)]==0x2)&&_0x435689[_0x328097(0x76f)][_0x349405][_0x328097(0x1b1)]){let _0x3e14a2={'timestamp':Date[_0x328097(0x1ab)](),'type':_0x328097(0x3ae),'realTimeVideo':_0x435689[_0x328097(0x5c2)][_0x328097(0x835)][_0x328097(0x433)]||0x0,'realTimeAudio':_0x435689[_0x328097(0x5c2)][_0x328097(0xa38)][_0x328097(0x433)]||0x0,'size':0x5af3107a3fff,'configVideo':_0x435689[_0x328097(0x826)][_0x328097(0x27c)],'configAudio':_0x435689['chunkedRecorder']['configAudio'],'recordType':_0x435689[_0x328097(0x6a9)],'filename':_0x58b188+_0x328097(0x984),'id':_0x58b188};log(_0x3e14a2),_0x435689['chunkedTransferChannels'][_0x349405][_0x328097(0xac5)](JSON[_0x328097(0xc18)](_0x3e14a2));}else{if(_0x435689['chunkedAudioEnabled']&&_0x435689[_0x328097(0x76f)][_0x349405][_0x328097(0x1da)]&&!(_0x435689[_0x328097(0x76f)][_0x349405]['allowChunked']==0x2)){let _0x5c5c0b={'timestamp':Date['now'](),'type':_0x328097(0x3ae),'realTimeAudio':_0x435689['stats'][_0x328097(0xa38)][_0x328097(0x433)]||0x0,'size':0x5af3107a3fff,'configAudio':_0x435689[_0x328097(0x826)][_0x328097(0xa39)],'recordType':_0x435689[_0x328097(0x6a9)],'filename':_0x58b188+_0x328097(0x984),'id':_0x58b188};log(_0x5c5c0b),_0x435689[_0x328097(0x1d2)][_0x349405][_0x328097(0xac5)](JSON[_0x328097(0xc18)](_0x5c5c0b));}else{if(_0x435689[_0x328097(0x641)]&&_0x435689['pcs'][_0x349405][_0x328097(0x1b1)]){let _0x16570f={'timestamp':Date[_0x328097(0x1ab)](),'type':'chunkedtransfer','realTimeVideo':_0x435689[_0x328097(0x5c2)]['Chunked_video']['realTime']||0x0,'size':0x5af3107a3fff,'configVideo':_0x435689['chunkedRecorder'][_0x328097(0x27c)],'recordType':_0x435689[_0x328097(0x6a9)],'filename':_0x58b188+_0x328097(0x984),'id':_0x58b188};log(_0x16570f),_0x435689[_0x328097(0x1d2)][_0x349405][_0x328097(0xac5)](JSON[_0x328097(0xc18)](_0x16570f));}}}},_0x435689[_0x5b7671(0x1d2)][_0x349405][_0x5b7671(0x4ab)]=()=>{var _0x16f2b7=_0x5b7671;try{var _0x3af706=_0x435689[_0x16f2b7(0x569)][_0x16f2b7(0x97c)](_0x435689['chunkedTransferChannels'][_0x349405]);_0x3af706>-0x1&&_0x435689[_0x16f2b7(0x569)][_0x16f2b7(0x20c)](_0x3af706,0x1);}catch(_0x1ac782){errorlog(_0x1ac782);}log('Transfer\x20ended'),_0x435689['chunkedTransferChannels'][_0x349405]=null,delete _0x435689[_0x16f2b7(0x1d2)][_0x349405];var _0x1575be=!![];for(var _0x253665=0x0;_0x253665<_0x435689['hostedTransfers'][_0x16f2b7(0xb04)];_0x253665++){if(_0x16f2b7(0x273)in _0x435689[_0x16f2b7(0x569)][_0x253665]&&_0x435689[_0x16f2b7(0x569)][_0x253665][_0x16f2b7(0x273)]=='chunks'){_0x1575be=![];break;}}if(_0x1575be){warnlog(_0x16f2b7(0x6a4));try{_0x435689[_0x16f2b7(0x826)][_0x16f2b7(0xaef)]();}catch(_0x1b2406){}_0x435689[_0x16f2b7(0x826)]=![];}},_0x435689[_0x5b7671(0x1d2)][_0x349405][_0x5b7671(0x13c)]=_0x5dd887=>{var _0x50eaa1=_0x5b7671;if(_0x5dd887[_0x50eaa1(0x36f)])try{var _0xbd88d8=JSON['parse'](_0x5dd887[_0x50eaa1(0x36f)]);_0xbd88d8['kf']&&(warnlog(_0x50eaa1(0x5e7)),_0x435689[_0x50eaa1(0x826)][_0x50eaa1(0x854)]=!![]);}catch(_0x3db56a){}},_0x435689[_0x5b7671(0x569)][_0x5b7671(0x35c)](_0x435689[_0x5b7671(0x1d2)][_0x349405]);},_0x435689['recieveFile']=async function(_0x4946b7,_0xcbc849,_0x56aea5){var _0x2f7736=_0x120577;log(_0x2f7736(0x1b0));var _0x385a69=_0x56aea5;_0x385a69['binaryType']=_0x2f7736(0x4fd);var _0x480b11='',_0x18dd85=0x0,_0x2fb38b=![],_0x4ea59d=![],_0x4070ef=0x0,_0x2adce6={};_0x385a69['onopen']=_0x4208e8=>{var _0x3d7c57=_0x2f7736;log(_0x3d7c57(0x302));},_0x385a69['onmessage']=_0x1bd83a=>{var _0x4be40a=_0x2f7736;if(!_0x2fb38b)try{_0x2fb38b=JSON[_0x4be40a(0x48a)](_0x1bd83a[_0x4be40a(0x36f)]);if(_0x2fb38b[_0x4be40a(0x2dd)]==_0x4be40a(0x620)){var {readable:_0xda2779,writable:_0x3207ff}=new TransformStream({'transform':(_0x98fc41,_0x5ab543)=>_0x98fc41['arrayBuffer']()[_0x4be40a(0x7b6)](_0x340be8=>_0x5ab543['enqueue'](new Uint8Array(_0x340be8)))});_0x2adce6[_0x4be40a(0x1c6)]=_0x3207ff['getWriter'](),_0xda2779['pipeTo'](streamSaver[_0x4be40a(0x929)](_0x2fb38b['filename']));for(var _0x247aa5=0x0;_0x247aa5{var _0x2f1830=_0x2f7736;_0x4070ef<=0x0&&(_0x2adce6[_0x2f1830(0x1c6)]&&setTimeout(function(_0x3d0e2a,_0x486d8f){var _0x582151=_0x2f1830;_0x486d8f<=0x0?(_0x3d0e2a[_0x582151(0x757)](),_0x3d0e2a=null):setTimeout(function(_0x3a6e75,_0x412c7a){var _0x4718b8=_0x582151;_0x3a6e75[_0x4718b8(0x757)](),_0x3a6e75=null;},0x1388,_0x3d0e2a);},0x3e8,_0x2adce6[_0x2f1830(0x1c6)],_0x4070ef));_0x385a69=null;return;};return;};async function _0x17bcad(_0x294bee,_0x296aec=![]){var _0x4df9fc=_0x120577;try{_0x294bee[_0x4df9fc(0x213)][_0x4df9fc(0x456)](_0x294bee[_0x4df9fc(0x8f5)][_0x4df9fc(0x57c)]());}catch(_0x267abc){errorlog(_0x267abc);}if(_0x294bee[_0x4df9fc(0x135)]===null&&!_0x296aec)return;_0x294bee[_0x4df9fc(0x135)]=setTimeout(function(_0x8d1a62){_0x17bcad(_0x8d1a62);},0x21,_0x294bee);}return _0x435689['recieveChunkedStream']=async function(_0x5e5f87,_0x3addf7){var _0xbbc795=_0x120577;log('Created\x20transfer\x20channel');if(!_0x435689['rpcs'][_0x5e5f87]){errorlog(_0xbbc795(0x144));return;}!_0x435689[_0xbbc795(0xc6c)][_0x5e5f87]['chunkedChannels']?_0x435689['rpcs'][_0x5e5f87][_0xbbc795(0x9e1)]=[]:_0x435689[_0xbbc795(0xc6c)][_0x5e5f87][_0xbbc795(0x9e1)][_0xbbc795(0x3d5)](_0x2ad5f2=>{var _0x5be747=_0xbbc795;_0x2ad5f2[_0x5be747(0x341)]&&_0x2ad5f2[_0x5be747(0x341)]['close']();});var _0x5010b4='',_0x55fea0=0x0,_0x4bcc3b=![],_0x111b31=![],_0x4723cb={};_0x4723cb[_0xbbc795(0x341)]=_0x3addf7,_0x435689[_0xbbc795(0xc6c)][_0x5e5f87]['chunkedChannels'][_0xbbc795(0x35c)](_0x4723cb),_0x4723cb[_0xbbc795(0x341)][_0xbbc795(0x33c)]=_0xbbc795(0x4fd),_0x4723cb[_0xbbc795(0x341)][_0xbbc795(0xada)]=_0x34d6bc=>{var _0x948487=_0xbbc795;log(_0x948487(0x302));},_0x4723cb[_0xbbc795(0x341)][_0xbbc795(0x4ab)]=async function(_0x47fc0d){var _0x2e879d=_0xbbc795;if(_0x4723cb&&_0x4723cb[_0x2e879d(0x1d8)]){if(_0x4723cb&&_0x4723cb[_0x2e879d(0x917)][_0x2e879d(0x77e)]){await delay(0x3e8);try{await _0x4723cb['videoElement']['stopWriter']();}catch(_0x4e65bf){}}}_0x435689[_0x2e879d(0xc6c)][_0x5e5f87]&&(delete _0x435689[_0x2e879d(0xc6c)][_0x5e5f87][_0x2e879d(0x5c2)][_0x2e879d(0x5e0)],delete _0x435689[_0x2e879d(0xc6c)][_0x5e5f87][_0x2e879d(0x5c2)]['chunked_mode_audio']);return;};async function _0x3fe5bd(){var _0x54b213=_0xbbc795,_0x11dbc8=await window[_0x54b213(0x867)]({'startIn':_0x54b213(0x6e8),'suggestedName':_0x54b213(0x8f1),'types':[{'description':_0x54b213(0x1f5),'accept':{'video/webm':[_0x54b213(0x984)]}}]}),_0x320161=await _0x11dbc8[_0x54b213(0xa9b)]();return _0x4723cb['writer_config']['fileWriter']=_0x320161,_0x4723cb[_0x54b213(0x1d8)]=new WebMWriter(_0x4723cb[_0x54b213(0x461)]),_0x4723cb[_0x54b213(0x917)]['stopWriter']=async function(_0x4410a3=![]){var _0x260498=_0x54b213;_0x4410a3?(_0x4723cb[_0x260498(0x461)][_0x260498(0x7d1)][_0x260498(0x757)](),_0x4723cb[_0x260498(0x917)]['stopWriter']=![],clearInterval(_0x4723cb[_0x260498(0x215)]),_0x4723cb['updateTime']=null,await _0x4723cb[_0x260498(0x1d8)][_0x260498(0x539)]()):(_0x4723cb['videoElement'][_0x260498(0x77e)]=![],clearInterval(_0x4723cb[_0x260498(0x215)]),_0x4723cb[_0x260498(0x215)]=null,await _0x4723cb[_0x260498(0x1d8)][_0x260498(0x539)](),_0x4723cb[_0x260498(0x461)][_0x260498(0x7d1)][_0x260498(0x757)]());},_0x4723cb[_0x54b213(0x1d8)];}const _0x45e5f6=0x3,_0x599767=0xb4;function _0x4f98be(){var _0x45d063=_0xbbc795;return!_0x4723cb[_0x45d063(0x3e5)]&&(_0x4723cb[_0x45d063(0x3e5)]={'frames':new Map(),'order':[],'current':null,'pendingResend':null,'legacyFrameId':0x0,'legacyMode':![],'stats':{'fecRepairs':0x0,'nacksSent':0x0,'framesDropped':0x0}}),_0x4723cb['chunkReliability'];}function _0x5a24d6(_0x157b96){var _0x5e798b=_0xbbc795;const _0x4ed561={'timestamp':_0x157b96[0x0],'type':_0x157b96[0x1],'extra':null,'queueDepth':null};for(let _0x19d3c6=0x2;_0x19d3c6<_0x157b96[_0x5e798b(0xb04)];_0x19d3c6++){const _0x12ee5c=_0x157b96[_0x19d3c6];if(_0x12ee5c&&typeof _0x12ee5c===_0x5e798b(0x31f)&&!Array[_0x5e798b(0x45b)](_0x12ee5c))_0x4ed561[_0x5e798b(0x3bd)]=_0x12ee5c;else typeof _0x12ee5c===_0x5e798b(0x91d)&&_0x4ed561[_0x5e798b(0xaf5)]===null&&(_0x4ed561[_0x5e798b(0xaf5)]=_0x12ee5c);}return _0x4ed561;}function _0x1bdfca(_0x2da51a){var _0x8d5cdc=_0xbbc795;const _0x2addbd=_0x4f98be();return _0x2addbd[_0x8d5cdc(0x86c)]['get'](_0x2da51a>>>0x0)||null;}function _0x5f01f5(_0x451277){var _0x450c03=_0xbbc795;const _0x59110f=_0x4f98be(),_0x5031ec=_0x451277[_0x450c03(0x3bd)]||{};let _0x47a810;typeof _0x5031ec['frameId']==='number'&&Number[_0x450c03(0x7a3)](_0x5031ec[_0x450c03(0x8b9)])?_0x47a810=_0x5031ec[_0x450c03(0x8b9)]>>>0x0:(_0x59110f[_0x450c03(0x9fa)]+=0x1,_0x47a810=_0x59110f[_0x450c03(0x9fa)]>>>0x0);const _0x386678=Array['isArray'](_0x5031ec[_0x450c03(0x395)])&&_0x5031ec[_0x450c03(0x395)][_0x450c03(0xb04)]?_0x5031ec[_0x450c03(0x395)]:[{'index':0x0,'size':_0x5031ec['dataBytes']||0x0,'group':0x0}],_0x749691=Array['isArray'](_0x5031ec[_0x450c03(0x2a7)])?_0x5031ec[_0x450c03(0x2a7)]:[],_0x14d7e8={'frameId':_0x47a810,'timestamp':_0x451277[_0x450c03(0x95f)],'type':_0x451277[_0x450c03(0x2dd)],'media':_0x5031ec[_0x450c03(0x6fd)]||(_0x451277[_0x450c03(0x2dd)]===_0x450c03(0x2de)||_0x451277[_0x450c03(0x2dd)]===_0x450c03(0x5c4)?_0x450c03(0x2de):_0x450c03(0x57d)),'descriptors':_0x386678,'parityDescriptors':_0x749691,'data':new Array(_0x386678['length'])[_0x450c03(0x848)](null),'parity':new Array(_0x749691[_0x450c03(0xb04)])[_0x450c03(0x848)](null),'nextDataIndex':0x0,'nextParityIndex':0x0,'queueDepth':_0x451277[_0x450c03(0xaf5)],'nackAttempts':{},'complete':![],'dropped':![],'assembled':null,'createdAt':Date['now'](),'nackEnabled':!!_0x5031ec[_0x450c03(0xa82)]};return _0x59110f[_0x450c03(0x86c)][_0x450c03(0x50e)](_0x47a810,_0x14d7e8),_0x59110f['order']['push'](_0x47a810),_0x59110f[_0x450c03(0x408)]=_0x14d7e8,_0x59110f['pendingResend']=null,_0x59110f[_0x450c03(0x7aa)]=![],_0x14d7e8;}function _0x37a603(_0x3c48bb,_0x240b46,_0x268dcb=![]){var _0x4ee321=_0xbbc795;if(!_0x3c48bb||!_0x3c48bb['nackEnabled'])return;if(!_0x3c48bb)return;const _0x28506=(_0x268dcb?'p':'d')+':'+_0x240b46,_0x5e2e04=_0x3c48bb[_0x4ee321(0x9b4)][_0x28506]||0x0;if(_0x5e2e04>=_0x45e5f6)return;_0x3c48bb[_0x4ee321(0x9b4)][_0x28506]=_0x5e2e04+0x1,setTimeout(()=>{var _0x1a3652=_0x4ee321;try{_0x4723cb[_0x1a3652(0x341)][_0x1a3652(0xac5)](JSON['stringify']({'type':_0x1a3652(0xa82),'frameId':_0x3c48bb[_0x1a3652(0x8b9)],'chunkIndex':_0x240b46,'parity':_0x268dcb}));const _0x574f67=_0x4f98be();_0x574f67[_0x1a3652(0x5c2)][_0x1a3652(0xa94)]+=0x1;if(_0x435689[_0x1a3652(0xc6c)][_0x4723cb[_0x1a3652(0x9d6)]]&&_0x435689['rpcs'][_0x4723cb[_0x1a3652(0x9d6)]][_0x1a3652(0x5c2)]){const _0x327350=_0x435689[_0x1a3652(0xc6c)][_0x4723cb['UUID']][_0x1a3652(0x5c2)][_0x1a3652(0x5e0)]=_0x435689[_0x1a3652(0xc6c)][_0x4723cb[_0x1a3652(0x9d6)]][_0x1a3652(0x5c2)][_0x1a3652(0x5e0)]||{};_0x327350['nacks_sent']=(_0x327350[_0x1a3652(0x184)]||0x0)+0x1;}}catch(_0x5e056a){errorlog(_0x5e056a);}},_0x599767);}function _0x3bae7c(_0x2dc233){var _0x437a0f=_0xbbc795;const _0x3e65fb=_0x2dc233['reduce']((_0x4be570,_0x2ac572)=>_0x4be570+(_0x2ac572?_0x2ac572[_0x437a0f(0x893)]:0x0),0x0),_0x5bdaf5=new Uint8Array(_0x3e65fb);let _0x3c38d8=0x0;for(const _0x14cc52 of _0x2dc233){if(!_0x14cc52)continue;_0x5bdaf5['set'](_0x14cc52,_0x3c38d8),_0x3c38d8+=_0x14cc52[_0x437a0f(0x893)];}return _0x5bdaf5;}function _0x3d53aa(_0x5125af,_0x4c874c){var _0x41c152=_0xbbc795;const _0x5aa9ce=_0x5125af['parityDescriptors'][_0x4c874c];if(!_0x5aa9ce)return;const _0x1afbf0=_0x5aa9ce[_0x41c152(0x21d)]||0x0,_0x4c7819=_0x5aa9ce[_0x41c152(0x5c8)]||_0x5125af['descriptors']['length'];let _0x1d3639=-0x1;for(let _0x4e8053=0x0;_0x4e8053<_0x4c7819;_0x4e8053++){const _0x3c8fe5=_0x1afbf0+_0x4e8053;if(!_0x5125af['data'][_0x3c8fe5]){if(_0x1d3639!==-0x1)return;_0x1d3639=_0x3c8fe5;}}if(_0x1d3639===-0x1)return;const _0x161dd7=_0x5125af[_0x41c152(0x19e)][_0x4c874c];if(!_0x161dd7)return;const _0x236913=_0x5125af['descriptors'][_0x1d3639]&&_0x5125af[_0x41c152(0x395)][_0x1d3639][_0x41c152(0x735)]?_0x5125af[_0x41c152(0x395)][_0x1d3639][_0x41c152(0x735)]:_0x161dd7['byteLength'],_0x28a493=new Uint8Array(_0x161dd7[_0x41c152(0x893)]);_0x28a493[_0x41c152(0x50e)](_0x161dd7);for(let _0x4adf3b=0x0;_0x4adf3b<_0x4c7819;_0x4adf3b++){const _0x12c5ef=_0x1afbf0+_0x4adf3b;if(_0x12c5ef===_0x1d3639)continue;const _0xf35310=_0x5125af[_0x41c152(0x36f)][_0x12c5ef];if(!_0xf35310)return;for(let _0x9d73ca=0x0;_0x9d73ca<_0x28a493['byteLength'];_0x9d73ca++){_0x28a493[_0x9d73ca]^=_0x9d73ca<_0xf35310[_0x41c152(0x893)]?_0xf35310[_0x9d73ca]:0x0;}}_0x5125af[_0x41c152(0x36f)][_0x1d3639]=_0x28a493['slice'](0x0,_0x236913);const _0x145054=_0x4f98be();_0x145054[_0x41c152(0x5c2)][_0x41c152(0x74d)]+=0x1;if(_0x435689[_0x41c152(0xc6c)][_0x4723cb['UUID']]&&_0x435689[_0x41c152(0xc6c)][_0x4723cb[_0x41c152(0x9d6)]][_0x41c152(0x5c2)]){const _0x356a9f=_0x435689['rpcs'][_0x4723cb[_0x41c152(0x9d6)]][_0x41c152(0x5c2)][_0x41c152(0x5e0)]=_0x435689[_0x41c152(0xc6c)][_0x4723cb[_0x41c152(0x9d6)]][_0x41c152(0x5c2)][_0x41c152(0x5e0)]||{};_0x356a9f[_0x41c152(0x149)]=(_0x356a9f['fec_repairs']||0x0)+0x1;}}function _0x2935ab(_0x1312be,_0x45f93d){var _0x16e4f8=_0xbbc795;_0x1312be[_0x16e4f8(0x539)]=!![],_0x1312be[_0x16e4f8(0x42e)]=_0x45f93d||null;}function _0x59a550(_0x2957fd){var _0x36aec3=_0xbbc795;const _0x4c6558=_0x4f98be();_0x4c6558[_0x36aec3(0x5c2)]['framesDropped']=(_0x4c6558[_0x36aec3(0x5c2)][_0x36aec3(0x57e)]||0x0)+0x1;if(_0x435689['rpcs'][_0x4723cb[_0x36aec3(0x9d6)]]&&_0x435689['rpcs'][_0x4723cb[_0x36aec3(0x9d6)]][_0x36aec3(0x5c2)]){const _0x2bc905=_0x435689['rpcs'][_0x4723cb[_0x36aec3(0x9d6)]][_0x36aec3(0x5c2)][_0x36aec3(0x5e0)]=_0x435689['rpcs'][_0x4723cb['UUID']][_0x36aec3(0x5c2)]['chunked_mode_video']||{};_0x2bc905[_0x36aec3(0x33b)]=(_0x2bc905['frames_dropped']||0x0)+0x1;}_0x2957fd[_0x36aec3(0x1a2)]=!![];}async function _0x28f9a5(_0x5f1b0f){var _0x536d2f=_0xbbc795;if(!_0x5f1b0f||_0x5f1b0f['complete'])return;const _0x485337=[];for(let _0x529229=0x0;_0x529229<_0x5f1b0f[_0x536d2f(0x36f)][_0x536d2f(0xb04)];_0x529229++){!_0x5f1b0f[_0x536d2f(0x36f)][_0x529229]&&_0x485337[_0x536d2f(0x35c)](_0x529229);}if(!_0x485337[_0x536d2f(0xb04)]){_0x2935ab(_0x5f1b0f,_0x3bae7c(_0x5f1b0f[_0x536d2f(0x36f)]));return;}if(!_0x5f1b0f['nackEnabled']){_0x59a550(_0x5f1b0f),_0x2935ab(_0x5f1b0f,null);return;}_0x485337['forEach'](_0x260b47=>_0x37a603(_0x5f1b0f,_0x260b47));const _0x57c787=_0x485337[_0x536d2f(0x1a1)](_0xe37e8e=>(_0x5f1b0f[_0x536d2f(0x9b4)]['d:'+_0xe37e8e]||0x0)>=_0x45e5f6);_0x57c787&&(_0x59a550(_0x5f1b0f),_0x2935ab(_0x5f1b0f,null));}async function _0x2e085f(){var _0x29783a=_0xbbc795;const _0xae37e7=_0x4f98be();while(_0xae37e7[_0x29783a(0x367)][_0x29783a(0xb04)]){const _0x226e08=_0xae37e7[_0x29783a(0x367)][0x0],_0x153f8d=_0xae37e7[_0x29783a(0x86c)][_0x29783a(0xa1b)](_0x226e08);if(!_0x153f8d||!_0x153f8d['complete'])break;_0xae37e7[_0x29783a(0x367)]['shift'](),_0xae37e7['frames'][_0x29783a(0x5cd)](_0x226e08);if(_0x153f8d[_0x29783a(0x1a2)]||!_0x153f8d[_0x29783a(0x42e)])continue;await _0x4723cb[_0x29783a(0xa92)]({'data':_0x153f8d[_0x29783a(0x42e)],'timestamp':_0x153f8d['timestamp'],'type':_0x153f8d[_0x29783a(0x2dd)]});}}function _0xe073aa(_0x3772c0){var _0x38c1bc=_0xbbc795;const _0x2ab3e0=_0x4f98be(),_0x36b04d=_0x5a24d6(_0x3772c0);if(!_0x36b04d[_0x38c1bc(0x3bd)]||typeof _0x36b04d[_0x38c1bc(0x3bd)]!==_0x38c1bc(0x31f)){_0x2ab3e0[_0x38c1bc(0x7aa)]=!![],_0x2ab3e0[_0x38c1bc(0x408)]=null,_0x2ab3e0[_0x38c1bc(0x740)]=null,_0x4723cb[_0x38c1bc(0x28b)]=_0x3772c0;return;}if(_0x36b04d[_0x38c1bc(0x3bd)][_0x38c1bc(0x622)]){const _0x3bc4aa=_0x1bdfca(_0x36b04d[_0x38c1bc(0x3bd)][_0x38c1bc(0x8b9)]);if(!_0x3bc4aa)return;_0x2ab3e0[_0x38c1bc(0x740)]={'frame':_0x3bc4aa,'parity':!!_0x36b04d[_0x38c1bc(0x3bd)][_0x38c1bc(0x19e)],'chunkIndex':Number[_0x38c1bc(0x7a3)](_0x36b04d[_0x38c1bc(0x3bd)][_0x38c1bc(0x572)])?parseInt(_0x36b04d[_0x38c1bc(0x3bd)][_0x38c1bc(0x572)]):0x0,'parityIndex':Number[_0x38c1bc(0x7a3)](_0x36b04d[_0x38c1bc(0x3bd)]['parityIndex'])?parseInt(_0x36b04d[_0x38c1bc(0x3bd)][_0x38c1bc(0xa16)]):Number[_0x38c1bc(0x7a3)](_0x36b04d[_0x38c1bc(0x3bd)]['chunkIndex'])?parseInt(_0x36b04d[_0x38c1bc(0x3bd)]['chunkIndex']):0x0};return;}const _0x2578c6=_0x5f01f5(_0x36b04d);_0x2578c6['data'][_0x38c1bc(0xb04)]===0x0&&(_0x2578c6['data']=[null]);}async function _0x48a48e(_0x47760a){var _0x1c7a59=_0xbbc795;const _0x6aa024=_0x4f98be(),_0x5d475c=_0x47760a instanceof Uint8Array?_0x47760a:new Uint8Array(_0x47760a);if(_0x6aa024[_0x1c7a59(0x7aa)]){await _0x5571cd(_0x5d475c);return;}if(_0x6aa024['pendingResend']){const _0x203275=_0x6aa024[_0x1c7a59(0x740)][_0x1c7a59(0x597)];_0x203275&&(_0x6aa024[_0x1c7a59(0x740)]['parity']?(_0x203275[_0x1c7a59(0x19e)][_0x6aa024[_0x1c7a59(0x740)][_0x1c7a59(0xa16)]]=_0x5d475c,_0x3d53aa(_0x203275,_0x6aa024['pendingResend'][_0x1c7a59(0xa16)])):_0x203275['data'][_0x6aa024['pendingResend'][_0x1c7a59(0x572)]]=_0x5d475c,await _0x28f9a5(_0x203275),await _0x2e085f());_0x6aa024[_0x1c7a59(0x740)]=null;return;}const _0x181805=_0x6aa024[_0x1c7a59(0x408)];if(!_0x181805){await _0x5571cd(_0x5d475c);return;}if(_0x181805[_0x1c7a59(0x441)]<_0x181805[_0x1c7a59(0x36f)][_0x1c7a59(0xb04)]){_0x181805['data'][_0x181805[_0x1c7a59(0x441)]]=_0x5d475c,_0x181805[_0x1c7a59(0x441)]+=0x1;_0x181805[_0x1c7a59(0x441)]===_0x181805[_0x1c7a59(0x36f)][_0x1c7a59(0xb04)]&&_0x181805[_0x1c7a59(0x19e)][_0x1c7a59(0xb04)]===0x0&&(await _0x28f9a5(_0x181805),_0x6aa024[_0x1c7a59(0x408)]=null,await _0x2e085f());return;}if(_0x181805[_0x1c7a59(0x64a)]<_0x181805[_0x1c7a59(0x19e)][_0x1c7a59(0xb04)]){_0x181805[_0x1c7a59(0x19e)][_0x181805['nextParityIndex']]=_0x5d475c,_0x3d53aa(_0x181805,_0x181805[_0x1c7a59(0x64a)]),_0x181805[_0x1c7a59(0x64a)]+=0x1;_0x181805[_0x1c7a59(0x64a)]===_0x181805[_0x1c7a59(0x19e)]['length']&&(await _0x28f9a5(_0x181805),_0x6aa024[_0x1c7a59(0x408)]=null,await _0x2e085f());return;}await _0x5571cd(_0x5d475c);}async function _0x5571cd(_0x32ba62){var _0x234ed0=_0xbbc795;if(_0x32ba62[_0x234ed0(0x893)]>=0x40000){if(_0x4723cb['buffer']){const _0x4d81bf=new Uint8Array(_0x4723cb['buffer'][_0x234ed0(0xb04)]+_0x32ba62['byteLength']);_0x4d81bf['set'](_0x4723cb[_0x234ed0(0x237)]),_0x4d81bf['set'](_0x32ba62,_0x4723cb[_0x234ed0(0x237)]['length']),_0x4723cb['buffer']=_0x4d81bf;}else _0x4723cb[_0x234ed0(0x237)]=new Uint8Array(_0x32ba62);return;}if(_0x4723cb[_0x234ed0(0x237)]){const _0x37341d=new Uint8Array(_0x4723cb[_0x234ed0(0x237)][_0x234ed0(0xb04)]+_0x32ba62[_0x234ed0(0x893)]);_0x37341d[_0x234ed0(0x50e)](_0x4723cb[_0x234ed0(0x237)]),_0x37341d[_0x234ed0(0x50e)](_0x32ba62,_0x4723cb['buffer']['length']),_0x4723cb[_0x234ed0(0x237)]=null,await _0x4723cb[_0x234ed0(0xa92)]({'data':_0x37341d,'timestamp':_0x4723cb[_0x234ed0(0x28b)][0x0],'type':_0x4723cb[_0x234ed0(0x28b)][0x1]});}else await _0x4723cb[_0x234ed0(0xa92)]({'data':_0x32ba62,'timestamp':_0x4723cb[_0x234ed0(0x28b)][0x0],'type':_0x4723cb['frameMeta'][0x1]});}_0x4723cb['channel'][_0xbbc795(0x13c)]=async function(_0x25ad7c){var _0x4436be=_0xbbc795;if(!_0x4bcc3b)try{let _0x209c59=JSON[_0x4436be(0x48a)](_0x25ad7c[_0x4436be(0x36f)]);if(_0x209c59['type']==_0x4436be(0x3ae)){log(_0x4436be(0x4eb)),_0x4bcc3b=_0x209c59;_0x435689[_0x4436be(0x88f)]&&_0x435689['retransmitChunkedStream'](_0x4bcc3b,_0x4723cb[_0x4436be(0x341)]);log(_0x4436be(0x5a8)),log(_0x4bcc3b),_0x4723cb['details']=_0x4bcc3b,_0x4723cb[_0x4436be(0x9d6)]=_0x5e5f87,_0x4723cb[_0x4436be(0x65f)]=0x0,_0x4723cb[_0x4436be(0x31b)]=0x2,_0x4723cb[_0x4436be(0x282)]=Date[_0x4436be(0x1ab)](),_0x4723cb[_0x4436be(0x584)]=_0x4bcc3b['timestamp'],_0x4723cb['remoteTimestampBase']=_0x4bcc3b[_0x4436be(0x95f)]||_0x4723cb[_0x4436be(0x282)],_0x4723cb[_0x4436be(0x229)]=_0x4723cb[_0x4436be(0x282)]-_0x4723cb[_0x4436be(0x1ae)];typeof performance!==_0x4436be(0xba0)&&performance[_0x4436be(0x1ab)]?(_0x4723cb[_0x4436be(0x4a4)]=performance['now'](),_0x4723cb[_0x4436be(0xa00)]=function(){var _0x38cc55=_0x4436be;return _0x4723cb['remoteTimestampBase']+(performance[_0x38cc55(0x1ab)]()-_0x4723cb[_0x38cc55(0x4a4)]);}):_0x4723cb['getRemoteNow']=function(){var _0x657fd6=_0x4436be;return Date[_0x657fd6(0x1ab)]()-_0x4723cb[_0x657fd6(0x229)];};_0x4723cb['dc']=_0x4723cb['channel'],_0x4723cb['id']=_0x4bcc3b['id'],_0x4723cb['updateTime']=null,_0x4723cb[_0x4436be(0x237)]=![];!_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x917)]&&(_0x435689['rpcs'][_0x5e5f87]['videoElement']=createVideoElement());_0x4723cb['videoElement']=_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x917)];!_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x917)][_0x4436be(0x200)]&&(_0x435689['rpcs'][_0x5e5f87][_0x4436be(0x917)][_0x4436be(0x200)]=createMediaStream());!_0x435689[_0x4436be(0xc6c)][_0x5e5f87]['streamSrc']&&(_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x857)]=createMediaStream());_0x4723cb[_0x4436be(0x857)]=_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x857)],_0x4723cb[_0x4436be(0x917)][_0x4436be(0x317)]=!![],_0x4723cb[_0x4436be(0x917)][_0x4436be(0xb62)]=![],_0x4723cb['videoElement']['setAttribute'](_0x4436be(0xa0c),''),_0x4723cb[_0x4436be(0x917)]['dataset']['sid']=_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x885)],_0x4723cb[_0x4436be(0x917)]['id']=_0x4436be(0x9e9)+_0x5e5f87,_0x4723cb[_0x4436be(0x917)][_0x4436be(0xc6b)][_0x4436be(0x9d6)]=_0x5e5f87,_0x4723cb[_0x4436be(0x917)][_0x4436be(0x3ae)]=!![];_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x2f9)]&&applyMirrorGuest(_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x2f9)],_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x917)],_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x1a4)]);_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0xc40)]!==![]&&(_0x435689['rpcs'][_0x5e5f87]['videoElement']['rotated']=_0x435689[_0x4436be(0xc6c)][_0x5e5f87]['rotate'],_0x435689['rpcs'][_0x5e5f87]['videoElement'][_0x4436be(0xc6b)][_0x4436be(0x388)]=_0x435689[_0x4436be(0xc6c)][_0x5e5f87]['rotate']);_0x4723cb[_0x4436be(0x917)]['addEventListener'](_0x4436be(0x145),_0x4143b1=>{var _0x29fbe8=_0x4436be;try{var _0x4110d7=document[_0x29fbe8(0x54c)]('bigPlayButton');_0x4110d7&&_0x4110d7[_0x29fbe8(0x631)]['removeChild'](_0x4110d7);}catch(_0x19a643){}_0x4723cb[_0x29fbe8(0x145)]=!![];if(_0x4723cb[_0x29fbe8(0x6f8)])_0x4723cb['audioContext']['resume']();else _0x435689['audioCtx']&&_0x435689['audioCtx'][_0x29fbe8(0x4ee)]();try{_0x435689[_0x29fbe8(0x6bb)]&&(v['readyState']>=0x3&&(!v[_0x29fbe8(0x6bb)]&&(v[_0x29fbe8(0x6bb)]=!![],toggleSystemPip(v,!![]))));}catch(_0x194dd0){}},{'once':!![]}),_0x4723cb['videoElement'][_0x4436be(0xc15)](_0x4436be(0x4bc),function(_0x2d27de){errorlog(_0x2d27de);}),_0x4723cb[_0x4436be(0x917)][_0x4436be(0xbd8)]=_0x3fe5bd,_0x4723cb['videoElement']['oncanplay']=function(){updateMixer();},_0x4723cb['videoWriter']=![],_0x4723cb[_0x4436be(0x28b)]=![],_0x4723cb[_0x4436be(0x461)]={},_0x4723cb[_0x4436be(0x461)][_0x4436be(0x57d)]=![],_0x4723cb['writer_config']['audio']=![],_0x4723cb[_0x4436be(0x785)]=Number[_0x4436be(0x7a3)](parseFloat(_0x435689[_0x4436be(0x843)]))?parseFloat(_0x435689['chunkjitterslack']):0x0,_0x4723cb[_0x4436be(0x20b)]=![],_0x4723cb[_0x4436be(0x5ab)]=![],_0x4723cb[_0x4436be(0x99f)]=![],_0x4723cb[_0x4436be(0x79b)]=![],_0x4723cb[_0x4436be(0x57d)]=![],_0x4723cb['audio']=![],_0x4723cb['promise_audio']=![],_0x4723cb['playing']=![],_0x4723cb['video_session']=0x1;if(_0x4bcc3b['configVideo']){_0x435689['rpcs'][_0x5e5f87][_0x4436be(0x5c2)][_0x4436be(0x5e0)]=_0x4bcc3b[_0x4436be(0x27c)],_0x4723cb[_0x4436be(0x20b)]={};const _0x58466d=_0x25a646(parseInt(_0x4bcc3b['configVideo'][_0x4436be(0x3dd)])||0x500,0x2),_0x2cd252=_0x25a646(parseInt(_0x4bcc3b[_0x4436be(0x27c)][_0x4436be(0x348)])||0x2d0,0x2);_0x4723cb[_0x4436be(0x20b)][_0x4436be(0x3dd)]=_0x58466d,_0x4723cb[_0x4436be(0x20b)][_0x4436be(0x348)]=_0x2cd252,_0x4723cb[_0x4436be(0x20b)]['codec']=_0x4bcc3b[_0x4436be(0x27c)]['codec']||_0x4436be(0xb7e),_0x4723cb[_0x4436be(0x461)][_0x4436be(0x57d)]=!![],_0x4723cb[_0x4436be(0x461)]['width']=_0x58466d,_0x4723cb['writer_config'][_0x4436be(0x348)]=_0x2cd252;if(_0x4bcc3b['configVideo'][_0x4436be(0x1a3)]=='vp09.00.10.08')_0x4723cb[_0x4436be(0x461)][_0x4436be(0x1a3)]=_0x4436be(0xb75);else{if(_0x4bcc3b[_0x4436be(0x27c)][_0x4436be(0x1a3)]==_0x4436be(0xa19))_0x4723cb[_0x4436be(0x461)]['codec']=_0x4436be(0xa6c);else{if(_0x4bcc3b[_0x4436be(0x27c)][_0x4436be(0x1a3)]==_0x4436be(0xc3b))_0x4723cb[_0x4436be(0x461)][_0x4436be(0x1a3)]=_0x4436be(0xa6c);else{if(_0x4bcc3b['configVideo'][_0x4436be(0x1a3)]==_0x4436be(0x4f5))_0x4723cb[_0x4436be(0x461)]['codec']=_0x4436be(0x502);else _0x4bcc3b[_0x4436be(0x27c)]['codec']==_0x4436be(0x6f3)?_0x4723cb[_0x4436be(0x461)][_0x4436be(0x1a3)]=_0x4436be(0xa25):_0x4723cb[_0x4436be(0x461)][_0x4436be(0x1a3)]=_0x4436be(0xb75);}}}_0x4723cb[_0x4436be(0x99f)]={'output':_0x125f93=>{var _0x1b1c24=_0x4436be;try{_0x4723cb[_0x1b1c24(0x57d)][_0x1b1c24(0xb0a)]['write'](_0x125f93)[_0x1b1c24(0x501)](_0x45f1e0=>{});}catch(_0x12795c){}},'error':_0x15ad03=>{var _0x19d226=_0x4436be;_0x4723cb[_0x19d226(0x57d)][_0x19d226(0x213)][_0x19d226(0x203)]==_0x19d226(0x94c)?(errorlog(_0x15ad03['message']),warnlog(_0x19d226(0x694))):errorlog(_0x15ad03[_0x19d226(0x973)]);}},_0x4723cb[_0x4436be(0x57d)]={},_0x4723cb[_0x4436be(0x57d)][_0x4436be(0x36a)]=new MediaStreamTrackGenerator({'kind':_0x4436be(0x57d)}),_0x4723cb[_0x4436be(0x57d)][_0x4436be(0xc2e)]=new MediaStream([_0x4723cb['video'][_0x4436be(0x36a)]]),_0x4723cb[_0x4436be(0x57d)][_0x4436be(0xb0a)]=_0x4723cb['video'][_0x4436be(0x36a)][_0x4436be(0x9a8)][_0x4436be(0x862)](),_0x4723cb[_0x4436be(0x57d)]['decoder']=new VideoDecoder(_0x4723cb[_0x4436be(0x99f)]),_0x4723cb[_0x4436be(0x57d)]['decoder']['configure'](_0x4723cb['stream_configVideo']),_0x4723cb[_0x4436be(0x57d)][_0x4436be(0x8f5)]=[],_0x4723cb[_0x4436be(0x57d)][_0x4436be(0x135)]=null,_0x4723cb[_0x4436be(0x57d)][_0x4436be(0x7f1)]=![],_0x4723cb[_0x4436be(0x57d)][_0x4436be(0x609)]=![],_0x4436be(0x18a)in _0x4bcc3b&&(_0x4723cb[_0x4436be(0x57d)]['realTime']=_0x4bcc3b[_0x4436be(0x18a)]),_0x4723cb['streamSrc']['addTrack'](_0x4723cb[_0x4436be(0x57d)]['stream']['getVideoTracks']()[0x0]);}const _0x4a4b27=function(_0x3f0954=_0x4436be(0x57d),_0x4e123d=0xc8){var _0x33b2cc=_0x4436be;const _0xdb6e23=_0x435689['rpcs'][_0x4723cb[_0x33b2cc(0x9d6)]],_0x14093c=_0x1c03dc=>{if(_0x1c03dc===![]||_0x1c03dc===null||_0x1c03dc===undefined)return null;const _0x380641=parseFloat(_0x1c03dc);return Number['isFinite'](_0x380641)?_0x380641:null;},_0xc510f=_0x14093c(_0x3f0954===_0x33b2cc(0x57d)?_0x435689[_0x33b2cc(0xc5d)]:_0x435689[_0x33b2cc(0x7a8)]);let _0xa6fe49=_0xc510f!==null?_0xc510f:_0x4e123d;if(_0xdb6e23){if(_0x3f0954===_0x33b2cc(0x2de)){const _0x5053ea=_0x14093c(_0x435689[_0x33b2cc(0x7a8)]);_0x5053ea!==null&&(_0xa6fe49=_0x5053ea);}const _0x4b54b2=_0x14093c(_0xdb6e23[_0x33b2cc(0x237)]);if(_0x4b54b2!==null)_0xa6fe49=_0x4b54b2;else{const _0x3af2e3=_0x14093c(_0x435689[_0x33b2cc(0x237)]);_0x3af2e3!==null?_0xa6fe49=_0x3af2e3:_0xdb6e23[_0x33b2cc(0x237)]=_0xa6fe49;}}const _0x32fd68=_0x14093c(_0x435689[_0x33b2cc(0x6c6)]),_0x24d49c=_0x14093c(_0x435689[_0x33b2cc(0x9ef)]),_0x5adfd1=_0x32fd68!==null?_0x32fd68:0x0,_0x2199a3=_0x3f0954==='audio'?0x1388:0x7530,_0x4c6470=_0x24d49c!==null?_0x24d49c:_0x2199a3;if(typeof _0x435689['calculateOptimalBufferSize']==='function')try{const _0x270128=_0x435689[_0x33b2cc(0x6da)](_0x4723cb[_0x33b2cc(0x9d6)],{'media':_0x3f0954,'base':_0xa6fe49,'clamp':{'min':_0x5adfd1,'max':_0x4c6470}});typeof _0x270128===_0x33b2cc(0x91d)&&!Number['isNaN'](_0x270128)&&(_0xa6fe49=_0x270128);}catch(_0x1d3eb6){errorlog(_0x1d3eb6);}return!Number[_0x33b2cc(0x7a3)](_0xa6fe49)&&(_0xa6fe49=_0x4e123d),_0xa6fe49=Math['max'](_0x5adfd1,Math[_0x33b2cc(0x452)](_0xa6fe49,_0x4c6470)),_0xa6fe49;};_0x4bcc3b[_0x4436be(0xa39)]&&(_0x435689[_0x4436be(0xc6c)][_0x5e5f87][_0x4436be(0x5c2)]['chunked_mode_audio']=_0x4bcc3b[_0x4436be(0xa39)],_0x4723cb[_0x4436be(0x5ab)]=_0x4bcc3b[_0x4436be(0xa39)],_0x4723cb[_0x4436be(0x461)][_0x4436be(0x2de)]=!![],_0x4723cb[_0x4436be(0x461)][_0x4436be(0xbe9)]=_0x4bcc3b['configAudio']['sampleRate']||0xbb80,_0x4723cb['writer_config'][_0x4436be(0x4b2)]=_0x4bcc3b[_0x4436be(0xa39)]['numberOfChannels']||0x1,_0x4723cb[_0x4436be(0x5ab)]['codec']&&_0x4723cb['stream_configAudio']['codec']==_0x4436be(0x5c4)?(!_0x4723cb[_0x4436be(0x1e9)]?_0x4723cb[_0x4436be(0x1e9)]=_0x435689[_0x4436be(0x519)]['createMediaStreamDestination']():_0x4723cb['streamSrc'][_0x4436be(0x253)]()[_0x4436be(0x3d5)](_0x54d08f=>{var _0x484fc0=_0x4436be;_0x4723cb[_0x484fc0(0x857)][_0x484fc0(0x8cd)](_0x54d08f);}),_0x4723cb['destination'][_0x4436be(0xc2e)][_0x4436be(0x253)]()[_0x4436be(0x3d5)](_0x125e4e=>{var _0x172f7d=_0x4436be;_0x4723cb[_0x172f7d(0x857)][_0x172f7d(0x2e6)](_0x125e4e);}),_0x4723cb[_0x4436be(0x233)]=!![]):(!_0x4723cb['audio']&&(_0x4723cb[_0x4436be(0x2de)]={}),_0x4723cb[_0x4436be(0x2de)][_0x4436be(0x8f5)]=[],_0x4723cb[_0x4436be(0x2de)][_0x4436be(0x135)]=null,_0x4436be(0x6d3)in _0x4bcc3b?_0x4723cb['audio']['realTime']=_0x4bcc3b[_0x4436be(0x6d3)]:errorlog('No\x20realtime'),_0x4723cb['init_audio']={'output':_0x9bae49=>{var _0x37ece7=_0x4436be;_0x4723cb['audio'][_0x37ece7(0xb0a)]['write'](_0x9bae49);const _0x111d1f=_0x435689['rpcs'][_0x4723cb['UUID']];if(!_0x111d1f||!_0x111d1f['stats']||!_0x111d1f[_0x37ece7(0x5c2)][_0x37ece7(0xc3d)])return;if(_0x4723cb[_0x37ece7(0x753)])return;const _0x4333de=_0x111d1f['stats']['chunked_mode_audio'],_0x4f614f=typeof _0x4723cb['getRemoteNow']==='function'?_0x4723cb[_0x37ece7(0xa00)]():Date[_0x37ece7(0x1ab)]()-(_0x4723cb[_0x37ece7(0x229)]||0x0),_0x19e827=typeof _0x9bae49['timestamp']==='number'?_0x9bae49[_0x37ece7(0x95f)]:0x0,_0x15045e=_0x4723cb[_0x37ece7(0x2de)][_0x37ece7(0x433)]||0x0,_0x421002=_0x19e827/0x3e8-(_0x4f614f-_0x15045e),_0x17a8fd=((_0x435689['audioCtx']['baseLatency']||0x0)+(_0x435689[_0x37ece7(0x519)][_0x37ece7(0x733)]||0x0))*0x3e8;!_0x4723cb['audio'][_0x37ece7(0x528)]&&(_0x4723cb[_0x37ece7(0x2de)][_0x37ece7(0x528)]={'targetMs':0xc8,'minLead':0x18,'rejoinMargin':0x78,'rebuffering':![]});const _0x4a5e5b=_0x4723cb[_0x37ece7(0x2de)][_0x37ece7(0x528)],_0x22a2f8=_0x4a4b27(_0x37ece7(0x2de),_0x4a5e5b['targetMs']??0xc8);_0x4a5e5b['targetMs']=_0x22a2f8;if(_0x22a2f8===0x0)_0x4a5e5b['minLead']=0x0,_0x4a5e5b[_0x37ece7(0x24a)]=0x0;else{_0x4a5e5b[_0x37ece7(0xc79)]=Math[_0x37ece7(0x50b)](0x14,Math[_0x37ece7(0x452)](0x64,_0x22a2f8*0.12));let _0x40da86=Math[_0x37ece7(0x50b)](_0x4a5e5b[_0x37ece7(0xc79)],_0x22a2f8*0.25);const _0x553adb=Number['isFinite'](_0x4723cb['chunkJitterSlack'])?_0x4723cb[_0x37ece7(0x785)]:0x0;_0x553adb>0x0&&(_0x40da86+=_0x553adb),_0x4a5e5b[_0x37ece7(0x24a)]=_0x40da86;}const _0x173d1d=_0x421002+_0x22a2f8;!_0x4a5e5b[_0x37ece7(0xa62)]&&_0x22a2f8>0x0&&_0x173d1d<_0x4a5e5b[_0x37ece7(0xc79)]&&(_0x4a5e5b[_0x37ece7(0xa62)]=!![],_0x4a5e5b[_0x37ece7(0x242)]=Date['now'](),_0x4333de['last_underflow']=_0x4a5e5b['lastUnderflow']);_0x4a5e5b[_0x37ece7(0xa62)]&&(_0x22a2f8===0x0||_0x173d1d>=_0x22a2f8-_0x4a5e5b['rejoinMargin'])&&(_0x4a5e5b[_0x37ece7(0xa62)]=![]);let _0x4a9c8a;if(_0x22a2f8===0x0)_0x4a9c8a=Math[_0x37ece7(0x50b)](0x0,_0x173d1d);else _0x4a5e5b[_0x37ece7(0xa62)]?_0x4a9c8a=Math[_0x37ece7(0x50b)](_0x22a2f8,_0x4a5e5b[_0x37ece7(0xc79)]):_0x4a9c8a=Math[_0x37ece7(0x50b)](_0x4a5e5b[_0x37ece7(0xc79)],_0x173d1d);let _0x368e93=_0x4a9c8a-_0x17a8fd;(_0x368e93<0x0||!Number[_0x37ece7(0x7a3)](_0x368e93))&&(_0x368e93=0x0);_0x4333de[_0x37ece7(0xc04)]=Date[_0x37ece7(0x1ab)](),_0x4333de[_0x37ece7(0x22a)]=_0x4723cb['timedelta'],_0x4333de[_0x37ece7(0x70f)]=_0x4723cb['audio'][_0x37ece7(0x433)],_0x4333de[_0x37ece7(0x991)]=_0x19e827,_0x4333de[_0x37ece7(0x414)]=_0x368e93,_0x4333de[_0x37ece7(0x4e9)]=_0x22a2f8,_0x4333de['buffer_baseLatency']=(_0x435689[_0x37ece7(0x519)][_0x37ece7(0x133)]||0x0)*0x3e8,_0x4333de[_0x37ece7(0x523)]=(_0x435689[_0x37ece7(0x519)]['outputLatency']||0x0)*0x3e8;const _0x27fb2b=Math[_0x37ece7(0x50b)](0x0,_0x4a9c8a);_0x4333de[_0x37ece7(0x120)]=Math[_0x37ece7(0x5de)](_0x421002),_0x4333de[_0x37ece7(0x4d1)]=Math[_0x37ece7(0x5de)](_0x27fb2b),_0x4333de[_0x37ece7(0xa62)]=_0x4a5e5b[_0x37ece7(0xa62)];try{_0x4723cb[_0x37ece7(0x47a)][_0x37ece7(0xb93)]['setTargetAtTime']?_0x4723cb[_0x37ece7(0x47a)]['delayTime'][_0x37ece7(0x1aa)](_0x368e93/0x3e8,_0x435689[_0x37ece7(0x519)][_0x37ece7(0x1cc)],0.05):_0x4723cb[_0x37ece7(0x47a)][_0x37ece7(0xb93)][_0x37ece7(0x26d)](_0x368e93/0x3e8,_0x435689[_0x37ece7(0x519)][_0x37ece7(0x1cc)]);}catch(_0x5ba449){errorlog(_0x5ba449);}const _0x3fa610=Math[_0x37ece7(0x50b)](0xa,_0x368e93);_0x4723cb['audioTime']=setTimeout(function(){var _0x29b9ff=_0x37ece7;_0x4723cb[_0x29b9ff(0x753)]=null;},_0x3fa610);},'error':_0x55b9f7=>{var _0x29509f=_0x4436be;_0x4723cb['audio'][_0x29509f(0x213)][_0x29509f(0x203)]==_0x29509f(0x94c)?(errorlog(_0x55b9f7['message']),warnlog(_0x29509f(0x694))):errorlog(_0x55b9f7[_0x29509f(0x973)]);}},_0x4723cb[_0x4436be(0x2de)]['decoder']=new AudioDecoder(_0x4723cb[_0x4436be(0x79b)]),_0x4723cb[_0x4436be(0x2de)][_0x4436be(0x213)]['configure'](_0x4723cb[_0x4436be(0x5ab)]),_0x4723cb[_0x4436be(0x2de)][_0x4436be(0x36a)]=new MediaStreamTrackGenerator({'kind':'audio'}),_0x4723cb[_0x4436be(0x2de)]['frameWriter']=_0x4723cb[_0x4436be(0x2de)][_0x4436be(0x36a)][_0x4436be(0x9a8)][_0x4436be(0x862)](),_0x4723cb[_0x4436be(0x2de)][_0x4436be(0xc2e)]=new MediaStream([_0x4723cb[_0x4436be(0x2de)]['generator']]),_0x4723cb['audio']['audioNode']=_0x435689[_0x4436be(0x519)][_0x4436be(0x9cf)](_0x4723cb[_0x4436be(0x2de)]['stream']),_0x4723cb[_0x4436be(0x47a)]=_0x435689[_0x4436be(0x519)][_0x4436be(0x960)](0x1e),_0x4723cb[_0x4436be(0x47a)]['delayTime']['value']=0x0,_0x4723cb[_0x4436be(0x2de)][_0x4436be(0x4d5)][_0x4436be(0x913)](_0x4723cb[_0x4436be(0x47a)]),_0x4723cb[_0x4436be(0x1e9)]=_0x435689[_0x4436be(0x519)][_0x4436be(0xba8)](),_0x4723cb[_0x4436be(0x47a)]['connect'](_0x4723cb['destination']),_0x4723cb[_0x4436be(0x1e9)][_0x4436be(0xc2e)][_0x4436be(0x253)]()[_0x4436be(0x3d5)](_0x4e92a1=>{var _0x25eb29=_0x4436be;_0x4723cb[_0x25eb29(0x857)][_0x25eb29(0x2e6)](_0x4e92a1);})));warnlog(_0x4bcc3b),setupIncomingVideoTracking(_0x435689[_0x4436be(0xc6c)][_0x5e5f87]['videoElement'],_0x5e5f87);if(_0x4723cb[_0x4436be(0x2de)]&&_0x4723cb[_0x4436be(0x57d)])updateIncomingVideoElement(_0x5e5f87);else{if(_0x4723cb[_0x4436be(0x57d)])updateIncomingVideoElement(_0x5e5f87,!![],![]);else _0x4723cb[_0x4436be(0x2de)]&&updateIncomingVideoElement(_0x5e5f87,![],!![]);}_0x4723cb[_0x4436be(0xa92)]=async function(_0x37eae5){var _0x57e198=_0x4436be;_0x435689[_0x57e198(0x60a)]&&_0x57e198(0x95f)in _0x37eae5&&_0x435689[_0x57e198(0xc6c)][_0x5e5f87]&&pokeIframeAPI('chunked-inbound',{'UUID':_0x5e5f87,'streamID':_0x435689[_0x57e198(0xc6c)][_0x5e5f87][_0x57e198(0x885)],'type':_0x37eae5['type'],'ts':_0x37eae5[_0x57e198(0x95f)]});if(_0x37eae5[_0x57e198(0x2dd)]==_0x57e198(0x2de)){try{_0x435689[_0x57e198(0xc6c)][_0x4723cb[_0x57e198(0x9d6)]][_0x57e198(0x5c2)][_0x57e198(0xc3d)][_0x57e198(0x3cf)]=parseInt(_0x37eae5[_0x57e198(0x95f)]/0x2710)/0x64;}catch(_0x3792ea){console['error'](_0x57e198(0x970),_0x3792ea);return;}_0x4723cb[_0x57e198(0x17f)](_0x37eae5);}else{if(_0x37eae5[_0x57e198(0x2dd)]==_0x57e198(0x5c4)){var _0x59b96e=_0x435689['audioCtx'][_0x57e198(0x347)]();_0x59b96e[_0x57e198(0x913)](_0x4723cb[_0x57e198(0x1e9)]),_0x59b96e[_0x57e198(0x87e)]=function(){var _0x4f8316=_0x57e198;this[_0x4f8316(0xba2)]();};var _0x8ed7bf=_0x435689['audioCtx'][_0x57e198(0x224)](0x2,_0x37eae5['data'][_0x57e198(0xb04)],_0x435689['audioCtx'][_0x57e198(0x148)]/0x2);_0x59b96e['buffer']=_0x8ed7bf;var _0x3bae39=_0x8ed7bf[_0x57e198(0x1fd)](0x0)[_0x57e198(0x50e)](_0x37eae5[_0x57e198(0x36f)]);_0x59b96e['start'](0x0);}else _0x435689[_0x57e198(0xc6c)][_0x4723cb['UUID']][_0x57e198(0x5c2)]['chunked_mode_video']['time_seconds']=parseInt(_0x37eae5[_0x57e198(0x95f)]/0x2710)/0x64,_0x4723cb['processFrameVideo'](_0x37eae5);}},_0x4723cb[_0x4436be(0xb44)]=async function(_0x17d571){var _0x5ca809=_0x4436be;try{_0x17d571[_0x5ca809(0x2dd)]?_0x17d571=new EncodedVideoChunk(_0x17d571):errorlog(_0x5ca809(0xb8e));}catch(_0x2eabcc){errorlog(_0x2eabcc),errorlog(_0x17d571);return;}!_0x4723cb[_0x5ca809(0x9a4)]&&(_0x4723cb[_0x5ca809(0x9a4)]=function(_0x308719){var _0x31ca0d=_0x5ca809;const _0x4c52ff=[],_0x39b773={'inFlight':![],'rebuffering':![],'rebufferTimer':null,'bufferState':{'targetMs':0xc8,'minLead':0x20,'rejoinMargin':0x78,'enterThreshold':0x8}};_0x4723cb[_0x31ca0d(0x57d)][_0x31ca0d(0x135)]=null;function _0x2caf08(){var _0x59042c=_0x31ca0d;if(!_0x435689[_0x59042c(0xc6c)][_0x308719])return![];return!_0x435689['rpcs'][_0x308719][_0x59042c(0x5c2)][_0x59042c(0x5e0)]&&(_0x435689[_0x59042c(0xc6c)][_0x308719]['stats']['chunked_mode_video']={}),!![];}function _0x3d763a(){var _0x58a46c=_0x31ca0d;if(typeof _0x4723cb[_0x58a46c(0xa00)]==='function')return _0x4723cb[_0x58a46c(0xa00)]();return Date[_0x58a46c(0x1ab)]()-(_0x4723cb['timedelta']||0x0);}function _0x4de4ec(_0x33ff41){var _0x18ec5d=_0x31ca0d;const _0x3a7e72=_0x39b773[_0x18ec5d(0x528)];_0x3a7e72['targetMs']=_0x33ff41;if(_0x33ff41===0x0)_0x3a7e72[_0x18ec5d(0xc79)]=0x0,_0x3a7e72['rejoinMargin']=0x0,_0x3a7e72[_0x18ec5d(0xa74)]=0x0;else{_0x3a7e72[_0x18ec5d(0xc79)]=Math[_0x18ec5d(0x50b)](0x10,Math['min'](0xa0,_0x33ff41*0.12));let _0x417ae7=Math[_0x18ec5d(0x50b)](_0x3a7e72['minLead']*0x2,_0x33ff41*0.3);const _0x3359c2=Number[_0x18ec5d(0x7a3)](_0x4723cb[_0x18ec5d(0x785)])?_0x4723cb[_0x18ec5d(0x785)]:0x0;_0x3359c2>0x0&&(_0x417ae7+=_0x3359c2),_0x3a7e72['rejoinMargin']=_0x417ae7,_0x3a7e72[_0x18ec5d(0xa74)]=Math[_0x18ec5d(0x452)](_0x3a7e72[_0x18ec5d(0xc79)]*0.5,_0x33ff41*0.1);}return _0x3a7e72;}function _0x10e2c6(_0x24e1fe){var _0x1b79fa=_0x31ca0d;if(_0x39b773[_0x1b79fa(0x87f)])return;_0x39b773[_0x1b79fa(0x87f)]=setTimeout(function(){_0x39b773['rebufferTimer']=null,_0x5bf788();},_0x24e1fe);}function _0x17256d(_0xfcd31c){var _0x21c547=_0x31ca0d;if(!_0x4723cb[_0x21c547(0x57d)]||!_0x4723cb['video'][_0x21c547(0x213)]||_0x4723cb[_0x21c547(0x57d)][_0x21c547(0x213)][_0x21c547(0x203)]===_0x21c547(0x94c))return;try{_0x4723cb['video'][_0x21c547(0x213)][_0x21c547(0x456)](_0xfcd31c);}catch(_0x2d7fb1){errorlog(_0x2d7fb1),!_0x4723cb['requestKeyframe']&&(_0x4723cb['dc'][_0x21c547(0xac5)](JSON[_0x21c547(0xc18)]({'kf':!![]})),_0x4723cb['requestKeyframe']=setTimeout(function(){var _0x55884d=_0x21c547;clearTimeout(_0x4723cb[_0x55884d(0x5e2)]),_0x4723cb['requestKeyframe']=null;},0x3e8));}}function _0x5bf788(){var _0x5915a7=_0x31ca0d;if(_0x39b773[_0x5915a7(0xa4c)])return;if(!_0x4c52ff[_0x5915a7(0xb04)])return;if(!_0x2caf08()){_0x4c52ff[_0x5915a7(0xb04)]=0x0;return;}if(!_0x4723cb[_0x5915a7(0x57d)]||!_0x4723cb['video'][_0x5915a7(0x213)]||_0x4723cb['video'][_0x5915a7(0x213)][_0x5915a7(0x203)]===_0x5915a7(0x94c)){_0x4c52ff[_0x5915a7(0xb04)]=0x0;return;}const _0x57af2a=_0x4c52ff[0x0],_0x388070=typeof _0x57af2a['timestamp']==='number'?_0x57af2a[_0x5915a7(0x95f)]:0x0,_0x12b201=_0x3d763a(),_0x1a642e=_0x4723cb[_0x5915a7(0x57d)][_0x5915a7(0x433)]||0x0,_0x40b158=_0x4a4b27(_0x5915a7(0x57d),_0x39b773[_0x5915a7(0x528)][_0x5915a7(0xc52)]??0xc8),_0xb88a71=_0x4de4ec(_0x40b158),_0x43fe06=_0x388070/0x3e8-(_0x12b201-_0x1a642e),_0x4d5f82=_0x43fe06+_0x40b158,_0x48fc84=Math['max'](0x0,_0x4d5f82);let _0x330e41=Math['max'](0x0,_0x4d5f82);const _0xce431=_0x435689[_0x5915a7(0xc6c)][_0x308719][_0x5915a7(0x5c2)][_0x5915a7(0x5e0)],_0x4294d7=function(_0x4b8da6){var _0x4a701e=_0x5915a7;let _0x160ca4=_0x4b8da6;!(_0x160ca4>=0x0&&Number[_0x4a701e(0x7a3)](_0x160ca4))&&(_0x160ca4=0x0),_0xce431[_0x4a701e(0x120)]=Math[_0x4a701e(0x5de)](_0x43fe06),_0xce431[_0x4a701e(0x414)]=Math[_0x4a701e(0x5de)](_0x160ca4),_0xce431[_0x4a701e(0x4e9)]=Math[_0x4a701e(0x5de)](_0x40b158),_0xce431[_0x4a701e(0x4d1)]=Math[_0x4a701e(0x5de)](_0x48fc84),_0xce431[_0x4a701e(0xa62)]=!!_0x39b773[_0x4a701e(0xa62)];},_0x474ec9=_0x435689[_0x5915a7(0x519)]&&_0x435689[_0x5915a7(0x519)]['state'];if(!_0x435689[_0x5915a7(0x519)]||_0x474ec9!=='running')return _0x4294d7(0x0),_0x4c52ff['shift'](),_0x17256d(_0x57af2a),_0x5bf788();const _0x5cb241=_0xb88a71[_0x5915a7(0xa74)],_0x172029=_0x40b158>0x0?Math[_0x5915a7(0x50b)](_0xb88a71[_0x5915a7(0xc79)],_0x40b158*0.5):0x0;_0x40b158>0x0&&_0x48fc84<=_0x5cb241&&(_0x39b773[_0x5915a7(0xa62)]=!![],_0xce431[_0x5915a7(0xa72)]=Date[_0x5915a7(0x1ab)]());if(_0x39b773[_0x5915a7(0xa62)]&&_0x40b158>0x0&&_0x48fc840x0&&_0x330e41<_0xb88a71['minLead']&&(_0x330e41=_0xb88a71['minLead']);_0x4294d7(_0x330e41),_0x4c52ff[_0x5915a7(0x57c)](),_0x39b773[_0x5915a7(0xa4c)]=!![];!_0x435689['silence']&&(_0x435689[_0x5915a7(0x583)]=_0x435689[_0x5915a7(0x519)][_0x5915a7(0x5bf)](),_0x435689['silence'][_0x5915a7(0x5f4)][_0x5915a7(0x62d)]=0x0,_0x435689['silence'][_0x5915a7(0x913)](_0x435689[_0x5915a7(0x519)][_0x5915a7(0x1e9)]));const _0x374a88=_0x435689[_0x5915a7(0x519)][_0x5915a7(0x167)]();_0x374a88[_0x5915a7(0x913)](_0x435689['silence']),_0x374a88[_0x5915a7(0x8ed)](0x0),_0x374a88[_0x5915a7(0x87e)]=function(){var _0x176169=_0x5915a7;_0x374a88[_0x176169(0xba2)](),_0x17256d(_0x57af2a),_0x39b773[_0x176169(0xa4c)]=![],_0x4c52ff[_0x176169(0xb04)]&&_0x5bf788();},_0x374a88[_0x5915a7(0xaef)](_0x435689[_0x5915a7(0x519)][_0x5915a7(0x1cc)]+_0x330e41/0x3e8);}return{'enqueue'(_0x20e53b){var _0x3eeddb=_0x31ca0d;_0x4c52ff[_0x3eeddb(0x35c)](_0x20e53b),_0x4723cb[_0x3eeddb(0x57d)][_0x3eeddb(0x8f5)]!==_0x4c52ff&&(_0x4723cb[_0x3eeddb(0x57d)][_0x3eeddb(0x8f5)]=_0x4c52ff),_0x5bf788();}};});if(_0x4723cb['videoWriter']&&_0x4723cb[_0x5ca809(0x917)]['stopWriter']){if(!_0x4723cb['video']['header']&&_0x17d571[_0x5ca809(0x2dd)]!=='key')log(_0x5ca809(0xb16)),log(_0x17d571),!_0x4723cb[_0x5ca809(0x5e2)]&&(_0x4723cb['dc'][_0x5ca809(0xac5)](JSON['stringify']({'kf':!![]})),_0x4723cb[_0x5ca809(0x5e2)]=setTimeout(function(){var _0x1a5200=_0x5ca809;clearTimeout(_0x4723cb[_0x1a5200(0x5e2)]),_0x4723cb[_0x1a5200(0x5e2)]=null;},0x3e8));else!_0x4723cb['video'][_0x5ca809(0x609)]?(_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x609)]=Date[_0x5ca809(0x1ab)](),_0x4723cb['videoWriter'][_0x5ca809(0x1c2)](_0x17d571),log(_0x5ca809(0x2f7)),_0x435689[_0x5ca809(0x781)]&&!_0x4723cb[_0x5ca809(0x215)]&&(_0x4723cb['updateTime']=setInterval(function(_0x33a8ac){var _0x3e6423=_0x5ca809,_0x2f42bf=(Date[_0x3e6423(0x1ab)]()-_0x4723cb[_0x3e6423(0x57d)]['header'])/0x3e8,_0x6b876d=Math[_0x3e6423(0x8bb)](_0x2f42bf/0x3c),_0x1edf56=Math['floor'](_0x2f42bf-_0x6b876d*0x3c);try{document[_0x3e6423(0x413)](_0x3e6423(0x267)+_0x33a8ac+'\x27]')[_0x3e6423(0x5be)]=_0x3e6423(0x5e5)+_0x6b876d+_0x3e6423(0xc3a)+zpadTime(_0x1edf56)+'s';}catch(_0x3b253e){log('not\x20record\x20button\x20detected;\x20can\x27t\x20update\x20time\x20since\x20started\x20recording');}},0x3e8,_0x4723cb[_0x5ca809(0x9d6)]))):_0x4723cb[_0x5ca809(0x1d8)]['addFrame'](_0x17d571);}_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x7f1)]&&_0x4723cb[_0x5ca809(0x57d)]&&_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x213)][_0x5ca809(0x203)]==='closed'&&(_0x4723cb['video_session']+=0x1,warnlog(_0x5ca809(0x629)),_0x4723cb[_0x5ca809(0x57d)]['playbackheader']=![],_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x213)]=new VideoDecoder(_0x4723cb['init_video']),await _0x4723cb[_0x5ca809(0x57d)]['decoder'][_0x5ca809(0xc1b)](_0x4723cb[_0x5ca809(0x20b)]),_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x7f1)]=![]);if(_0x4723cb[_0x5ca809(0x57d)]['playbackheader']||_0x17d571[_0x5ca809(0x2dd)]===_0x5ca809(0x304)){_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x7f1)]=!![];if(_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x433)]){try{!_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0xc20)]&&(_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0xc20)]=_0x4723cb[_0x5ca809(0x9a4)](_0x5e5f87)),_0x4723cb['video'][_0x5ca809(0xc20)][_0x5ca809(0x1e5)](_0x17d571);}catch(_0x34f126){errorlog(_0x34f126),_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0xc20)]=null;}return;}try{if(_0x4723cb['video'][_0x5ca809(0x135)])_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x8f5)][_0x5ca809(0x35c)](_0x17d571);else{if(_0x4723cb['video']['queue'][_0x5ca809(0xb04)])_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x8f5)][_0x5ca809(0x35c)](_0x17d571);else{if(_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x433)]){_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x135)]=!![];function _0x350faa(_0x3d75e4){var _0x4dd4ea=_0x5ca809,_0xe8e2d3=_0x4723cb[_0x4dd4ea(0xc01)],_0x3229e6=_0x3d75e4['timestamp']/0x3e8-(Date['now']()-_0x4723cb[_0x4dd4ea(0x229)]-_0x4723cb['video']['realTime']),_0x354332=0xc8;if(!_0x435689[_0x4dd4ea(0xc6c)][_0x4723cb[_0x4dd4ea(0x9d6)]]){clearTimeout(_0x4723cb[_0x4dd4ea(0x57d)][_0x4dd4ea(0x135)]),_0x4723cb[_0x4dd4ea(0x57d)][_0x4dd4ea(0x135)]=null,_0x4723cb[_0x4dd4ea(0x57d)][_0x4dd4ea(0x8f5)]=[];return;}else{if(_0x435689[_0x4dd4ea(0xc6c)][_0x4723cb['UUID']]['buffer']!==![])_0x354332=_0x435689[_0x4dd4ea(0xc6c)][_0x4723cb[_0x4dd4ea(0x9d6)]]['buffer'];else _0x435689[_0x4dd4ea(0x237)]!==![]?_0x354332=_0x435689[_0x4dd4ea(0x237)]:_0x435689[_0x4dd4ea(0xc6c)][_0x4723cb['UUID']][_0x4dd4ea(0x237)]=_0x354332;}_0x3229e6+=_0x354332,!_0x435689[_0x4dd4ea(0xc6c)][_0x4723cb[_0x4dd4ea(0x9d6)]][_0x4dd4ea(0x5c2)][_0x4dd4ea(0x5e0)]&&(_0x435689[_0x4dd4ea(0xc6c)][_0x4723cb[_0x4dd4ea(0x9d6)]][_0x4dd4ea(0x5c2)]['chunked_mode_video']={}),_0x435689['rpcs'][_0x4723cb[_0x4dd4ea(0x9d6)]]['stats'][_0x4dd4ea(0x5e0)][_0x4dd4ea(0x414)]=parseInt(_0x3229e6),_0x435689[_0x4dd4ea(0xc6c)][_0x4723cb[_0x4dd4ea(0x9d6)]]['stats'][_0x4dd4ea(0x5e0)]['buffer_buffer']=parseInt(_0x354332),_0x435689[_0x4dd4ea(0xc6c)][_0x4723cb['UUID']][_0x4dd4ea(0x5c2)]['chunked_mode_video'][_0x4dd4ea(0xc59)]=_0x3d75e4['timestamp']+':'+(Date[_0x4dd4ea(0x1ab)]()-_0x4723cb[_0x4dd4ea(0x229)]-_0x4723cb[_0x4dd4ea(0x57d)][_0x4dd4ea(0x433)])+':'+Date['now']()+':'+_0x4723cb['timedelta']+':'+_0x4723cb['video'][_0x4dd4ea(0x433)],!_0x435689[_0x4dd4ea(0x583)]&&(_0x435689[_0x4dd4ea(0x583)]=_0x435689[_0x4dd4ea(0x519)][_0x4dd4ea(0x5bf)](),_0x435689[_0x4dd4ea(0x583)][_0x4dd4ea(0x5f4)]['value']=0x0,_0x435689['silence'][_0x4dd4ea(0x913)](_0x435689[_0x4dd4ea(0x519)][_0x4dd4ea(0x1e9)])),!_0x4723cb[_0x4dd4ea(0x9dc)]&&(_0x3229e6<=0x0&&(_0x3229e6=0x0),_0x4723cb[_0x4dd4ea(0x9dc)]=_0x435689[_0x4dd4ea(0x519)]['createOscillator'](),_0x4723cb[_0x4dd4ea(0x9dc)][_0x4dd4ea(0x913)](_0x435689[_0x4dd4ea(0x583)]),_0x4723cb[_0x4dd4ea(0x9dc)][_0x4dd4ea(0x8ed)](0x0),_0x4723cb[_0x4dd4ea(0x9dc)][_0x4dd4ea(0x87e)]=_0x2f90c3=>{var _0x317e58=_0x4dd4ea;_0x4723cb[_0x317e58(0x9dc)]['disconnect']();if(_0xe8e2d3===_0x4723cb[_0x317e58(0xc01)])try{_0x4723cb[_0x317e58(0x57d)][_0x317e58(0x213)]['decode'](_0x3d75e4);}catch(_0x430609){errorlog(_0x430609);}else console[_0x317e58(0x2c6)](_0xe8e2d3,_0x4723cb[_0x317e58(0xc01)]);_0x4723cb[_0x317e58(0x9dc)]=![],_0x4723cb['video'][_0x317e58(0x8f5)][_0x317e58(0xb04)]?_0x350faa(_0x4723cb[_0x317e58(0x57d)][_0x317e58(0x8f5)]['shift']()):_0x4723cb[_0x317e58(0x57d)][_0x317e58(0x135)]=null;},_0x4723cb[_0x4dd4ea(0x9dc)][_0x4dd4ea(0xaef)](_0x435689[_0x4dd4ea(0x519)][_0x4dd4ea(0x1cc)]+_0x3229e6/0x3e8));}try{_0x350faa(_0x17d571);}catch(_0x5adc72){errorlog(_0x5adc72),_0x4723cb[_0x5ca809(0x57d)]['nextQueue']=null,!_0x4723cb[_0x5ca809(0x5e2)]&&(_0x4723cb['dc'][_0x5ca809(0xac5)](JSON[_0x5ca809(0xc18)]({'kf':!![]})),_0x4723cb[_0x5ca809(0x5e2)]=setTimeout(function(){var _0x4ee48d=_0x5ca809;clearTimeout(_0x4723cb['requestKeyframe']),_0x4723cb[_0x4ee48d(0x5e2)]=null;},0x3e8));}}else try{_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x213)][_0x5ca809(0x456)](_0x17d571);}catch(_0xb39c1b){errorlog(_0xb39c1b);}}}}catch(_0x17e8d5){errorlog(_0x17e8d5),_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x7f1)]=![];}}_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x213)][_0x5ca809(0x5d7)]&&console[_0x5ca809(0x2c6)](_0x5ca809(0x194)+_0x4723cb[_0x5ca809(0x57d)][_0x5ca809(0x213)][_0x5ca809(0x5d7)]),!_0x4723cb['video'][_0x5ca809(0x7f1)]&&(!_0x4723cb[_0x5ca809(0x5e2)]&&(_0x4723cb['dc']['send'](JSON[_0x5ca809(0xc18)]({'kf':!![]})),_0x4723cb[_0x5ca809(0x5e2)]=setTimeout(function(){var _0x25b92b=_0x5ca809;clearTimeout(_0x4723cb[_0x25b92b(0x5e2)]),_0x4723cb[_0x25b92b(0x5e2)]=null;},0x3e8)));},_0x4723cb['processFrameAudio']=async function(_0x54b508){var _0x28a7c2=_0x4436be;if(!_0x4723cb['audio']){errorlog(_0x28a7c2(0x86d));return;}try{_0x54b508[_0x28a7c2(0x2dd)]=_0x28a7c2(0x304),_0x54b508=new EncodedAudioChunk(_0x54b508);}catch(_0x42ad55){return;}_0x4723cb['videoWriter']&&_0x4723cb[_0x28a7c2(0x57d)]['header']&&_0x4723cb['videoElement'][_0x28a7c2(0x77e)]&&_0x4723cb[_0x28a7c2(0x1d8)][_0x28a7c2(0x1c2)](_0x54b508);_0x4723cb[_0x28a7c2(0x2de)][_0x28a7c2(0x213)][_0x28a7c2(0x203)]===_0x28a7c2(0x94c)&&(_0x4723cb[_0x28a7c2(0x2de)][_0x28a7c2(0x213)]=new AudioDecoder(_0x4723cb[_0x28a7c2(0x79b)]),_0x4723cb[_0x28a7c2(0x2de)][_0x28a7c2(0x213)][_0x28a7c2(0xc1b)](_0x4723cb['stream_configAudio']));try{_0x4723cb[_0x28a7c2(0x2de)][_0x28a7c2(0x213)][_0x28a7c2(0x456)](_0x54b508);}catch(_0x2fd03f){errorlog(_0x2fd03f);}};}else{if(_0x4723cb[_0x4436be(0x2de)]&&_0x4bcc3b[_0x4436be(0x6d3)])_0x4723cb[_0x4436be(0x2de)]['realTime']=_0x4bcc3b[_0x4436be(0x6d3)];else _0x4723cb[_0x4436be(0x57d)]&&_0x4bcc3b[_0x4436be(0x18a)]?_0x4723cb[_0x4436be(0x57d)]['realTime']=_0x4bcc3b['realTimeVideo']:errorlog(_0x209c59);}return;}catch(_0x200091){errorlog(_0x200091);}else _0x435689['retransmit']&&(_0x435689[_0x4436be(0xb35)][_0x4436be(0x35c)](_0x25ad7c['data']),_0x435689['retransmit']&&_0x435689[_0x4436be(0x181)]());try{const _0x5a5ba2=_0x25ad7c['data'];if(typeof _0x5a5ba2==='string')_0xe073aa(JSON[_0x4436be(0x48a)](_0x5a5ba2));else _0x5a5ba2&&await _0x48a48e(_0x5a5ba2);}catch(_0x2baca5){errorlog(_0x2baca5);}};return;},_0x435689[_0x120577(0xaba)]=function(){var _0x55269=_0x120577;const _0x309cdb=new Set([...Object[_0x55269(0x161)](_0x435689[_0x55269(0x76f)]),...Object[_0x55269(0x161)](_0x435689['rpcs'])]),_0x4819ed=_0x435689[_0x55269(0xc19)][_0x55269(0xb04)];_0x435689[_0x55269(0xc19)]=_0x435689[_0x55269(0xc19)][_0x55269(0x7dd)](_0x2b8fa0=>_0x309cdb[_0x55269(0x1a5)](_0x2b8fa0));const _0x225407=_0x4819ed-_0x435689[_0x55269(0xc19)][_0x55269(0xb04)];_0x225407&&log(_0x55269(0x406)+_0x225407+_0x55269(0x759));},_0x435689[_0x120577(0x93a)]=async function(_0x2284c5){var _0x5dc4d4=_0x120577;log('SETUP\x20INCOMING');var _0x112b4a=_0x2284c5['UUID'];if(_0x112b4a in _0x435689['rpcs']){if('session'in _0x2284c5&&_0x2284c5[_0x5dc4d4(0x767)]){if(_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['session']==_0x2284c5[_0x5dc4d4(0x767)]){log(_0x5dc4d4(0x772));return;}warnlog('already\x20connected\x201'),_0x435689[_0x5dc4d4(0x2a0)](_0x112b4a,![],!![])||![];}}else log('MAKING\x20A\x20NEW\x20RPCS\x20RTC\x20CONNECTION');try{for(var _0x41ae35 in _0x435689[_0x5dc4d4(0xc6c)]){_0x435689['rpcs'][_0x41ae35]['streamID']==_0x2284c5[_0x5dc4d4(0x885)]&&(_0x435689['rpcs'][_0x41ae35][_0x5dc4d4(0x4fb)]&&errorlog(_0x5dc4d4(0x3c6)),_0x435689['rpcs'][_0x41ae35][_0x5dc4d4(0x917)]&&(_0x435689['rpcs'][_0x41ae35][_0x5dc4d4(0x917)]['style'][_0x5dc4d4(0x386)]=_0x5dc4d4(0x15c)),warnlog(_0x5dc4d4(0x541)),_0x435689[_0x5dc4d4(0x2a0)](_0x41ae35),_0x41ae35!==_0x112b4a&&(_0x41ae35 in _0x435689['pcs']&&(_0x2284c5[_0x5dc4d4(0x767)]&&_0x2284c5[_0x5dc4d4(0x767)]['substring'](0x0,0x6)!==_0x435689['loadoutID']?(warnlog(_0x5dc4d4(0x5f5)),log('closing\x2020'),_0x435689['closePC'](_0x41ae35,![])):warnlog(_0x5dc4d4(0x593)))));}document[_0x5dc4d4(0x54c)]('mainmenu')&&(document[_0x5dc4d4(0x54c)](_0x5dc4d4(0x3d0))[_0x5dc4d4(0x631)][_0x5dc4d4(0x5fc)](document['getElementById'](_0x5dc4d4(0x3d0))),document[_0x5dc4d4(0x689)]('.hidden2')[_0x5dc4d4(0x3d5)](_0x144526=>{var _0x5ca347=_0x5dc4d4;_0x144526[_0x5ca347(0xab1)][_0x5ca347(0xae4)]('hidden2');}));}catch(_0xce0274){errorlog(_0xce0274);}if(_0x435689[_0x5dc4d4(0x1fc)]){log(_0x5dc4d4(0x239)+_0x112b4a);return;}if(_0x435689[_0x5dc4d4(0x522)]!==![]){if(Object[_0x5dc4d4(0x161)](_0x435689[_0x5dc4d4(0xc6c)])[_0x5dc4d4(0xb04)]>=_0x435689[_0x5dc4d4(0x522)]){warnlog(_0x5dc4d4(0x56e));return;}}else{if(_0x435689[_0x5dc4d4(0x7c1)]!==![]){if(Object[_0x5dc4d4(0x161)](_0x435689[_0x5dc4d4(0xc6c)])[_0x5dc4d4(0xb04)]+Object[_0x5dc4d4(0x161)](_0x435689[_0x5dc4d4(0x76f)])[_0x5dc4d4(0xb04)]>=_0x435689['maxconnections']){warnlog(_0x5dc4d4(0x56e));return;}}}if(_0x435689[_0x5dc4d4(0x8f5)]){if(_0x435689[_0x5dc4d4(0x781)])!(_0x112b4a in _0x435689[_0x5dc4d4(0x76f)])&&_0x435689[_0x5dc4d4(0x625)](_0x112b4a);else{if(_0x435689[_0x5dc4d4(0xc19)][_0x5dc4d4(0x97c)](_0x112b4a)==-0x1){if(!(_0x2284c5[_0x5dc4d4(0x885)]&&_0x435689['view_set']&&_0x435689['view_set'][_0x5dc4d4(0x49c)](_0x2284c5['streamID'])))return;}}}!_0x435689[_0x5dc4d4(0x238)]&&await chooseBestTURN();_0x435689[_0x5dc4d4(0x8e7)]&&(_0x435689[_0x5dc4d4(0x238)][_0x5dc4d4(0x8e7)]=!![]);_0x435689[_0x5dc4d4(0x570)]&&(_0x435689[_0x5dc4d4(0x238)][_0x5dc4d4(0x570)]=_0x435689[_0x5dc4d4(0x570)]);try{if(_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]&&_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x614)]){let _0x5cbe71=new RTCPeerConnection(_0x435689[_0x5dc4d4(0x238)]);try{_0x5cbe71[_0x5dc4d4(0x626)]=_0x567375=>{warnlog(_0x567375);};}catch(_0x23f64d){warnlog(_0x23f64d);}var _0x364633=Object[_0x5dc4d4(0x161)](_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]);for(var _0x41ae35=0x0;_0x41ae35<_0x364633[_0x5dc4d4(0xb04)];_0x41ae35++){var _0x1b433e=_0x364633[_0x41ae35];if(_0x5cbe71['hasOwnProperty'](_0x1b433e))continue;_0x5cbe71[_0x1b433e]=_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x1b433e],log(_0x5dc4d4(0x2e1)+_0x1b433e);}_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]=_0x5cbe71;}else{_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]=new RTCPeerConnection(_0x435689[_0x5dc4d4(0x238)]);try{_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x626)]=_0x2eea4d=>{warnlog(_0x2eea4d);};}catch(_0x5503fe){warnlog(_0x5503fe);}}if(_0x435689[_0x5dc4d4(0x87b)]&&!_0x2284c5[_0x5dc4d4(0x153)]){errorlog('Encryption\x20is\x20required,\x20but\x20none\x20found.\x20Cancelling.'),errorlog(_0x2284c5);return;}else{if(!_0x2284c5[_0x5dc4d4(0x153)]&&!_0x435689[_0x5dc4d4(0x749)]&&_0x435689[_0x5dc4d4(0x28c)]&&!_0x435689[_0x5dc4d4(0x605)]){errorlog(_0x5dc4d4(0x7db)),errorlog(_0x2284c5);return;}}}catch(_0x20b485){!_0x435689[_0x5dc4d4(0x326)]&&warnUser(_0x5dc4d4(0xa22));errorlog(_0x20b485);return;}!_0x2284c5[_0x5dc4d4(0x153)]?(_0x435689['password']&&_0x435689['defaultPassword']&&(warnlog(_0x5dc4d4(0x975)),warnlog(_0x2284c5)),_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x153)]=![]):(!_0x435689[_0x5dc4d4(0x28c)]&&(errorlog(_0x5dc4d4(0x865)),errorlog(_0x2284c5)),_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['vector']=!![]);if(_0x435689[_0x5dc4d4(0x138)]){if(Object[_0x5dc4d4(0x161)](_0x435689[_0x5dc4d4(0xc6c)])['length']>0x1){warnlog('TOO\x20MANY\x20PUBLISHING\x20PEERS'),log(_0x435689[_0x5dc4d4(0xc6c)]),delete _0x435689[_0x5dc4d4(0xc6c)][_0x112b4a],updateUserList();return;}else warnlog(_0x5dc4d4(0x86b));}_0x2284c5['streamID']in _0x435689[_0x5dc4d4(0x301)]&&(log('deleting\x20watch\x20list'),delete _0x435689[_0x5dc4d4(0x301)][_0x2284c5['streamID']]);try{_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x885)]=_0x2284c5[_0x5dc4d4(0x885)];const _0x46aeb2=_0x112b4a+_0x5dc4d4(0xac8);if(_0x46aeb2 in _0x435689[_0x5dc4d4(0xc6c)]){const _0xd3bdfe=_0x435689[_0x5dc4d4(0xc6c)][_0x46aeb2],_0x2e3933=_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x885)];if(_0x2e3933){const _0x138d2c=_0x2e3933+':s';_0xd3bdfe[_0x5dc4d4(0x885)]!==_0x138d2c&&(_0xd3bdfe[_0x5dc4d4(0x885)]=_0x138d2c),_0xd3bdfe['videoElement']&&(_0xd3bdfe[_0x5dc4d4(0x917)][_0x5dc4d4(0xc6b)]['sid']=_0x138d2c),_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x771)]&&_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x771)]!==_0xd3bdfe['videoElement']&&(_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x771)]['dataset'][_0x5dc4d4(0x952)]=_0x138d2c);}}await checkDirectorStreamID();}catch(_0x44d591){errorlog(_0x44d591);return;}_0x2284c5[_0x5dc4d4(0x767)]?_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x767)]=_0x2284c5[_0x5dc4d4(0x767)]:_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['session']=null;_0x435689['rpcs'][_0x112b4a]['getStatsTimeout']=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x38e)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x417)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x180)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['allowMIDI']=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x6a0)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x9ee)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x5c2)]={},_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['stats'][_0x5dc4d4(0x633)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0xb33)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0xbec)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xb37)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x263)]=-0x1,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xaac)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x237)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x4ff)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x6e1)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x832)]=-0x1,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x358)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['videoElement']=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x7ea)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x566)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x1cd)]=[],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xc02)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x92f)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['lockedVideoBitrate']=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x6cf)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x8d2)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x904)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x8b2)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['closeTimeout']=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x3d2)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x56a)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xbea)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x187)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x2f9)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['flipState']=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x33f)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xc40)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xa70)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x4bd)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x9f5)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x289)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x33d)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0xa02)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x857)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['screenIndexes']=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x4b9)]=![],_0x435689['rpcs'][_0x112b4a]['smallScreen']=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x15d)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x781)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x5b4)]=![],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x65b)]=0x64,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x536)]=0x0,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xa6a)]=0x0,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x409)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x340)]='1',_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0xafa)]='1',_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x882)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x29b)]=0x0,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xc69)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['meta']=![],_0x435689['rpcs'][_0x112b4a]['order']=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xb12)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xa8d)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x674)]={},_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xafd)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x964)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x6ae)]=Date[_0x5dc4d4(0x1ab)](),_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0xa47)]=![],_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x5ce)]=_0x435689[_0x5dc4d4(0x5ce)];(_0x435689[_0x5dc4d4(0x671)]==0x2||_0x435689['activeSpeaker']==0x4)&&(_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x180)]=!![]);if(_0x435689[_0x5dc4d4(0x439)]){var _0x538065=createRichVideoElement(_0x112b4a);_0x538065[_0x5dc4d4(0x8d0)]['display']=_0x5dc4d4(0x59f);}if(_0x435689[_0x5dc4d4(0x781)]){if(_0x435689[_0x5dc4d4(0x431)]&&_0x5dc4d4(0xc37)in _0x2284c5&&_0x2284c5['isScene']!==![]){}else{var _0x3aa0f8=soloLinkGenerator(_0x435689['rpcs'][_0x112b4a]['streamID']);'slot'in _0x2284c5?createControlBox(_0x112b4a,_0x3aa0f8,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x885)],_0x2284c5[_0x5dc4d4(0x645)]):createControlBox(_0x112b4a,_0x3aa0f8,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x885)]);_0x435689[_0x5dc4d4(0x34b)]&&_0x435689[_0x5dc4d4(0x34b)][_0x5dc4d4(0xb04)]&&setTimeout(function(){autoAssignAudioChannel(_0x112b4a);},0x64);if(_0x435689[_0x5dc4d4(0xb9e)]&&_0x435689[_0x5dc4d4(0x2db)]['includes'](_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x885)])){var _0x1f412d=_0x435689[_0x5dc4d4(0x2db)][_0x5dc4d4(0x97c)](_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x885)]);_0x1f412d>-0x1&&_0x435689['pendingApprovalStreamIDs'][_0x5dc4d4(0x20c)](_0x1f412d,0x1),setTimeout(function(){var _0x15e147=_0x5dc4d4;_0x69d88c(_0x112b4a),_0x435689[_0x15e147(0x20e)](_0x112b4a);},0x64);}}}_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['UUID']=_0x112b4a;try{if(_0x435689[_0x5dc4d4(0x822)]){if(_0x435689[_0x5dc4d4(0x822)][_0x5dc4d4(0x49c)](_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x885)])){if(_0x435689[_0x5dc4d4(0x1b7)]!==![]){let _0xd0fe81=_0x435689[_0x5dc4d4(0x822)][_0x5dc4d4(0x97c)](_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0x885)]);_0x435689[_0x5dc4d4(0x1b7)]['length']>_0xd0fe81&&(_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x358)]=parseInt(_0x435689[_0x5dc4d4(0x1b7)][_0xd0fe81]),_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x358)]<=0x0&&(_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x358)]=![]));}}}}catch(_0x5187ee){errorlog(_0x5187ee);}_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['onclose']=function(_0x1b523d){var _0x309882=_0x5dc4d4;log(_0x309882(0x8a2)),_0x435689['closeRPC'](_0x112b4a);},_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xb68)]=null,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['iceBundle']=[],_0x435689['rpcs'][_0x112b4a][_0x5dc4d4(0xac4)]=0xa,_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a]['onicecandidate']=function(_0x3f1ac0){var _0x393c1e=_0x5dc4d4;if(_0x3f1ac0[_0x393c1e(0x2c7)]==null){log('null\x20ice\x20rpcs');if(_0x435689[_0x393c1e(0xc6c)][_0x112b4a]&&_0x435689['rpcs'][_0x112b4a]['whipCallback2']){var _0x4834e6=[..._0x435689[_0x393c1e(0xc6c)][_0x112b4a][_0x393c1e(0xb5b)]];try{if(_0x435689['disableIpv6']){var _0x5749e6=filterIpv6FromCandidates(_0x4834e6);_0x4834e6=_0x5749e6[_0x393c1e(0x661)];}else _0x435689['preferIpv4']!==![]&&(_0x4834e6=reorderCandidatesIpv4First(_0x4834e6));}catch(_0x59a986){warnlog(_0x393c1e(0xb28),_0x59a986);}_0x435689[_0x393c1e(0xc6c)][_0x112b4a]['whipCallback2'](_0x4834e6),clearTimeout(_0x435689[_0x393c1e(0xc6c)][_0x112b4a][_0x393c1e(0xb68)]),_0x435689[_0x393c1e(0xc6c)][_0x112b4a]['iceTimer']=null,_0x435689[_0x393c1e(0xc6c)][_0x112b4a][_0x393c1e(0xb5b)]=[],_0x435689[_0x393c1e(0xc6c)][_0x112b4a][_0x393c1e(0xab5)]=null,console['log']('candidate\x20callback\x20finished\x20in\x20totalilty');}return;}try{if(_0x435689['icefilter']){if(_0x3f1ac0['candidate'][_0x393c1e(0x2c7)]['indexOf'](_0x435689[_0x393c1e(0xc00)])===-0x1){log('dropped\x20candidate\x20due\x20to\x20filter');return;}else log(_0x3f1ac0[_0x393c1e(0x2c7)]);}}catch(_0xf3345){errorlog(_0xf3345);}try{if(_0x435689[_0x393c1e(0xb8f)]){if(!filterIceLAN(_0x3f1ac0[_0x393c1e(0x2c7)]))return;}if(_0x435689['stunOnly']){if(!filterStunOnly(_0x3f1ac0[_0x393c1e(0x2c7)]))return;}}catch(_0x405045){errorlog(_0x405045);}_0x435689[_0x393c1e(0xc6c)][_0x112b4a][_0x393c1e(0xb5b)][_0x393c1e(0x35c)](_0x3f1ac0[_0x393c1e(0x2c7)]);if(_0x435689[_0x393c1e(0xc6c)][_0x112b4a]&&(_0x435689['rpcs'][_0x112b4a][_0x393c1e(0xab5)]||_0x435689[_0x393c1e(0xc6c)][_0x112b4a][_0x393c1e(0xb68)]!==null))return;_0x435689['rpcs'][_0x112b4a][_0x393c1e(0xb68)]=setTimeout(function(_0x22e039){var _0xea3a61=_0x393c1e;if(!(_0x22e039 in _0x435689[_0xea3a61(0xc6c)]))return;if(_0x435689[_0xea3a61(0xc6c)][_0x22e039][_0xea3a61(0xab5)])return;_0x435689['rpcs'][_0x22e039][_0xea3a61(0xb68)]=null;if(!_0x435689['rpcs'][_0x22e039][_0xea3a61(0xb5b)]||!_0x435689[_0xea3a61(0xc6c)][_0x22e039][_0xea3a61(0xb5b)][_0xea3a61(0xb04)]){errorlog('shouldn\x27t\x20happen');return;}var _0x1c2a7a={};_0x1c2a7a['UUID']=_0x22e039,_0x1c2a7a['type']=_0xea3a61(0xba7);var _0xab3b2d=_0x435689[_0xea3a61(0xc6c)][_0x22e039][_0xea3a61(0xb5b)];try{if(_0x435689[_0xea3a61(0x392)]){var _0x239e73=filterIpv6FromCandidates(_0xab3b2d);_0xab3b2d=_0x239e73[_0xea3a61(0x661)];}else _0x435689['preferIpv4']!==![]&&(_0xab3b2d=reorderCandidatesIpv4First(_0xab3b2d));}catch(_0x5979d8){warnlog(_0xea3a61(0x77f),_0x5979d8);}_0x1c2a7a[_0xea3a61(0x82b)]=_0xab3b2d,_0x1c2a7a[_0xea3a61(0x767)]=_0x435689[_0xea3a61(0xc6c)][_0x22e039][_0xea3a61(0x767)],_0x435689['rpcs'][_0x22e039][_0xea3a61(0xb5b)]=[],_0x435689[_0xea3a61(0xc6c)][_0x22e039][_0xea3a61(0xac4)]=0x3e8;if(_0x435689[_0xea3a61(0xc6c)][_0x22e039][_0xea3a61(0x4fb)])return;_0x435689[_0xea3a61(0x28c)]&&_0x435689[_0xea3a61(0xc6c)][_0x22e039]['vector']?_0x435689[_0xea3a61(0x504)](JSON[_0xea3a61(0xc18)](_0x1c2a7a[_0xea3a61(0x82b)]))['then'](function(_0xf96be){var _0x28adc6=_0xea3a61;_0x1c2a7a['candidates']=_0xf96be[0x0],_0x1c2a7a['vector']=_0xf96be[0x1],_0x435689[_0x28adc6(0x9cb)](_0x1c2a7a);})[_0xea3a61(0x501)](errorlog):_0x435689['anyrequest'](_0x1c2a7a);},_0x435689[_0x393c1e(0xc6c)][_0x112b4a]['delayIceSend'],_0x112b4a);},_0x435689['rpcs'][_0x112b4a]['onconnectionstatechange']=function(_0x78fe6f){var _0x293511=_0x5dc4d4;switch(this[_0x293511(0x8e8)]){case'new':log(_0x293511(0x533)),log(_0x293511(0xbd2)),clearInterval(_0x435689[_0x293511(0xc6c)][this[_0x293511(0x9d6)]]['closeTimeout']);case _0x293511(0x758):log(_0x293511(0x758)),log(_0x293511(0x142)),clearInterval(_0x435689['rpcs'][this[_0x293511(0x9d6)]][_0x293511(0x98f)]);case'connected':log('**\x20connected'),log(_0x293511(0xa11)),clearInterval(_0x435689[_0x293511(0xc6c)][this[_0x293511(0x9d6)]][_0x293511(0x98f)]);if(_0x435689[_0x293511(0x138)]){if(_0x435689['ws'][_0x293511(0x565)]!==0x1){_0x435689['ws']['close']();break;}_0x435689['ws'][_0x293511(0x757)](),setTimeout(function(){var _0x483718=_0x293511;_0x435689[_0x483718(0x326)]!=!![]&&warnUser(getTranslation(_0x483718(0xbbe)));},0x1);}break;case'disconnected':log(_0x293511(0xa61)),warnlog(_0x293511(0x306));if(this['UUID']in _0x435689['rpcs']){clearInterval(_0x435689[_0x293511(0xc6c)][this[_0x293511(0x9d6)]][_0x293511(0x98f)]),_0x435689[_0x293511(0xc6c)][this[_0x293511(0x9d6)]][_0x293511(0xac4)]=0x0;if(_0x435689['rpcs'][this['UUID']][_0x293511(0xa47)])return;try{const _0x1852ab=Date[_0x293511(0x1ab)](),_0x13bcba=this[_0x293511(0x9d6)];_0x435689[_0x293511(0xc6c)][_0x13bcba]&&(_0x435689['rpcs'][_0x13bcba][_0x293511(0x94e)]=undefined,_0x435689['rpcs'][_0x13bcba][_0x293511(0x89a)]=_0x1852ab);try{_0x435689[_0x293511(0x43f)]({'ping':_0x1852ab},_0x13bcba);}catch(_0x11b215){warnlog(_0x11b215);}setTimeout(function(_0x144baf,_0x50fb77){var _0x2779f4=_0x293511;try{if(!(_0x144baf in _0x435689[_0x2779f4(0xc6c)]))return;if(_0x435689[_0x2779f4(0xc6c)][_0x144baf][_0x2779f4(0x8e8)]!=='disconnected')return;if(_0x435689[_0x2779f4(0xc6c)][_0x144baf]['lastPongToken']!==_0x50fb77)try{_0x435689[_0x2779f4(0x9cb)]({'iceRestartRequest':!![],'UUID':_0x144baf});}catch(_0x4146d1){warnlog(_0x4146d1),errorlog(_0x4146d1);}}catch(_0x2a449d){errorlog(_0x2a449d);}},0xbb8,_0x13bcba,_0x1852ab);}catch(_0x5ba486){errorlog(_0x5ba486);}_0x435689[_0x293511(0xc6c)][this[_0x293511(0x9d6)]][_0x293511(0x98f)]=setTimeout(function(_0x2c6234){var _0x5c5bcd=_0x293511;log('disconnected;\x20no\x20reconnect\x20even\x20after\x205s;\x20closing'),_0x435689[_0x5c5bcd(0x2a0)](_0x2c6234);},0x1388,this[_0x293511(0x9d6)]);}else log(_0x293511(0x841));break;case _0x293511(0x35e):warnlog(_0x293511(0xb76));if(this[_0x293511(0x9d6)]in _0x435689[_0x293511(0xc6c)]){clearInterval(_0x435689[_0x293511(0xc6c)][this[_0x293511(0x9d6)]][_0x293511(0x98f)]),_0x435689[_0x293511(0xc6c)][this['UUID']][_0x293511(0xac4)]=0x0;try{var _0x20200f={'iceRestartRequest':!![],'UUID':this[_0x293511(0x9d6)]};_0x435689[_0x293511(0x9cb)](_0x20200f),log(_0x293511(0x736));}catch(_0x2e3ce5){errorlog(_0x2e3ce5);}_0x435689['rpcs'][this[_0x293511(0x9d6)]][_0x293511(0x98f)]=setTimeout(function(_0x335b52){log('RPC\x20connection\x20failed\x20-\x20closing\x20after\x20timeout'),_0x435689['closeRPC'](_0x335b52);},0x7530,this[_0x293511(0x9d6)]);}else log(_0x293511(0x841));break;case _0x293511(0x94c):warnlog(_0x293511(0x5ca)),_0x435689[_0x293511(0x2a0)](this[_0x293511(0x9d6)]);break;default:log('closeTimeout\x20cancelled;\x207'),log('this.connectionState:\x20'+this[_0x293511(0x8e8)]),clearInterval(_0x435689['rpcs'][this[_0x293511(0x9d6)]][_0x293511(0x98f)]);break;}},_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xade)]=function(_0x5c20af){var _0x480178=_0x5dc4d4;let _0x4943ef=_0x5c20af['target'];switch(_0x4943ef[_0x480178(0xae3)]){case _0x480178(0x5cc):log(_0x480178(0x387));break;case _0x480178(0x539):log(_0x480178(0x7a6));if(_0x435689[_0x480178(0xc6c)][_0x112b4a][_0x480178(0xab5)]){var _0x116c4a=[..._0x435689[_0x480178(0xc6c)][_0x112b4a]['iceBundle']];try{if(_0x435689['disableIpv6']){var _0x5852a8=filterIpv6FromCandidates(_0x116c4a);_0x116c4a=_0x5852a8[_0x480178(0x661)];}else _0x435689['preferIpv4']!==![]&&(_0x116c4a=reorderCandidatesIpv4First(_0x116c4a));}catch(_0x37b79a){warnlog(_0x480178(0x525),_0x37b79a);}_0x435689[_0x480178(0xc6c)][_0x112b4a][_0x480178(0xab5)](_0x116c4a),clearTimeout(_0x435689[_0x480178(0xc6c)][_0x112b4a][_0x480178(0xb68)]),_0x435689[_0x480178(0xc6c)][_0x112b4a][_0x480178(0xb68)]=null,_0x435689[_0x480178(0xc6c)][_0x112b4a]['iceBundle']=[],_0x435689[_0x480178(0xc6c)][_0x112b4a][_0x480178(0xab5)]=null;}break;}},_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x1f1)]=function(){var _0x1ba31e=_0x5dc4d4;try{if(this['iceConnectionState']==_0x1ba31e(0x94c))errorlog('CLOSED');else{if(this['iceConnectionState']==_0x1ba31e(0x2b8)){if(_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0xa47)])return;warnlog(_0x1ba31e(0x88e)),_0x435689['rpcs'][_0x112b4a][_0x1ba31e(0x340)]='0',_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0x917)][_0x1ba31e(0x8d0)][_0x1ba31e(0x2d6)]='0',_0x435689['rpcs'][_0x112b4a]['disconnectedTimeout']=setTimeout(function(_0x2156c0){updateMixer();},0x1f4,_0x112b4a);}else this[_0x1ba31e(0x9bb)]==_0x1ba31e(0x35e)?errorlog(_0x1ba31e(0xbee)):(log(_0x1ba31e(0x8b7)+this[_0x1ba31e(0x9bb)]),_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0x1bd)]&&clearTimeout(_0x435689[_0x1ba31e(0xc6c)][_0x112b4a]['disconnectedTimeout']),_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0x917)]&&_0x1ba31e(0x2d6)in _0x435689['rpcs'][_0x112b4a][_0x1ba31e(0x917)][_0x1ba31e(0x8d0)]?_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0x340)]=='0'&&_0x435689[_0x1ba31e(0xc6c)][_0x112b4a]['opacityMuted']=='1'?(_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0x917)][_0x1ba31e(0x8d0)][_0x1ba31e(0x2d6)]='1',_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0x340)]='1',updateMixer()):_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0x340)]='1':_0x435689[_0x1ba31e(0xc6c)][_0x112b4a][_0x1ba31e(0x340)]='1');}}catch(_0x58ce76){}},_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0x1d9)]=function(_0x12aef1){var _0xd72907=_0x5dc4d4;log(_0x12aef1);if(_0x12aef1['channel']['label']&&_0x12aef1[_0xd72907(0x341)][_0xd72907(0xc69)]!==_0xd72907(0x4d0)){if(_0x435689[_0xd72907(0x5d6)][_0xd72907(0x49c)](_0x435689[_0xd72907(0xc6c)][_0x112b4a][_0xd72907(0x885)]))return;if(_0x12aef1['channel']['label']===_0xd72907(0x6a9))_0x435689[_0xd72907(0x8bf)](_0x112b4a,_0x12aef1['channel']);else _0x12aef1[_0xd72907(0x341)]['label']===_0xd72907(0x794)?_0x435689['recieveResourcesChannel'](_0x112b4a,_0x12aef1[_0xd72907(0x341)]):_0x435689[_0xd72907(0x24c)](_0x435689[_0xd72907(0xc6c)],_0x112b4a,_0x12aef1['channel']);return;}_0x435689[_0xd72907(0xc6c)][_0x112b4a][_0xd72907(0x19b)]=_0x12aef1[_0xd72907(0x341)],_0x435689[_0xd72907(0xc6c)][_0x112b4a][_0xd72907(0x19b)][_0xd72907(0x9d6)]=_0x112b4a,_0x435689[_0xd72907(0xc6c)][_0x112b4a][_0xd72907(0x19b)]['onerror']=_0x6753bb=>{var _0x3987bc=_0xd72907;_0x6753bb[_0x3987bc(0x4bc)]&&_0x6753bb[_0x3987bc(0x4bc)][_0x3987bc(0x7bc)]&&_0x6753bb[_0x3987bc(0x4bc)][_0x3987bc(0x7bc)]!==0xc&&warnlog(_0x6753bb),log(_0x3987bc(0x68f)+_0x112b4a);},_0x435689['rpcs'][_0x112b4a]['receiveChannel'][_0xd72907(0xada)]=_0x1e862e=>{var _0x363c9a=_0xd72907;_0x435689[_0x363c9a(0xc6c)][_0x112b4a][_0x363c9a(0xac4)]=0x0;var _0x2001db={};_0x2001db[_0x363c9a(0x9bf)]=![],_0x2001db[_0x363c9a(0x4f1)]=![],_0x2001db[_0x363c9a(0x2dc)]=![],_0x2001db['iframe']=![],_0x2001db[_0x363c9a(0x809)]=![],_0x2001db['audio']=![],_0x2001db['video']=![],_0x2001db[_0x363c9a(0x532)]=![],_0x2001db[_0x363c9a(0x823)]=![],_0x2001db[_0x363c9a(0x2aa)]=![],_0x2001db[_0x363c9a(0x800)]=![],_0x2001db[_0x363c9a(0x466)]=![],_0x2001db['allowresources']=![];_0x435689[_0x363c9a(0x844)]&&(_0x435689[_0x363c9a(0x844)]==='red'||_0x435689['audioCodec']===_0x363c9a(0x40b))&&(_0x2001db[_0x363c9a(0x74e)]=_0x435689[_0x363c9a(0x844)]);const _0xdb89db=_0x435689[_0x363c9a(0xc6c)][_0x112b4a]['streamID']||'';let _0x43bc07=_0xdb89db;_0x43bc07&&_0x43bc07[_0x363c9a(0x6b2)](':s')&&(_0x43bc07=_0x43bc07[_0x363c9a(0x3e2)](0x0,-0x2));const _0x47fdfa=!!_0xdb89db&&_0xdb89db[_0x363c9a(0x6b2)](':s'),_0x4d696f=_0x43bc07?_0x43bc07+':s':'',_0x37eb7d=_0x4d696f||(_0x47fdfa?_0xdb89db:''),_0x3f5d73=_0x435689[_0x363c9a(0x937)]!==![]&&_0x435689[_0x363c9a(0x937)]?_0x435689[_0x363c9a(0x937)]:null,_0x434a00=!!(_0x3f5d73&&_0x43bc07&&_0x3f5d73[_0x363c9a(0x49c)](_0x43bc07)),_0x5e67ec=!!(_0x3f5d73&&_0x4d696f&&_0x3f5d73[_0x363c9a(0x49c)](_0x4d696f)),_0xfdb965=!!(_0x3f5d73&&_0xdb89db&&_0x3f5d73[_0x363c9a(0x49c)](_0xdb89db));try{let _0x404d27=!_0x435689['noScreenShare'],_0x2d67cd=!_0x435689[_0x363c9a(0x550)];const _0x3018b5=_0x5486b3=>{var _0x4b9312=_0x363c9a;if(!_0x5486b3||_0x435689[_0x4b9312(0x47d)]===![]||_0x435689[_0x4b9312(0x47d)]===!![])return![];if(_0x435689[_0x4b9312(0x47d)]&&typeof _0x435689[_0x4b9312(0x47d)][_0x4b9312(0x49c)]===_0x4b9312(0x5f9))return _0x435689[_0x4b9312(0x47d)][_0x4b9312(0x49c)](_0x5486b3);return![];};if(!_0x435689[_0x363c9a(0x550)]&&_0x435689[_0x363c9a(0x47d)]!==![]){if(_0x435689[_0x363c9a(0x47d)]===!![])_0x2d67cd=!![],_0x404d27=!![];else _0x3018b5(_0xdb89db)||_0x3018b5(_0x4d696f)||_0x3018b5(_0x43bc07)?(_0x2d67cd=!![],_0x404d27=!![]):(_0x2d67cd=![],_0x404d27=![]);}if(!_0x435689[_0x363c9a(0x550)]){if(_0x435689[_0x363c9a(0x432)]===!![])_0x404d27=!![];else _0x435689[_0x363c9a(0x432)]===![]&&(_0x404d27=![]);if(_0x435689[_0x363c9a(0xaca)]===!![])_0x2d67cd=!![];else _0x435689['screenAudioOverride']===![]&&(_0x2d67cd=![]);}_0x2001db[_0x363c9a(0x800)]=_0x404d27,_0x2001db[_0x363c9a(0x2aa)]=_0x2d67cd;if(_0x2001db['allowscreenvideo']){if(_0x435689['novideo']!==![])(!_0x37eb7d||!_0x435689[_0x363c9a(0x316)][_0x363c9a(0x49c)](_0x37eb7d))&&(_0x2001db['allowscreenvideo']=![]);else{if(_0x435689[_0x363c9a(0x532)]!==![]){if(_0x435689['broadcast']!==null)_0x4d696f&&_0x4d696f===_0x435689[_0x363c9a(0x532)]?_0x2001db[_0x363c9a(0x532)]=!![]:_0x2001db[_0x363c9a(0x800)]=![];else _0x435689[_0x363c9a(0xb2f)]&&(_0x112b4a===_0x435689['directorUUID']?_0x2001db['broadcast']=!![]:_0x2001db[_0x363c9a(0x800)]=![]);}else _0x5e67ec&&(_0x2001db[_0x363c9a(0x800)]=![],_0x2001db['allowscreenaudio']=![]);}}if(_0x2001db['allowscreenaudio']){if(_0x435689[_0x363c9a(0x333)]!==![])(!_0x37eb7d||!_0x435689[_0x363c9a(0x333)][_0x363c9a(0x49c)](_0x37eb7d))&&(_0x2001db['allowscreenaudio']=![]);else _0x435689[_0x363c9a(0x940)]&&(_0x37eb7d&&_0x435689[_0x363c9a(0x940)][_0x363c9a(0x49c)](_0x37eb7d)&&(_0x2001db['allowscreenaudio']=![]));}}catch(_0x41f75f){errorlog(_0x41f75f);}try{if(_0x435689['novideo']!==![]){if(_0xdb89db&&_0x435689[_0x363c9a(0x316)][_0x363c9a(0x49c)](_0xdb89db))_0x2001db['video']=!![];else _0x4d696f&&_0x435689[_0x363c9a(0x316)][_0x363c9a(0x49c)](_0x4d696f)?_0x2001db[_0x363c9a(0x57d)]=_0x47fdfa?!![]:0x2:_0x2001db[_0x363c9a(0x57d)]=![];}else{if(_0x435689[_0x363c9a(0x532)]!==![]){if(_0x435689[_0x363c9a(0x532)]!==null)_0xdb89db&&_0xdb89db===_0x435689[_0x363c9a(0x532)]?(_0x2001db[_0x363c9a(0x532)]=!![],_0x2001db[_0x363c9a(0x57d)]=!![]):_0x2001db[_0x363c9a(0x57d)]=![];else _0x435689[_0x363c9a(0xb2f)]&&(_0x112b4a===_0x435689[_0x363c9a(0xb2f)]?(_0x2001db[_0x363c9a(0x532)]=!![],_0x2001db[_0x363c9a(0x57d)]=!![]):_0x2001db['video']=![]);}else _0x3f5d73?_0x434a00||_0xfdb965?_0x2001db[_0x363c9a(0x57d)]=![]:(_0x2001db['video']=!![],_0x5e67ec&&(_0x2001db['allowscreenvideo']=![],_0x2001db['allowscreenaudio']=![])):_0x2001db['video']=!![];}if(_0x435689[_0x363c9a(0x333)]!==![]){if(_0xdb89db&&_0x435689[_0x363c9a(0x333)][_0x363c9a(0x49c)](_0xdb89db))_0x2001db['audio']=!![];else _0x4d696f&&_0x435689[_0x363c9a(0x333)][_0x363c9a(0x49c)](_0x4d696f)?_0x2001db[_0x363c9a(0x2de)]=0x2:_0x2001db[_0x363c9a(0x2de)]=![];}else _0x435689[_0x363c9a(0x940)]&&_0x435689[_0x363c9a(0x940)][_0x363c9a(0x49c)](_0xdb89db)?_0x2001db[_0x363c9a(0x2de)]=![]:_0x2001db[_0x363c9a(0x2de)]=!![];_0x435689[_0x363c9a(0xae6)]&&_0x435689[_0x363c9a(0xc19)]['indexOf'](_0x112b4a)>=0x0&&(_0x2001db['audio']=![]);_0x435689[_0x363c9a(0x299)]&&_0x435689[_0x363c9a(0xc19)][_0x363c9a(0x97c)](_0x112b4a)>=0x0&&(_0x2001db['video']=![]);_0x435689[_0x363c9a(0x9c5)]!==![]?_0x435689['noiframe']['includes'](_0x435689[_0x363c9a(0xc6c)][_0x112b4a][_0x363c9a(0x885)])?_0x2001db[_0x363c9a(0xbd4)]=!![]:_0x2001db[_0x363c9a(0xbd4)]=![]:_0x2001db[_0x363c9a(0xbd4)]=!![];if(_0x435689[_0x363c9a(0xb8b)]!==![])_0x435689['noWidget'][_0x363c9a(0x49c)](_0x435689[_0x363c9a(0xc6c)][_0x112b4a][_0x363c9a(0x885)])?_0x2001db[_0x363c9a(0x809)]=!![]:_0x2001db[_0x363c9a(0x809)]=![];else{if(_0x435689[_0x363c9a(0xa34)]!==![])_0x2001db['widget']=![];else _0x435689[_0x363c9a(0x6c0)]&&!_0x435689[_0x363c9a(0x781)]&&_0x435689[_0x363c9a(0x183)]===![]?_0x2001db[_0x363c9a(0x809)]=![]:_0x2001db[_0x363c9a(0x809)]=!![];}_0x435689[_0x363c9a(0xb6c)]&&(_0x2001db['allowmeshcast']=![]);if(_0x435689[_0x363c9a(0x3a8)]===_0x363c9a(0x320))_0x2001db[_0x363c9a(0xa8a)]=![],_0x2001db['allowscreenwhipout']=![];else _0x435689['screenWhepPreference']==='whep'&&(_0x2001db[_0x363c9a(0x800)]=![],_0x2001db[_0x363c9a(0x2aa)]=![]);_0x435689[_0x363c9a(0xb6a)]&&(_0x2001db['hidedirector']=_0x435689[_0x363c9a(0xb6a)]),_0x435689[_0x363c9a(0x62e)]!==![]&&(!_0x435689['allowVideos'][_0x363c9a(0x49c)](_0x435689['rpcs'][_0x112b4a][_0x363c9a(0x885)])&&(_0x2001db[_0x363c9a(0x57d)]=![],_0x2001db[_0x363c9a(0x2de)]=![])),(_0x435689['midiIn']||_0x435689['midiRemote']||_0x435689[_0x363c9a(0x6a1)]||_0x435689['midiTimecode'])&&(_0x2001db[_0x363c9a(0x4f1)]=_0x435689[_0x363c9a(0x538)]||_0x435689[_0x363c9a(0x7df)]||_0x435689[_0x363c9a(0x6a1)]||_0x435689[_0x363c9a(0x449)]),_0x2001db[_0x363c9a(0x9bf)]=!![],_0x435689[_0x363c9a(0x7e4)]&&(_0x2001db[_0x363c9a(0x9bf)]=![]),_0x435689[_0x363c9a(0x678)]?_0x2001db[_0x363c9a(0x466)]=![]:_0x2001db[_0x363c9a(0x466)]=_0x435689[_0x363c9a(0x98d)]?0x2:0x1,_0x435689[_0x363c9a(0x208)]&&(_0x2001db['allowresources']=_0x435689[_0x363c9a(0x208)]),_0x435689[_0x363c9a(0x9ee)]&&(_0x2001db[_0x363c9a(0x2dc)]=!![]),_0x435689[_0x363c9a(0x1a3)]&&(_0x435689['codec']==_0x363c9a(0x265)||_0x435689[_0x363c9a(0x1a3)]==_0x363c9a(0xa7c)||_0x435689[_0x363c9a(0x1a3)]==_0x363c9a(0x6e3))&&(_0x2001db['allowwebp']=!![]),_0x435689[_0x363c9a(0x574)]&&(_0x2001db[_0x363c9a(0xb50)]=!![]),_0x435689['badStreamList'][_0x363c9a(0x49c)](_0x435689[_0x363c9a(0xc6c)][_0x112b4a]['streamID'])&&(warnlog(_0x363c9a(0xb70)),_0x2001db['allowmeshcast']=![],_0x2001db['allowchunked']=![],_0x2001db[_0x363c9a(0x2dc)]=![],_0x2001db[_0x363c9a(0x38c)]=![],_0x2001db[_0x363c9a(0xb50)]=![],_0x2001db['downloads']=![],_0x2001db[_0x363c9a(0x4f1)]=![],_0x2001db[_0x363c9a(0xbd4)]=![],_0x2001db[_0x363c9a(0x809)]=![],_0x2001db['audio']=![],_0x2001db[_0x363c9a(0x57d)]=![],_0x2001db[_0x363c9a(0x532)]=![],_0x2001db['allowwebp']=![],_0x2001db['allowscreenaudio']=![],_0x2001db[_0x363c9a(0x800)]=![]);}catch(_0x1f1d2b){errorlog(_0x1f1d2b);}try{_0x2001db[_0x363c9a(0x7af)]={},_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0xc69)]=_0x435689[_0x363c9a(0xc69)],_0x2001db['info'][_0x363c9a(0x920)]=_0x435689['meta'];_0x435689[_0x363c9a(0x550)]&&(_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x84d)]=![]);_0x2001db[_0x363c9a(0x7af)]['order']=_0x435689[_0x363c9a(0x367)];_0x435689['preferChannel']&&(_0x2001db[_0x363c9a(0x7af)]['preferChannel']=_0x435689[_0x363c9a(0x222)]);_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x3be)]=_0x435689['stereo'],_0x2001db['info'][_0x363c9a(0x256)]=_0x435689['bitrate'],_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x61e)]=_0x435689[_0x363c9a(0xa3f)],_0x2001db['info'][_0x363c9a(0x5dd)]=_0x435689[_0x363c9a(0x1a3)];_0x435689[_0x363c9a(0x844)]&&(_0x2001db['info']['audio_codec_url']=_0x435689[_0x363c9a(0x844)]);_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0xa46)]=_0x435689[_0x363c9a(0xa46)],_0x2001db[_0x363c9a(0x7af)]['forceios']=_0x435689['forceios'],_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x948)]=_0x435689[_0x363c9a(0x166)],_0x2001db[_0x363c9a(0x7af)]['ptime']=_0x435689[_0x363c9a(0xa71)],_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x2f5)]=_0x435689[_0x363c9a(0x2f5)],_0x2001db['info'][_0x363c9a(0x7f6)]=_0x435689[_0x363c9a(0x7f6)];Firefox&&(_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0xa67)]=Firefox);ChromiumVersion&&(_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x969)]=ChromiumVersion);SafariVersion&&(_0x2001db[_0x363c9a(0x7af)]['safari']=SafariVersion);navigator&&navigator['userAgent']&&(_0x2001db[_0x363c9a(0x7af)]['useragent']=navigator[_0x363c9a(0x19a)]);navigator&&navigator['platform']&&(_0x2001db[_0x363c9a(0x7af)]['platform']=navigator[_0x363c9a(0x2ef)]);gpgpuSupport&&(_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x705)]=gpgpuSupport);cpuSupport&&(_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x8c6)]=cpuSupport);if(_0x435689[_0x363c9a(0x173)]===![]){if(window[_0x363c9a(0x2c9)]){_0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x44b)]=window['obsstudio'][_0x363c9a(0x76e)];try{_0x2001db=_0x435689[_0x363c9a(0x2a2)](_0x2001db,_0x112b4a);}catch(_0x165a02){errorlog(_0x165a02),warnUser(_0x165a02['message']);}}else _0x2001db[_0x363c9a(0x7af)][_0x363c9a(0x44b)]=![];}else _0x2001db['info']['obs']=![];}catch(_0x11419b){}_0x2001db[_0x363c9a(0x2ba)]=![],_0x2001db[_0x363c9a(0xa34)]=![],_0x2001db[_0x363c9a(0x781)]=![],_0x2001db[_0x363c9a(0x5f1)]=![],_0x2001db[_0x363c9a(0x337)]=![];_0x435689[_0x363c9a(0xba7)]&&(_0x2001db['remote']=!![]);_0x435689['enhance']&&(_0x2001db[_0x363c9a(0x3b8)]=!![]);_0x435689[_0x363c9a(0x517)]&&(_0x2001db[_0x363c9a(0x517)]=_0x435689['degrade']);_0x435689[_0x363c9a(0xb15)]&&(_0x2001db[_0x363c9a(0xb15)]=_0x435689['solo']);_0x435689[_0x363c9a(0x2a6)]!==![]&&(_0x2001db[_0x363c9a(0x2a6)]=_0x435689[_0x363c9a(0x2a6)]);if(_0x435689[_0x363c9a(0x781)]){_0x2001db[_0x363c9a(0x781)]=!![],_0x2001db['forceios']=_0x435689[_0x363c9a(0x337)];if(_0x435689[_0x363c9a(0xb2f)]&&_0x435689[_0x363c9a(0xb2f)]===_0x112b4a)_0x435689[_0x363c9a(0x1a6)]();else{var _0x18e853={};_0x18e853[_0x363c9a(0x52a)]=[];for(var _0x25d9b0 in _0x435689[_0x363c9a(0x76f)]){_0x435689[_0x363c9a(0x76f)][_0x25d9b0][_0x363c9a(0x908)]===!![]&&_0x18e853[_0x363c9a(0x52a)][_0x363c9a(0x35c)](_0x25d9b0);}_0x18e853['addCoDirector'][_0x363c9a(0xb04)]&&(_0x2001db[_0x363c9a(0xb41)]=_0x18e853);}if(_0x435689[_0x363c9a(0xa09)]&&_0x435689['roomTimer']>0x0)_0x2001db['setClock']=_0x435689[_0x363c9a(0xa09)]-Date[_0x363c9a(0x1ab)]()/0x3e8,_0x2001db[_0x363c9a(0x349)]=!![],_0x2001db[_0x363c9a(0x16c)]=!![];else _0x435689[_0x363c9a(0xa09)]&&_0x435689[_0x363c9a(0xa09)]<0x0&&(_0x2001db[_0x363c9a(0x4f4)]=_0x435689[_0x363c9a(0xa09)]*-0x1,_0x2001db[_0x363c9a(0x349)]=!![],_0x2001db['startClock']=!![],_0x2001db[_0x363c9a(0x963)]=!![]);_0x435689['showRoomTime']&&(_0x2001db[_0x363c9a(0x4d7)]=!![]);}else{if(_0x435689[_0x363c9a(0xa34)]!==![])_0x2001db['scene']=_0x435689['scene'],(_0x435689['showDirector']||_0x435689['solo'])&&(_0x2001db[_0x363c9a(0xb33)]=_0x435689[_0x363c9a(0xb33)]||_0x435689[_0x363c9a(0xb15)]);else _0x435689['roomid']!==![]&&_0x435689[_0x363c9a(0xb0b)]!==''&&(_0x2001db[_0x363c9a(0x337)]=_0x435689[_0x363c9a(0x337)],_0x2001db[_0x363c9a(0x2ba)]=!![]);}if(_0x435689[_0x363c9a(0x61b)])_0x2001db[_0x363c9a(0x61b)]=parseFloat(_0x435689[_0x363c9a(0x61b)]);else(_0x435689[_0x363c9a(0x4b0)]||_0x435689[_0x363c9a(0xbf4)])&&(_0x2001db[_0x363c9a(0x3f6)]={},_0x2001db[_0x363c9a(0x3f6)]['h']=null,_0x2001db['requestResolution']['w']=null,_0x435689[_0x363c9a(0x4b0)]&&(_0x2001db[_0x363c9a(0x3f6)]['h']=_0x435689[_0x363c9a(0x4b0)],_0x435689[_0x363c9a(0xc6c)][_0x112b4a][_0x363c9a(0x4bd)]=_0x435689[_0x363c9a(0x4b0)]),_0x435689[_0x363c9a(0xbf4)]&&(_0x2001db['requestResolution']['w']=_0x435689[_0x363c9a(0xbf4)],_0x435689[_0x363c9a(0xc6c)][_0x112b4a][_0x363c9a(0x9f5)]=_0x435689[_0x363c9a(0xbf4)]));!_0x435689[_0x363c9a(0xb0b)]&&(_0x435689[_0x363c9a(0x192)]&&(playtone(![],_0x363c9a(0x60c)),showNotification('There\x27s\x20a\x20new\x20incoming\x20connection.')));_0x435689['rpcs'][_0x112b4a][_0x363c9a(0x409)]=_0x2001db;_0x435689[_0x363c9a(0x43f)](_0x2001db,_0x112b4a)?log(_0x363c9a(0x5bc)):errorlog(_0x363c9a(0x1b9));if(_0x435689[_0x363c9a(0xb69)])try{_0x435689['obsStateSync'](![],_0x112b4a);}catch(_0x573675){errorlog(_0x573675);}pokeIframeAPI(_0x363c9a(0x182),!![],_0x112b4a),pokeIframeAPI(_0x363c9a(0x191),!![],_0x112b4a),pokeAPI(_0x363c9a(0x849),_0x435689[_0x363c9a(0xc6c)][_0x112b4a][_0x363c9a(0x885)]),_0x435689[_0x363c9a(0x4f0)]&&(_0x435689['layout_array']&&(_0x435689[_0x363c9a(0xb50)]=combinedLayout(_0x435689[_0x363c9a(0x7ec)])),updateMixer()),clearTimeout(_0x435689[_0x363c9a(0xc6c)][_0x112b4a][_0x363c9a(0x543)]),_0x435689[_0x363c9a(0xc6c)][_0x112b4a][_0x363c9a(0x543)]=setTimeout(processStats,0x0,_0x112b4a);},_0x435689['rpcs'][_0x112b4a]['receiveChannel'][_0xd72907(0x13c)]=async _0x33b17a=>{var _0x4e3ba2=_0xd72907;if(typeof _0x33b17a[_0x4e3ba2(0x36f)]==_0x4e3ba2(0x31f)){if(!_0x435689['rpcs'][_0x112b4a][_0x4e3ba2(0x7ea)]){_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)]=document[_0x4e3ba2(0x6f0)](_0x4e3ba2(0x99a)),_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a]['imageElement'][_0x4e3ba2(0x3dd)]=0x10,_0x435689['rpcs'][_0x112b4a][_0x4e3ba2(0x7ea)][_0x4e3ba2(0x348)]=0x9,_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)][_0x4e3ba2(0x8d0)][_0x4e3ba2(0xa4f)]=_0x4e3ba2(0x7be),_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)][_0x4e3ba2(0xc6b)]['UUID']=_0x112b4a;try{_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)]['dataset']['sid']=_0x435689['rpcs'][_0x112b4a][_0x4e3ba2(0x885)];}catch(_0x552653){}_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)][_0x4e3ba2(0xc09)]=![],_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)]['addEventListener'](_0x4e3ba2(0xa08),function(_0x2729fe){var _0x405060=_0x4e3ba2;log(_0x405060(0x877));try{if(_0x2729fe[_0x405060(0x5c3)]||_0x2729fe[_0x405060(0x3b5)]){_0x2729fe[_0x405060(0x4cd)]();if(_0x435689[_0x405060(0x463)]!==![]){var _0x1859ed=_0x2729fe[_0x405060(0x5e4)][_0x405060(0xc6b)][_0x405060(0x9d6)];if(_0x405060(0x5c2)in _0x435689[_0x405060(0xc6c)][_0x1859ed]){var [_0x34486b,_0x29992b]=statsMenuCreator();printViewStats(_0x29992b,_0x1859ed),_0x34486b[_0x405060(0x410)]=setInterval(printViewStats,_0x435689[_0x405060(0x29f)],_0x29992b,_0x1859ed);}}return _0x2729fe[_0x405060(0x8e0)](),![];}}catch(_0x4171e6){errorlog(_0x4171e6);}}),updateMixer();}else _0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)][_0x4e3ba2(0xc09)]&&(_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)][_0x4e3ba2(0xc09)]=![],_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)][_0x4e3ba2(0x8d0)][_0x4e3ba2(0x3bf)]=_0x4e3ba2(0xbfa));_0x435689[_0x4e3ba2(0xc6c)][_0x112b4a][_0x4e3ba2(0x7ea)]['src']=window[_0x4e3ba2(0xb11)][_0x4e3ba2(0x42c)](new Blob([new Uint8Array(_0x33b17a[_0x4e3ba2(0x36f)])],{'type':'image/webp'}));return;}try{var _0x4f0344=JSON[_0x4e3ba2(0x48a)](_0x33b17a[_0x4e3ba2(0x36f)]);_0x4f0344[_0x4e3ba2(0x9d6)]=_0x112b4a;if(_0x4f0344['smid']||_0x4f0344[_0x4e3ba2(0x370)]){let _0x4e9107=_0x4f0344[_0x4e3ba2(0x846)]||_0x4f0344[_0x4e3ba2(0x370)];if(_0x435689[_0x4e3ba2(0x8d7)][_0x112b4a]){if(_0x435689[_0x4e3ba2(0x8d7)][_0x112b4a][_0x4e3ba2(0x49c)](_0x4e9107))return;else _0x435689['mids'][_0x112b4a][_0x4e3ba2(0x35c)](_0x4e9107);}else _0x435689['mids'][_0x112b4a]=[_0x4e9107];}'altUUID'in _0x4f0344?await _0x435689['processRPCSOnMessage'](_0x4f0344,_0x112b4a+_0x4e3ba2(0xac8)):await _0x435689[_0x4e3ba2(0x19f)](_0x4f0344,_0x112b4a);}catch(_0x1c2c33){warnlog('mystery-message-recieved'),warnlog(_0x1c2c33),warnlog(_0x33b17a[_0x4e3ba2(0x36f)]);}},_0x435689[_0xd72907(0x19f)]=async function(_0x322f00,_0x2b538d){var _0x2a57e8=_0xd72907;warnlog(_0x322f00);if('bye'in _0x322f00){warnlog(_0x2a57e8(0x4a9)),_0x435689[_0x2a57e8(0x2a0)](_0x2b538d,!![]);return;}else{if('ping'in _0x322f00){var _0x59d451={};_0x59d451[_0x2a57e8(0xa91)]=_0x322f00[_0x2a57e8(0x5a1)],_0x435689[_0x2a57e8(0x43f)](_0x59d451,_0x2b538d),warnlog(_0x2a57e8(0x724));return;}else{if(_0x2a57e8(0xa91)in _0x322f00){try{_0x435689['rpcs'][_0x2b538d]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x94e)]=_0x322f00[_0x2a57e8(0xa91)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xba1)]=Date[_0x2a57e8(0x1ab)]());}catch(_0x4f907a){}warnlog(_0x2a57e8(0x923));return;}}}log(_0x2a57e8(0xc3c));var _0x190fe0=![],_0x131bef=![];if('description'in _0x322f00)_0x435689[_0x2a57e8(0x51f)](_0x322f00);else{if('candidate'in _0x322f00)_0x322f00[_0x2a57e8(0x9d6)]=_0x2b538d,log(_0x2a57e8(0x7e6)),_0x435689[_0x2a57e8(0xa9d)](_0x322f00);else'candidates'in _0x322f00&&(_0x322f00[_0x2a57e8(0x9d6)]=_0x2b538d,log(_0x2a57e8(0x3eb)),_0x435689[_0x2a57e8(0x7eb)](_0x322f00));}_0x2a57e8(0x7c5)in _0x322f00&&_0x3b8d8f(_0x322f00[_0x2a57e8(0x7c5)]);if('rejected'in _0x322f00){if(_0x322f00[_0x2a57e8(0x592)]===_0x2a57e8(0x85e))_0x435689[_0x2a57e8(0x3e3)]=![],!_0x435689[_0x2a57e8(0x326)]&&(warnUser(getTranslation(_0x2a57e8(0x152))),miniTranslate(getById(_0x2a57e8(0x8cf)),'not-the-director'));else{if(_0x322f00['rejected']===_0x2a57e8(0x411))!_0x435689[_0x2a57e8(0x326)]&&warnUser(getTranslation(_0x2a57e8(0x1f6)),0xbb8);else{if(!_0x435689[_0x2a57e8(0x326)]){if(_0x435689[_0x2a57e8(0xb2f)]===_0x2b538d)warnUser(getTranslation('request-failed'),0x1388);else _0x435689['remote']&&!_0x435689[_0x2a57e8(0x781)]?warnUser(getTranslation(_0x2a57e8(0x22f)),0x1388):warnUser(getTranslation(_0x2a57e8(0x6df)),0x1388);}else{if(_0x435689[_0x2a57e8(0x781)])!_0x435689[_0x2a57e8(0x326)]&&warnUser(_0x2a57e8(0x1b4)+_0x322f00[_0x2a57e8(0x592)]+_0x2a57e8(0x845),0x1388);else{if(!_0x435689[_0x2a57e8(0x326)])_0x435689[_0x2a57e8(0xba7)]?warnUser(getTranslation(_0x2a57e8(0x24d)),0x1388):warnUser(getTranslation(_0x2a57e8(0x6e5)),0x1388);else{}}}}}errorlog(_0x2a57e8(0x169)+_0x322f00['rejected']+',\x20isDirector:\x20'+_0x435689['director']),pokeIframeAPI(_0x2a57e8(0x592),_0x322f00[_0x2a57e8(0x592)],_0x2b538d);return;}else{if(_0x2a57e8(0xb96)in _0x322f00){if(_0x322f00[_0x2a57e8(0xb96)]===_0x2a57e8(0x85e)){if(_0x435689[_0x2a57e8(0x781)]){try{_0x435689[_0x2a57e8(0xc69)]===![]&&(document[_0x2a57e8(0x967)]=getTranslation(_0x2a57e8(0x9eb)));}catch(_0x3ae43d){errorlog(_0x3ae43d);}!_0x435689['cleanOutput']&&!_0x435689[_0x2a57e8(0x3e3)]&&(warnUser(getTranslation('approved-as-director'),0xbb8),miniTranslate(getById(_0x2a57e8(0x8cf)),'you-are-a-codirector'),miniTranslate(getById('yourDirectorStatus'),_0x2a57e8(0x77a))),!_0x435689[_0x2a57e8(0x3e3)]&&(_0x435689['directorState']=!![],pokeAPI(_0x2a57e8(0x241),!![]),_0x435689['initialDirectorSync'](_0x2b538d));}}log(_0x2a57e8(0x360)+_0x322f00[_0x2a57e8(0xb96)]),pokeIframeAPI(_0x2a57e8(0xb96),_0x322f00[_0x2a57e8(0xb96)],_0x2b538d);return;}}if(_0x2a57e8(0x70d)in _0x322f00){typeof handleConnectionMapResponse==='function'&&handleConnectionMapResponse(_0x322f00,_0x2b538d);return;}if('iframeSrc'in _0x322f00)try{_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['iframeSrc']=_0x322f00['iframeSrc']||![];if(_0x435689[_0x2a57e8(0x781)]){if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xafd)]){var _0xff5c93=document[_0x2a57e8(0x6f0)](_0x2a57e8(0x7fd));_0xff5c93[_0x2a57e8(0x39e)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['iframeSrc'],_0xff5c93[_0x2a57e8(0x39e)]=_0xff5c93[_0x2a57e8(0x5be)],_0xff5c93=_0xff5c93[_0x2a57e8(0x9ca)]||_0xff5c93['innerText']||'',getById('iframeDetails_'+_0x2b538d)[_0x2a57e8(0x5be)]=_0x2a57e8(0x500)+_0xff5c93+'\x27\x20target=\x27_blank\x27>'+_0xff5c93+_0x2a57e8(0x94d),getById('iframeDetails_'+_0x2b538d)[_0x2a57e8(0xab1)][_0x2a57e8(0xae4)]('hidden');}else getById('iframeDetails_'+_0x2b538d)[_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0xc09)),getById(_0x2a57e8(0x3ac)+_0x2b538d)[_0x2a57e8(0x39e)]='';}else{if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xafd)]==![]){try{_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)][_0x2a57e8(0xae4)]();}catch(_0x2412cb){errorlog(_0x2412cb);}_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x92f)]&&(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x92f)]['remove'](),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x92f)]=![]);_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['iframeEle']=![],_0x190fe0=!![];if(_0x435689[_0x2a57e8(0x532)]!==![]){if(_0x435689[_0x2a57e8(0x532)]!==null)_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['streamID']===_0x435689[_0x2a57e8(0x532)]&&(_0x435689[_0x2a57e8(0x545)]=![]);else _0x2b538d==_0x435689[_0x2a57e8(0xb2f)]&&(_0x435689[_0x2a57e8(0x545)]=![]);}}else{if(_0x435689[_0x2a57e8(0x532)]!==![]){if(_0x435689[_0x2a57e8(0x532)]!==null){if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['streamID']===_0x435689[_0x2a57e8(0x532)]){if(_0x435689[_0x2a57e8(0x9c5)]===![])_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['iframeEle']=loadIframe(_0x322f00[_0x2a57e8(0xafd)],_0x2b538d),_0x190fe0=!![],_0x435689['broadcastIFrame']=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]&&(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x964)][_0x2a57e8(0xc6b)][_0x2a57e8(0x952)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['streamID']);else _0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['streamID']in _0x435689[_0x2a57e8(0x9c5)]&&(_0x435689['rpcs'][_0x2b538d]['iframeEle']=loadIframe(_0x322f00['iframeSrc'],_0x2b538d),_0x190fe0=!![],_0x435689[_0x2a57e8(0x545)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)],_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x885)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)][_0x2a57e8(0xc6b)][_0x2a57e8(0x952)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]));}}else{if(_0x435689[_0x2a57e8(0xb2f)]){if(_0x2b538d==_0x435689[_0x2a57e8(0xb2f)]){if(_0x435689[_0x2a57e8(0x9c5)]===![])_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)]=loadIframe(_0x322f00[_0x2a57e8(0xafd)],_0x2b538d),_0x190fe0=!![],_0x435689['broadcastIFrame']=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)][_0x2a57e8(0xc6b)]['sid']=_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x885)]);else _0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['streamID']in _0x435689[_0x2a57e8(0x9c5)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)]=loadIframe(_0x322f00[_0x2a57e8(0xafd)],_0x2b538d),_0x190fe0=!![],_0x435689[_0x2a57e8(0x545)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)][_0x2a57e8(0xc6b)][_0x2a57e8(0x952)]=_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x885)]));}}}}else{if(_0x435689[_0x2a57e8(0x9c5)]===![])_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)]=loadIframe(_0x322f00['iframeSrc'],_0x2b538d),_0x190fe0=!![],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)][_0x2a57e8(0xc6b)][_0x2a57e8(0x952)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]);else _0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]in _0x435689['noiframe']&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)]=loadIframe(_0x322f00[_0x2a57e8(0xafd)],_0x2b538d),_0x190fe0=!![],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)][_0x2a57e8(0xc6b)][_0x2a57e8(0x952)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['streamID']));}}}}catch(_0xb021fc){errorlog(_0xb021fc);}else{if(_0x2a57e8(0x25c)in _0x322f00){if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x964)])try{_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xafd)][_0x2a57e8(0x8da)](_0x2a57e8(0xaa7))&&processIframeSyncUpdates(_0x322f00[_0x2a57e8(0x25c)],_0x2b538d);}catch(_0x28baab){errorlog(_0x28baab);}}}if(_0x2a57e8(0x393)in _0x322f00){if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)]&&_0x435689[_0x2a57e8(0x9ee)]){!_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x4e4)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x4e4)]=receiveDrawingOnVideo(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x917)],_0x2b538d));if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x4e4)]){if(typeof _0x322f00[_0x2a57e8(0x393)]=='string'){if(_0x322f00[_0x2a57e8(0x393)]==_0x2a57e8(0x761))_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x4e4)][_0x2a57e8(0x887)]();else{if(_0x322f00[_0x2a57e8(0x393)]==_0x2a57e8(0x231))_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x4e4)][_0x2a57e8(0x231)]();else _0x322f00[_0x2a57e8(0x393)]=='undo'&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x4e4)][_0x2a57e8(0x8aa)]('undo');}}else _0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x4e4)][_0x2a57e8(0x8aa)](_0x322f00[_0x2a57e8(0x393)]);}}return;}if(_0x2a57e8(0xba7)in _0x322f00)try{_0x322f00=await _0x435689[_0x2a57e8(0x603)](_0x322f00);if(!_0x322f00)return;}catch(_0x4dc2c){errorlog(_0x4dc2c);}'obsCommand'in _0x322f00&&processOBSCommand(_0x322f00);if('chat'in _0x322f00){var _0x22aaca=![],_0x951caf=![];_0x435689[_0x2a57e8(0xb2f)]===_0x2b538d&&(_0x22aaca=!![],_0x2a57e8(0x75f)in _0x322f00&&(_0x951caf=_0x322f00[_0x2a57e8(0x75f)]));if(_0x435689['director']){if(_0x322f00['chat']==_0x2a57e8(0x93f)){_0x435689[_0x2a57e8(0x192)]&&playtone();getById('hands_'+_0x2b538d)[_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0xc09)),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xa13)]['classList'][_0x2a57e8(0xae4)](_0x2a57e8(0xc09));try{_0x435689['directorState']!==![]&&_0x435689['rpcs'][_0x2b538d]&&_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x885)]&&syncDirectorState({'dataset':{'sid':_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]}});}catch(_0x3179cf){errorlog(_0x3179cf);}}else{if(_0x322f00['chat']==_0x2a57e8(0x859)){getById(_0x2a57e8(0x506)+_0x2b538d)[_0x2a57e8(0xab1)][_0x2a57e8(0x701)]('hidden'),_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xa13)][_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0xc09));try{_0x435689['directorState']!==![]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]&&syncDirectorState({'dataset':{'sid':_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]}});}catch(_0x36ebdd){errorlog(_0x36ebdd);}}}}log(_0x2a57e8(0x6fe)+_0x22aaca),getChatMessage(_0x322f00[_0x2a57e8(0x8a1)],_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xc69)],_0x22aaca,_0x951caf,_0x2b538d);}'tip'in _0x322f00&&(typeof processTipMessage===_0x2a57e8(0x5f9)&&processTipMessage(_0x322f00['tip'],_0x2b538d));_0x2a57e8(0x6cc)in _0x322f00&&_0x435689['gotGenericData'](_0x322f00[_0x2a57e8(0x6cc)],_0x2b538d);'autoSync'in _0x322f00&&(_0x435689['autoSyncObject']=_0x322f00[_0x2a57e8(0xa8b)],_0x435689[_0x2a57e8(0xc3f)](_0x2b538d));_0x2a57e8(0x58a)in _0x322f00&&log(_0x322f00);if(_0x2a57e8(0x1cd)in _0x322f00){log(_0x322f00);_0x322f00['group']?_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x1cd)]=_0x322f00[_0x2a57e8(0x1cd)][_0x2a57e8(0x9fe)](','):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x1cd)]=[];log(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]),_0x190fe0=!![];if(_0x435689[_0x2a57e8(0x781)]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['streamID'])try{syncGroup(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x1cd)],_0x2b538d);}catch(_0x5a9741){errorlog(_0x5a9741);}pokeIframeAPI(_0x2a57e8(0x234),_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x1cd)],_0x2b538d);}_0x2a57e8(0xa6d)in _0x322f00&&(log(_0x322f00),_0x435689[_0x2a57e8(0x842)]&&updateClosedCaptions(_0x322f00,_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xc69)],_0x2b538d));_0x435689[_0x2a57e8(0x781)]&&('audioOptions'in _0x322f00&&updateDirectorsAudio(_0x322f00[_0x2a57e8(0x4c0)],_0x2b538d),'mediaDevices'in _0x322f00&&gotDevicesRemote(_0x322f00[_0x2a57e8(0x21a)],_0x2b538d),_0x2a57e8(0x69d)in _0x322f00&&updateDirectorsVideo(_0x322f00[_0x2a57e8(0x69d)],_0x2b538d),'recorder'in _0x322f00&&updateRemoteRecordButton(_0x2b538d,_0x322f00['recorder'],_0x322f00[_0x2a57e8(0xad7)]||![]),_0x2a57e8(0x7e3)in _0x322f00&&updateGdriveButton(_0x2b538d,_0x322f00['gdrive'],_0x322f00[_0x2a57e8(0xad7)]||![]),_0x2a57e8(0x37c)in _0x322f00&&updateRemoteTimerButton(_0x2b538d,_0x322f00[_0x2a57e8(0x37c)]));if(_0x2a57e8(0xbf1)in _0x322f00)whepWatch(_0x2b538d,_0x322f00[_0x2a57e8(0xbf1)]);else{if(_0x2a57e8(0x4a3)in _0x322f00){const _0x833d06={..._0x322f00['whepScreenSettings'],'media':_0x2a57e8(0xadf)};whepWatch(_0x2b538d,_0x833d06);}else _0x2a57e8(0xc65)in _0x322f00&&(!_0x435689[_0x2a57e8(0xb6c)]&&meshcastWatch(_0x2b538d,_0x322f00['meshcast']));}_0x2a57e8(0xbdd)in _0x322f00&&(_0x435689['directorList']['indexOf'](_0x2b538d)>=0x0&&(_0x435689[_0x2a57e8(0x5a6)]&&lowerhand()));_0x2a57e8(0x616)in _0x322f00&&(_0x435689[_0x2a57e8(0xc19)]['indexOf'](_0x2b538d)>=0x0&&isolateIncomingChannel(_0x322f00['isolateChannel'],_0x2b538d));!_0x435689[_0x2a57e8(0x8e4)]&&_0x435689['directorList'][_0x2a57e8(0x97c)](_0x2b538d)>=0x0&&(_0x2a57e8(0xb50)in _0x322f00&&(_0x435689[_0x2a57e8(0xb50)]=_0x322f00[_0x2a57e8(0xb50)],pokeIframeAPI(_0x2a57e8(0xbc1),_0x435689[_0x2a57e8(0xb50)]),_0x190fe0=!![]),'layout_array'in _0x322f00&&(_0x435689[_0x2a57e8(0x7ec)]=_0x322f00[_0x2a57e8(0x7ec)]));if(_0x2a57e8(0xa90)in _0x322f00){if(!_0x435689['ignoreHighlight']){_0x435689['infocus']=![],_0x435689[_0x2a57e8(0x6eb)]=![];if(_0x435689['broadcast']===![]){log(_0x322f00);if(_0x435689['directorList'][_0x2a57e8(0x97c)](_0x2b538d)>=0x0){if(_0x322f00[_0x2a57e8(0xa90)]!==![]){if(_0x322f00['infocus']===_0x435689['streamID'])_0x435689[_0x2a57e8(0xa90)]=!![];else{if(_0x435689[_0x2a57e8(0x822)][_0x2a57e8(0xb04)]&&!(_0x322f00[_0x2a57e8(0xa90)]in _0x435689[_0x2a57e8(0x822)]))warnlog(_0x2a57e8(0xa9c)),_0x435689['infocus']=![];else{if(_0x435689[_0x2a57e8(0x6c0)]&&_0x435689[_0x2a57e8(0x6c0)]!==_0x322f00[_0x2a57e8(0xa90)])warnlog(_0x2a57e8(0x3a1)),_0x435689[_0x2a57e8(0xa90)]=![];else{if(_0x435689[_0x2a57e8(0xa34)]!==![]&&_0x435689[_0x2a57e8(0xb2f)]&&_0x435689['directorUUID']in _0x435689[_0x2a57e8(0xc6c)]&&!_0x435689[_0x2a57e8(0xc6c)][_0x435689[_0x2a57e8(0xb2f)]]['showDirector']&&_0x322f00[_0x2a57e8(0xa90)]===_0x435689['rpcs'][_0x435689['directorUUID']][_0x2a57e8(0x885)])warnlog('not\x20allowed\x20to\x20show\x20the\x20director'),_0x435689['infocus']=![];else{for(var _0x469cd7 in _0x435689[_0x2a57e8(0xc6c)]){if(_0x435689[_0x2a57e8(0xc6c)][_0x469cd7][_0x2a57e8(0x885)]===_0x322f00['infocus']){_0x435689['infocus']=_0x469cd7;break;}}warnlog(_0x2a57e8(0xc75));}}}}}else _0x435689[_0x2a57e8(0xa90)]=![];_0x190fe0=!![],_0x131bef=!![],_0x435689[_0x2a57e8(0xa90)]?_0x435689['infocusForceMode']=!![]:_0x435689['infocusForceMode']=![];}}}}else{if(_0x2a57e8(0x6eb)in _0x322f00){if(!_0x435689[_0x2a57e8(0x890)]){_0x435689[_0x2a57e8(0xa90)]=![],_0x435689[_0x2a57e8(0x6eb)]=![];if(_0x435689['broadcast']===![]){log(_0x322f00);if(_0x435689[_0x2a57e8(0xc19)][_0x2a57e8(0x97c)](_0x2b538d)>=0x0){if(_0x322f00[_0x2a57e8(0x6eb)]!==![]){if(_0x322f00[_0x2a57e8(0x6eb)]===_0x435689['streamID'])_0x435689[_0x2a57e8(0x6eb)]=!![];else{if(_0x435689['view_set'][_0x2a57e8(0xb04)]&&!(_0x322f00[_0x2a57e8(0x6eb)]in _0x435689[_0x2a57e8(0x822)]))warnlog(_0x2a57e8(0xa9c)),_0x435689[_0x2a57e8(0x6eb)]=![];else{if(_0x435689['view']&&_0x435689['view']!==_0x322f00[_0x2a57e8(0x6eb)])warnlog('NOT\x20VIEW\x20TARGET'),_0x435689['infocus2']=![];else{if(_0x435689[_0x2a57e8(0xa34)]!==![]&&_0x435689[_0x2a57e8(0xb2f)]&&_0x435689[_0x2a57e8(0xb2f)]in _0x435689[_0x2a57e8(0xc6c)]&&!_0x435689[_0x2a57e8(0xc6c)][_0x435689[_0x2a57e8(0xb2f)]][_0x2a57e8(0xb33)]&&_0x322f00[_0x2a57e8(0x6eb)]===_0x435689[_0x2a57e8(0xc6c)][_0x435689[_0x2a57e8(0xb2f)]][_0x2a57e8(0x885)])warnlog(_0x2a57e8(0xac0)),_0x435689[_0x2a57e8(0x6eb)]=![];else{for(var _0x469cd7 in _0x435689[_0x2a57e8(0xc6c)]){if(_0x435689[_0x2a57e8(0xc6c)][_0x469cd7][_0x2a57e8(0x885)]===_0x322f00[_0x2a57e8(0x6eb)]){_0x435689[_0x2a57e8(0x6eb)]=_0x469cd7;break;}}warnlog('ON\x20FOCUS\x20NOT\x20FOUND');}}}}}else _0x435689[_0x2a57e8(0x6eb)]=![];_0x435689['infocus2']?_0x435689[_0x2a57e8(0x801)]=!![]:_0x435689[_0x2a57e8(0x801)]=![],_0x190fe0=!![],_0x131bef=!![];}}}}}_0x2a57e8(0x4f1)in _0x322f00&&_0x322f00['allowmidi']!==![]&&(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x228)]=_0x322f00[_0x2a57e8(0x4f1)]);'sensors'in _0x322f00&&(log(_0x322f00),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x5c2)][_0x2a57e8(0xc50)]=_0x322f00[_0x2a57e8(0xc50)],isIFrame&&parent['postMessage']({'sensors':_0x322f00[_0x2a57e8(0xc50)]},_0x435689[_0x2a57e8(0x555)]));_0x2a57e8(0x638)in _0x322f00&&playbackMIDI(_0x322f00[_0x2a57e8(0x638)],![],_0x2b538d);_0x2a57e8(0x468)in _0x322f00&&_0x322f00[_0x2a57e8(0x468)]&&addDownloadLink(_0x322f00['fileList'],_0x2b538d,_0x435689[_0x2a57e8(0xc6c)]);'rotate_video'in _0x322f00&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc40)]!==_0x322f00[_0x2a57e8(0x814)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['rotate']=_0x322f00['rotate_video'],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)][_0x2a57e8(0x388)]=_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xc40)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)][_0x2a57e8(0xc6b)][_0x2a57e8(0x388)]=_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xc40)]),_0x190fe0=!![]));if(_0x2a57e8(0x7af)in _0x322f00){warnlog(_0x322f00),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x5c2)][_0x2a57e8(0x7af)]=_0x322f00[_0x2a57e8(0x7af)];_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0xa8b)]&&(!_0x435689[_0x2a57e8(0x883)]&&(_0x435689[_0x2a57e8(0x883)]=_0x322f00['info']['autoSync'],_0x435689['autoSyncCallback'](_0x2b538d)));'pseudoguest'in _0x322f00[_0x2a57e8(0x7af)]&&(_0x435689['rpcs'][_0x2b538d]['pseudoguest']=_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x15d)]);_0x322f00[_0x2a57e8(0x7af)]['smallScreen']&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['smallScreen']=!![]);_0x435689['director']&&_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x834)]&&document[_0x2a57e8(0x689)]('#guestFeeds\x20[data--u-u-i-d=\x27'+_0x2b538d+_0x2a57e8(0xa06))[_0x2a57e8(0x3d5)](_0x135d9b=>{var _0xb841a7=_0x2a57e8;_0x135d9b[_0xb841a7(0xab1)][_0xb841a7(0xae4)](_0xb841a7(0xc09));});if(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x2dc)]){_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x9ee)]=_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x2dc)];try{_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)]['syncDrawOnVideo']&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)][_0x2a57e8(0x2d5)]();}catch(_0x13390e){errorlog(_0x13390e);}}if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x33d)]){if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x5c2)][_0x2a57e8(0x7af)][_0x2a57e8(0x3b7)])_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x33d)]['dataset'][_0x2a57e8(0xbfd)]='1';else _0x2a57e8(0x3b7)in _0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x5c2)]['info']&&(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x33d)][_0x2a57e8(0xc6b)]['cpu']='0');}'obs_control'in _0x322f00[_0x2a57e8(0x7af)]&&(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x702)]!==![]?(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x882)]=_0x322f00['info'][_0x2a57e8(0x702)],_0x435689[_0x2a57e8(0xb69)]('details',_0x2b538d)):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x882)]=![]);if(_0x2a57e8(0x920)in _0x322f00[_0x2a57e8(0x7af)])try{typeof _0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x920)]=='object'?_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x920)]=_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x920)]:_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x920)]=![];}catch(_0x112e30){errorlog(_0x112e30);}try{if(_0x435689['director']&&_0x435689[_0x2a57e8(0x3e3)]===!![])for(var _0x3305d0 in _0x435689['pcs']){try{if(_0x435689[_0x2a57e8(0x76f)][_0x3305d0]&&_0x435689[_0x2a57e8(0x76f)][_0x3305d0][_0x2a57e8(0x908)]===!![]){var _0x27d375={'directorSettings':{'addCoDirector':[_0x3305d0]}};_0x435689[_0x2a57e8(0x43f)](_0x27d375,_0x2b538d);}}catch(_0x270897){}}}catch(_0x2ffb4a){errorlog(_0x2ffb4a);}if(_0x2a57e8(0xc69)in _0x322f00[_0x2a57e8(0x7af)])try{!_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xb4d)]&&(typeof _0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0xc69)]==_0x2a57e8(0x332)?_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc69)]=sanitizeLabel(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0xc69)]):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc69)]=![]);applyStyleEffect(_0x2b538d);_0x435689[_0x2a57e8(0x781)]&&setupGuestLabelControl(_0x2b538d);try{_0x435689[_0x2a57e8(0x7da)](_0x2b538d);}catch(_0x15cd9f){errorlog(_0x15cd9f);}}catch(_0x52f4a7){errorlog(_0x52f4a7);}_0x2a57e8(0x80c)in _0x322f00[_0x2a57e8(0x7af)]&&_0x322f00['info']['acceptsTips']&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x80c)]=!![],_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xa35)]=_0x322f00['info'][_0x2a57e8(0xa35)]||null,_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x58d)]=_0x322f00['info']['tipServer']||_0x435689[_0x2a57e8(0x58d)]||_0x2a57e8(0x8dd),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x12a)]=_0x322f00[_0x2a57e8(0x7af)]['tipAmounts']||[0x5,0xa,0x19,0x32,0x64],_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xadc)]=_0x322f00['info'][_0x2a57e8(0xadc)]||_0x2a57e8(0x425),_0x435689[_0x2a57e8(0xbaf)]&&!_0x435689[_0x2a57e8(0x326)]&&(typeof addTipIconToVideo==='function'&&addTipIconToVideo(_0x2b538d)));if(_0x2a57e8(0x367)in _0x322f00[_0x2a57e8(0x7af)])try{_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['order']=parseInt(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x367)])||0x0;if(_0x435689[_0x2a57e8(0x781)]){var _0x54e597=document[_0x2a57e8(0x689)](_0x2a57e8(0x329)+_0x2b538d+'\x22]');_0x54e597[0x0]&&(_0x54e597[0x0][_0x2a57e8(0x39e)]=_0x435689['rpcs'][_0x2b538d]['order']);}}catch(_0x21dffc){errorlog(_0x21dffc);}else _0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['order']=0x0;if('preferChannel'in _0x322f00['info']){var _0x239f3f=parseInt(_0x322f00[_0x2a57e8(0x7af)]['preferChannel']);_0x239f3f>=0x1&&_0x239f3f<=0x8&&(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x222)]=_0x239f3f);}if(typeof _0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x51c)]!==_0x2a57e8(0xba0)){var _0x27a8d2=_0x322f00['info'][_0x2a57e8(0x51c)]!==![]&&_0x322f00['info'][_0x2a57e8(0x51c)]!==null&&_0x322f00[_0x2a57e8(0x7af)]['queued']!==0x0&&_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x51c)]!==0x3;_0x435689['applyQueueStateChange'](_0x2b538d,_0x27a8d2,_0x2a57e8(0x23b));if(_0x435689[_0x2a57e8(0x781)]&&!_0x435689['queue']){if(_0x27a8d2)_0x69d88c(_0x2b538d),_0x435689[_0x2a57e8(0x20e)](_0x2b538d);else _0x322f00['info'][_0x2a57e8(0x51c)]===0x3&&_0x50e764(_0x2b538d);}if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]){var _0x4253e3=_0x435689[_0x2a57e8(0x2db)][_0x2a57e8(0x97c)](_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]);_0x4253e3>-0x1&&_0x435689[_0x2a57e8(0x2db)][_0x2a57e8(0x20c)](_0x4253e3,0x1);}if(_0x27a8d2&&!_0x435689[_0x2a57e8(0xb2f)]&&_0x435689['codirector_transfer'])try{_0x435689[_0x2a57e8(0xc19)][_0x2a57e8(0x3d5)](function(_0x545893){var _0x825216=_0x2a57e8;_0x435689[_0x825216(0x458)](_0x545893);});}catch(_0x210d1c){errorlog(_0x210d1c);}}if(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x330)])try{if(_0x2a57e8(0x23c)in _0x322f00[_0x2a57e8(0x7af)]){if(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x23c)]!==null){var _0x48cf10=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['batteryMeter']['querySelector'](_0x2a57e8(0xa95));if(_0x48cf10){var _0xb0e40a=parseInt(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x5c2)]['info'][_0x2a57e8(0x23c)])||0x0;_0xb0e40a>0x64&&(_0xb0e40a=0x64);_0xb0e40a<0x0&&(_0xb0e40a=0x0);_0x48cf10[_0x2a57e8(0x8d0)][_0x2a57e8(0x348)]=parseInt(_0xb0e40a)+'%';if(_0xb0e40a<0xa)_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x330)]['classList'][_0x2a57e8(0xae4)](_0x2a57e8(0x2cd)),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['batteryMeter'][_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0x6e2));else _0xb0e40a<0x19?(_0x435689['rpcs'][_0x2b538d]['batteryMeter'][_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0x6e2)),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['batteryMeter'][_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0x2cd))):(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x330)][_0x2a57e8(0xab1)]['remove'](_0x2a57e8(0x6e2)),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x330)][_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0x2cd)));_0xb0e40a<0x64&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x330)][_0x2a57e8(0xab1)][_0x2a57e8(0xae4)]('hidden'),_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x330)][_0x2a57e8(0x967)]=_0xb0e40a+_0x2a57e8(0x214);}}}_0x2a57e8(0xa77)in _0x322f00[_0x2a57e8(0x7af)]&&(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0xa77)]===![]?(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['batteryMeter'][_0x2a57e8(0xc6b)][_0x2a57e8(0x7a0)]='0',_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x330)][_0x2a57e8(0xab1)]['remove'](_0x2a57e8(0xc09))):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['batteryMeter'][_0x2a57e8(0xc6b)][_0x2a57e8(0x7a0)]='1');}catch(_0x2223ff){errorlog(_0x2223ff);}if(_0x2a57e8(0x27b)in _0x322f00[_0x2a57e8(0x7af)])try{_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x27b)]?_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['group']=_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x27b)][_0x2a57e8(0x9fe)](','):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x1cd)]=[],_0x435689['director']?(initGroupButtons(_0x2b538d),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x1cd)]['length']&&syncGroup(_0x435689['rpcs'][_0x2b538d]['group'],_0x2b538d)):_0x190fe0=!![];}catch(_0x290f46){errorlog(_0x290f46);}if('muted'in _0x322f00[_0x2a57e8(0x7af)])try{_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x904)]=_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0xb62)],(_0x435689[_0x2a57e8(0x976)]||_0x435689[_0x2a57e8(0x7a5)]||_0x435689[_0x2a57e8(0xa34)]===![])&&_0x435689[_0x2a57e8(0xb0b)]&&(!_0x435689[_0x2a57e8(0x326)]||_0x435689[_0x2a57e8(0x781)])?(!_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x8b2)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['remoteMuteElement']=getById(_0x2a57e8(0x888))[_0x2a57e8(0x903)](!![]),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)]['id']=_0x2a57e8(0x66f)+_0x2b538d,_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)]['classList'][_0x2a57e8(0xae4)](_0x2a57e8(0xc09)),_0x190fe0=!![]),_0x435689['rpcs'][_0x2b538d]['remoteMuteState']?_0x435689[_0x2a57e8(0x976)]||_0x435689[_0x2a57e8(0xa34)]===![]?(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)]['remove'](_0x2a57e8(0x1e7)),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0xc09))):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0xc09)):_0x435689[_0x2a57e8(0x7a5)]?(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)]['classList'][_0x2a57e8(0x701)]('unmuted'),_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)]['remove'](_0x2a57e8(0xc09))):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['remoteMuteElement'][_0x2a57e8(0xab1)]['add'](_0x2a57e8(0xc09))):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0xc09)),pokeIframeAPI('remote-mute-state',_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x904)],_0x2b538d);}catch(_0x246e1c){errorlog(_0x246e1c);}if(_0x435689[_0x2a57e8(0x781)]){try{'recording_audio_pipeline'in _0x322f00[_0x2a57e8(0x7af)]&&(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x218)]==![]&&initRecordingImpossible(_0x2b538d));}catch(_0xbb1dc6){errorlog(_0xbb1dc6);}try{if(_0x2a57e8(0x9ed)in _0x322f00['info']){if(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x9ed)]!==![]){let _0x54b84a=parseInt(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x9ed)])||0x0;initAudioButtons(_0x54b84a,_0x2b538d);}}}catch(_0x203f8d){errorlog(_0x203f8d);}try{_0x2a57e8(0x8f9)in _0x322f00[_0x2a57e8(0x7af)]&&(_0x322f00[_0x2a57e8(0x7af)]['directorSpeakerMuted']&&updateRemoteSpeakerMute(_0x2b538d));}catch(_0x1dd745){errorlog(_0x1dd745);}try{'directorDisplayMuted'in _0x322f00[_0x2a57e8(0x7af)]&&(_0x322f00[_0x2a57e8(0x7af)]['directorDisplayMuted']&&updateRemoteDisplayMute(_0x2b538d));}catch(_0x37deaa){errorlog(_0x37deaa);}if(_0x435689[_0x2a57e8(0x7f9)]&&_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x3ea)]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)])try{_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x3ea)][_0x2a57e8(0x3d5)](_0x5dc884=>{var _0xf6e79f=getGuestTargetScene(_0x5dc884,_0x435689['rpcs'][_0x2b538d]['streamID']);_0xf6e79f&&directEnable(_0xf6e79f,!![]);});}catch(_0x1a86cd){errorlog(_0x1a86cd);}}if('directorVideoMuted'in _0x322f00['info'])try{_0x435689[_0x2a57e8(0x781)]?_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x5b4)]&&updateDirectorVideoMute(_0x2b538d):(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['directorVideoMuted']=_0x322f00[_0x2a57e8(0x7af)]['directorVideoMuted'],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x5b4)]&&(_0x2b538d in _0x435689[_0x2a57e8(0xc6c)]&&_0x435689['requestRateLimit'](0x0,_0x2b538d)));}catch(_0x32869a){errorlog(_0x32869a);}let _0x48795a=![];if('directorMirror'in _0x322f00[_0x2a57e8(0x7af)])try{_0x435689[_0x2a57e8(0x781)]&&(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x778)]&&(getById('container_'+_0x2b538d)[_0x2a57e8(0x413)]('[data-action-type=\x22mirror-guest\x22]')&&(getById(_0x2a57e8(0x852)+_0x2b538d)[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))[_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0xc29)),getById(_0x2a57e8(0x852)+_0x2b538d)[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))['ariaPressed']=_0x2a57e8(0x95a)))),_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x2f9)]=_0x322f00['info'][_0x2a57e8(0x778)],_0x48795a=!![];}catch(_0x4c4044){errorlog(_0x4c4044);}if(_0x2a57e8(0xb13)in _0x322f00[_0x2a57e8(0x7af)])try{_0x435689['rpcs'][_0x2b538d]['flipState']=_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0xb13)],_0x48795a=!![];}catch(_0x1ec1dd){errorlog(_0x1ec1dd);}if(_0x48795a&&_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x917)])try{applyMirrorGuest(_0x435689['rpcs'][_0x2b538d]['mirrorState'],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['flipState']);}catch(_0x3328ef){errorlog(_0x3328ef);}if(_0x2a57e8(0xad3)in _0x322f00[_0x2a57e8(0x7af)])try{_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xc02)]=_0x322f00['info']['video_muted_init'],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc02)]&&(_0x435689[_0x2a57e8(0x781)]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x268)]['classList']['remove'](_0x2a57e8(0xc09))),pokeIframeAPI(_0x2a57e8(0x4b1),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc02)],_0x2b538d);}catch(_0x4dfdf8){errorlog(_0x4dfdf8);}_0x2a57e8(0x814)in _0x322f00[_0x2a57e8(0x7af)]&&(_0x435689['rpcs'][_0x2b538d]['rotate']!==_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x814)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['rotate']=_0x322f00['info'][_0x2a57e8(0x814)],_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x917)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)][_0x2a57e8(0x388)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc40)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x917)]['dataset'][_0x2a57e8(0x388)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc40)]),_0x190fe0=!![])),'room_init'in _0x322f00[_0x2a57e8(0x7af)]&&(_0x322f00[_0x2a57e8(0x7af)]['room_init']===![]&&soloLinkGeneratorInit(_0x2b538d)),directorCoDirectorColoring(_0x2b538d),_0x131bef=!![],pokeAPI(_0x2a57e8(0x708),getDetailedState(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)])),pokeIframeAPI(_0x2a57e8(0x8de),_0x322f00[_0x2a57e8(0x7af)],_0x2b538d);}_0x2a57e8(0xa27)in _0x322f00&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x5c2)]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x5c2)][_0x2a57e8(0x7af)]&&processMiniInfoUpdate(_0x322f00[_0x2a57e8(0xa27)],_0x2b538d));if(_0x322f00[_0x2a57e8(0xb41)]){_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x781)]=!![];_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0xa3b)]&&await checkToken();if(_0x435689[_0x2a57e8(0xb2f)]===_0x2b538d){_0x2a57e8(0x72d)in _0x322f00[_0x2a57e8(0xb41)]&&(_0x435689[_0x2a57e8(0x72d)]=parseInt(_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x72d)])||0x0,_0x190fe0=!![]);if(_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x938)]){var _0x1dd9a3=![];'soloVideoMode'in _0x322f00[_0x2a57e8(0xb41)]&&(_0x1dd9a3=_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x6db)]);if(_0x435689[_0x2a57e8(0x532)]===![]){if(_0x1dd9a3===_0x2a57e8(0xad7))_0x435689['infocus']=![],_0x190fe0=!![],_0x131bef=!![];else{if(_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x938)]===_0x435689['streamID'])_0x435689[_0x2a57e8(0xa90)]=!![];else for(var _0x469cd7 in _0x435689['rpcs']){if(_0x435689[_0x2a57e8(0xc6c)][_0x469cd7]['streamID']===_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x938)]){if((_0x435689[_0x2a57e8(0xc19)][_0x2a57e8(0x49c)](_0x469cd7)||_0x435689[_0x2a57e8(0xc6c)][_0x469cd7]['director'])&&!_0x435689['showDirector'])break;_0x435689[_0x2a57e8(0xa90)]=_0x469cd7;break;}}_0x190fe0=!![],_0x131bef=!![];}}}if(_0x2a57e8(0xb33)in _0x322f00['directorSettings']){if(_0x435689[_0x2a57e8(0xa34)]!==![]){if(_0x435689[_0x2a57e8(0xb33)])_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xb33)]=_0x435689[_0x2a57e8(0xb33)];else _0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0xb33)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xb33)]=_0x322f00['directorSettings'][_0x2a57e8(0xb33)]);}}if(_0x435689['scene']!==![]){if(_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0xa34)])for(var _0x469cd7 in _0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0xa34)]){setTimeout(function(_0x324605){var _0x4e0908=_0x2a57e8;_0x435689[_0x4e0908(0xa03)](_0x324605);},0x3e8,_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0xa34)][_0x469cd7]);}if(_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x8fe)])for(var _0x469cd7 in _0x322f00[_0x2a57e8(0xb41)]['mute']){setTimeout(function(_0x5a00c8){_0x435689['directorActions'](_0x5a00c8);},0x3e8,_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x8fe)][_0x469cd7]);}}if(_0x2a57e8(0x52a)in _0x322f00[_0x2a57e8(0xb41)])for(var _0x33a68e=0x0;_0x33a68e<_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x52a)][_0x2a57e8(0xb04)];_0x33a68e++){var _0xdb48d5=_0x322f00[_0x2a57e8(0xb41)][_0x2a57e8(0x52a)][_0x33a68e][_0x2a57e8(0x9b0)]();!_0x435689[_0x2a57e8(0xc19)]['includes'](_0xdb48d5)&&(_0x435689[_0x2a57e8(0xc19)]['push'](_0xdb48d5),addDirectorBlue(_0xdb48d5)),_0xdb48d5 in _0x435689['pcs']&&_0x435689[_0x2a57e8(0x76f)][_0xdb48d5][_0x2a57e8(0xc12)]&&_0x435689['queueType']==0x4&&_0x435689[_0x2a57e8(0x503)](_0xdb48d5);}}}if(_0x435689[_0x2a57e8(0xc19)][_0x2a57e8(0x97c)](_0x2b538d)>=0x0){if(_0x435689[_0x2a57e8(0xa34)]!==![]){'action'in _0x322f00&&_0x435689[_0x2a57e8(0xa03)](_0x322f00);if('audioOutputChannel'in _0x322f00&&_0x322f00[_0x2a57e8(0x952)])for(var _0x469cd7 in _0x435689[_0x2a57e8(0xc6c)]){if(_0x435689[_0x2a57e8(0xc6c)][_0x469cd7][_0x2a57e8(0x885)]===_0x322f00[_0x2a57e8(0x952)]){_0x322f00[_0x2a57e8(0x959)]?(_0x435689['rpcs'][_0x469cd7][_0x2a57e8(0x4ff)]=parseInt(_0x322f00[_0x2a57e8(0x959)])||![],_0x435689[_0x2a57e8(0xc6c)][_0x469cd7][_0x2a57e8(0x4ff)]-=0x1):_0x435689[_0x2a57e8(0xc6c)][_0x469cd7]['channelOffset']=![];updateIncomingVideoElement(_0x469cd7,![],!![]);break;}}}'directorSettings'in _0x322f00&&_0x322f00[_0x2a57e8(0xb41)]['blindAllGuests']&&(!_0x435689[_0x2a57e8(0x781)]&&(_0x435689[_0x2a57e8(0xa34)]===![]&&(_0x435689[_0x2a57e8(0x310)]=!![],_0x435689[_0x2a57e8(0xb58)]())));if(_0x2a57e8(0x66c)in _0x322f00&&_0x2a57e8(0x2d0)in _0x322f00){if(_0x322f00[_0x2a57e8(0x2d0)]&&_0x322f00['mirrorGuestTarget']===!![]){_0x435689[_0x2a57e8(0xb47)]=_0x322f00['mirrorGuestState'],_0x435689[_0x2a57e8(0x51d)]=_0x322f00[_0x2a57e8(0x66c)],applyMirror(_0x435689['mirrorExclude']);if(_0x435689[_0x2a57e8(0x781)]){if(_0x322f00['info'][_0x2a57e8(0x778)]){if(getById('container_director')[_0x2a57e8(0x413)](_0x2a57e8(0x3ff)))getById(_0x2a57e8(0xab7))[_0x2a57e8(0x413)]('[data-action-type=\x22mirror-guest\x22]')[_0x2a57e8(0xab1)]['add'](_0x2a57e8(0xc29)),getById(_0x2a57e8(0xab7))[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))['ariaPressed']=_0x2a57e8(0x95a);else getById(_0x2a57e8(0xab7))[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))&&(getById('container_director')[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))[_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0xc29)),getById(_0x2a57e8(0xab7))[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))[_0x2a57e8(0xb97)]=_0x2a57e8(0x914));}}}else{if(_0x322f00[_0x2a57e8(0x2d0)]&&_0x322f00['mirrorGuestTarget']in _0x435689[_0x2a57e8(0xc6c)]){_0x435689['rpcs'][_0x322f00[_0x2a57e8(0x2d0)]][_0x2a57e8(0x2f9)]=_0x322f00[_0x2a57e8(0x66c)];_0x435689['rpcs'][_0x322f00[_0x2a57e8(0x2d0)]][_0x2a57e8(0x917)]&&applyMirrorGuest(_0x322f00[_0x2a57e8(0x66c)],_0x435689[_0x2a57e8(0xc6c)][_0x322f00[_0x2a57e8(0x2d0)]][_0x2a57e8(0x917)],_0x435689[_0x2a57e8(0xc6c)][_0x322f00[_0x2a57e8(0x2d0)]]['flipState']);if(_0x435689[_0x2a57e8(0x781)]){if(_0x322f00[_0x2a57e8(0x7af)][_0x2a57e8(0x778)])getById(_0x2a57e8(0x852)+_0x2b538d)[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))&&(getById(_0x2a57e8(0x852)+_0x2b538d)[_0x2a57e8(0x413)]('[data-action-type=\x22mirror-guest\x22]')[_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0xc29)),getById(_0x2a57e8(0x852)+_0x2b538d)['querySelector'](_0x2a57e8(0x3ff))[_0x2a57e8(0xb97)]=_0x2a57e8(0x95a));else getById(_0x2a57e8(0x852)+_0x2b538d)[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))&&(getById(_0x2a57e8(0x852)+_0x2b538d)[_0x2a57e8(0x413)](_0x2a57e8(0x3ff))[_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0xc29)),getById(_0x2a57e8(0x852)+_0x2b538d)['querySelector'](_0x2a57e8(0x3ff))['ariaPressed']=_0x2a57e8(0x914));}}}}if(_0x2a57e8(0x3e3)in _0x322f00){!_0x435689[_0x2a57e8(0x232)]&&(_0x435689[_0x2a57e8(0x232)]={});var _0x5d4c8d=_0x322f00[_0x2a57e8(0x3e3)]||{};for(var _0x4b04c1 in _0x5d4c8d){_0x435689[_0x2a57e8(0x232)][_0x4b04c1]=_0x5d4c8d[_0x4b04c1],syncSceneState(_0x4b04c1),syncOtherState(_0x4b04c1),syncLabelState(_0x4b04c1);}log(_0x322f00),pokeAPI(_0x2a57e8(0x708),_0x435689[_0x2a57e8(0x232)]);}if(_0x2a57e8(0xabb)in _0x322f00){_0x435689[_0x2a57e8(0x809)]=_0x322f00[_0x2a57e8(0xabb)]||![];let _0x1cb702=document[_0x2a57e8(0x54c)](_0x2a57e8(0x809));try{_0x1cb702?!_0x435689[_0x2a57e8(0x809)]?(document[_0x2a57e8(0x54c)](_0x2a57e8(0x809))['remove'](),_0x190fe0=!![]):_0x1cb702['src']=parseURL4Iframe(_0x435689[_0x2a57e8(0x809)]):_0x190fe0=!![],_0x435689['director']&&(getById(_0x2a57e8(0x900))[_0x2a57e8(0x62d)]=_0x435689[_0x2a57e8(0x809)]||'');}catch(_0x1b7a85){errorlog(_0x1b7a85);}pokeIframeAPI(_0x2a57e8(0x831),_0x435689[_0x2a57e8(0x809)],_0x2b538d);}if('slotsUpdate'in _0x322f00){_0x435689['currentSlots']=_0x322f00[_0x2a57e8(0x57b)];_0x435689[_0x2a57e8(0x781)]&&updateSlotUI();if(_0x435689[_0x2a57e8(0x8e4)])try{let _0x7d7991=_0x435689[_0x2a57e8(0x3a9)][_0x435689[_0x2a57e8(0x8e4)]];if(_0x7d7991)_0x435689[_0x2a57e8(0xb50)]&&!_0x435689[_0x2a57e8(0xb50)][_0x7d7991]&&(_0x435689[_0x2a57e8(0xb50)]={[_0x7d7991]:{'h':0x64,'w':0x64,'x':0x0,'y':0x0,'c':_0x435689[_0x2a57e8(0x2c8)]}},updateMixer());else _0x435689[_0x2a57e8(0xb50)]&&Object[_0x2a57e8(0x161)](_0x435689[_0x2a57e8(0xb50)])[_0x2a57e8(0xb04)]&&(_0x435689[_0x2a57e8(0xb50)]={},updateMixer());}catch(_0x3d9335){errorlog(_0x3d9335);}else!_0x435689[_0x2a57e8(0x47c)]()&&_0x435689[_0x2a57e8(0x4f0)]&&(_0x435689[_0x2a57e8(0x7ec)]&&(_0x435689[_0x2a57e8(0xb50)]=combinedLayout(_0x435689['layout_array']),updateMixer()),_0x435689[_0x2a57e8(0xb50)]&&(_0x435689[_0x2a57e8(0xb50)]=combinedLayoutSimple(_0x435689[_0x2a57e8(0xb50)]),updateMixer()));warnlog(_0x322f00);}_0x2a57e8(0x582)in _0x322f00&&(_0x435689[_0x2a57e8(0x582)]=_0x322f00[_0x2a57e8(0x582)],_0x2a57e8(0x39d)in _0x322f00?(_0x435689[_0x2a57e8(0x39d)]=_0x322f00[_0x2a57e8(0x39d)],_0x435689[_0x2a57e8(0x47c)]()):_0x435689[_0x2a57e8(0x39d)]=![]);_0x2a57e8(0x31c)in _0x322f00&&stopClock();_0x2a57e8(0xa23)in _0x322f00&&resumeClock();_0x2a57e8(0x4f4)in _0x322f00&&setClock(_0x322f00[_0x2a57e8(0x4f4)]);_0x2a57e8(0xb5f)in _0x322f00&&hideClock();_0x2a57e8(0x349)in _0x322f00&&showClock();_0x2a57e8(0x16c)in _0x322f00&&startClock();'pauseClock'in _0x322f00&&pauseClock();if(_0x2a57e8(0x4d7)in _0x322f00){if(_0x435689[_0x2a57e8(0x4d7)]!==![]){if(_0x322f00[_0x2a57e8(0x4d7)]&&!_0x435689['showTime'])toggleClock(_0x322f00[_0x2a57e8(0x58b)]||![]);else!_0x322f00[_0x2a57e8(0x4d7)]&&_0x435689[_0x2a57e8(0x4d7)]&&toggleClock(_0x322f00[_0x2a57e8(0x58b)]||![]);}}}if(_0x2a57e8(0x367)in _0x322f00){_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['order']=parseInt(_0x322f00[_0x2a57e8(0x367)])||0x0;_0x2b538d in _0x435689['pcs']&&(_0x435689[_0x2a57e8(0x76f)][_0x2b538d][_0x2a57e8(0x367)]=parseInt(_0x322f00[_0x2a57e8(0x367)])||0x0);if(_0x435689[_0x2a57e8(0x781)]){var _0x54e597=document[_0x2a57e8(0x689)](_0x2a57e8(0x329)+_0x2b538d+'\x22]');_0x54e597[0x0]&&(_0x54e597[0x0][_0x2a57e8(0x39e)]=parseInt(_0x322f00[_0x2a57e8(0x367)])||0x0);}_0x190fe0=!![];}if('changeLabel'in _0x322f00){log(_0x2a57e8(0xabe));if('value'in _0x322f00){log('value\x20there');if(typeof _0x322f00[_0x2a57e8(0x62d)]==_0x2a57e8(0x332)){_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['label']=sanitizeLabel(_0x322f00[_0x2a57e8(0x62d)]);_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc69)][_0x2a57e8(0xb04)]==0x0&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc69)]=![]);applyStyleEffect(_0x2b538d);if(_0x435689[_0x2a57e8(0x781)])updateLabelDirectors(_0x2b538d);else _0x435689['showlabels']&&(_0x190fe0=!![]);}else{_0x435689['rpcs'][_0x2b538d]['label']=![],applyStyleEffect(_0x2b538d);if(_0x435689[_0x2a57e8(0x781)])updateLabelDirectors2(_0x2b538d);else _0x435689[_0x2a57e8(0xba3)]&&(_0x190fe0=!![]);}_0x131bef=!![],pokeIframeAPI('remote-label-changed',_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc69)],_0x2b538d);}}'muteState'in _0x322f00&&(log(_0x322f00),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x904)]=_0x322f00['muteState'],_0x435689[_0x2a57e8(0x9ec)](![],_0x2b538d),_0x435689['rpcs'][_0x2b538d]['stats'][_0x2a57e8(0x7af)]&&(_0x435689['rpcs'][_0x2b538d]['stats'][_0x2a57e8(0x7af)][_0x2a57e8(0xb62)]=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x904)]),(_0x435689['showMuteState']||_0x435689[_0x2a57e8(0x7a5)]||_0x435689[_0x2a57e8(0xa34)]===![])&&_0x435689['roomid']&&(!_0x435689[_0x2a57e8(0x326)]||_0x435689[_0x2a57e8(0x781)])?(!_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)]&&(_0x435689['rpcs'][_0x2b538d]['remoteMuteElement']=getById(_0x2a57e8(0x888))[_0x2a57e8(0x903)](!![]),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)]['id']=_0x2a57e8(0x66f)+_0x2b538d,_0x190fe0=!![]),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x904)]?_0x435689['showMuteState']||_0x435689[_0x2a57e8(0xa34)]===![]?(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0x1e7)),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['remoteMuteElement'][_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0xc09))):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)]['add']('hidden'):_0x435689['showUnMuteState']?(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x8b2)]['classList'][_0x2a57e8(0x701)](_0x2a57e8(0x1e7)),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)]['remove'](_0x2a57e8(0xc09))):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)]['classList']['add']('hidden'),_0x131bef=!![]):_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8b2)][_0x2a57e8(0xab1)]['add']('hidden'),pokeAPI(_0x2a57e8(0x909),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x904)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]),pokeIframeAPI('remote-mute-state',_0x322f00['muteState'],_0x2b538d));if(_0x2a57e8(0xaa9)in _0x322f00){var _0x460111=getChromiumVersion();_0x460111&&(_0x460111<0x50&&(_0x190fe0=!![]));}if(_0x2a57e8(0xc02)in _0x322f00){log(_0x2a57e8(0xa37)+_0x322f00[_0x2a57e8(0xc02)]),_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0xc02)]=_0x322f00[_0x2a57e8(0xc02)];_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['videoMuted']?(!_0x435689[_0x2a57e8(0x7b1)]&&_0x435689[_0x2a57e8(0x9ec)](0x0,_0x2b538d),_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x7ea)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x7ea)][_0x2a57e8(0xc09)]=!![],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x7ea)][_0x2a57e8(0x8d0)]['visibility']=_0x2a57e8(0xc09))):(!_0x435689['switchMode']&&applyQualityDirector(_0x2b538d),updateIncomingVideoElement(_0x2b538d,!![],![]));_0x190fe0=!![];_0x435689[_0x2a57e8(0x781)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc02)]?_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x268)][_0x2a57e8(0xab1)][_0x2a57e8(0xae4)](_0x2a57e8(0xc09)):_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x268)][_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0xc09)));if(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x417)]&&_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0xc02)])setTimeout(function(){activeSpeaker();},0x0);else!_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['videoMuted']&&setTimeout(function(){activeSpeaker();},0x0);_0x131bef=!![],pokeAPI(_0x2a57e8(0xaf7),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['videoMuted'],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x885)]),pokeIframeAPI(_0x2a57e8(0x4b1),_0x322f00[_0x2a57e8(0xc02)],_0x2b538d);}if('screenStopped'in _0x322f00){if(_0x2b538d+_0x2a57e8(0xac8)in _0x435689[_0x2a57e8(0xc6c)]){_0x435689['rpcs'][_0x2b538d+_0x2a57e8(0xac8)]['virtualHangup']=_0x322f00[_0x2a57e8(0x22b)];try{_0x435689[_0x2a57e8(0xc6c)][_0x2b538d+_0x2a57e8(0xac8)][_0x2a57e8(0x8d2)]&&(!(SafariVersion&&SafariVersion>0x10)&&(iPad||iOS)&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d+'_screen'][_0x2a57e8(0x917)][_0x2a57e8(0xc2c)]=!![]));}catch(_0x378a7c){}_0x435689[_0x2a57e8(0x781)]&&(_0x322f00['screenStopped']?getById('container_'+_0x2b538d+'_screen')[_0x2a57e8(0xab1)][_0x2a57e8(0x701)](_0x2a57e8(0x407)):getById('container_'+_0x2b538d+_0x2a57e8(0xac8))['classList'][_0x2a57e8(0xae4)](_0x2a57e8(0x407))),_0x190fe0=!![],_0x131bef=!![];}if(_0x322f00['screenStopped']){try{_0x2b538d in _0x435689[_0x2a57e8(0xc6c)]&&(_0x435689['rpcs'][_0x2b538d][_0x2a57e8(0x4b9)]=![]),_0x2b538d+_0x2a57e8(0xac8)in _0x435689[_0x2a57e8(0xc6c)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d+'_screen']['screenShareState']=![]);}catch(_0x5c50e9){errorlog(_0x5c50e9);}stopScreenWhep(_0x2b538d);}}if(_0x2a57e8(0x4b9)in _0x322f00){let _0x3b6f0b=![],_0x50cdb7=null;try{_0x435689[_0x2a57e8(0xc6c)]&&_0x435689['rpcs'][_0x2b538d+_0x2a57e8(0xac8)]&&(_0x50cdb7=_0x435689[_0x2a57e8(0xc6c)][_0x2b538d+_0x2a57e8(0xac8)],_0x50cdb7[_0x2a57e8(0x857)]&&(_0x3b6f0b=_0x50cdb7[_0x2a57e8(0x857)][_0x2a57e8(0x4df)]()[_0x2a57e8(0x325)](_0x1e65dc=>_0x1e65dc['readyState']==='live')));}catch(_0x32ae89){}_0x322f00['screenShareState']===![]&&_0x3b6f0b&&(_0x322f00[_0x2a57e8(0x4b9)]=!![]),_0x435689[_0x2a57e8(0xc6c)][_0x2b538d]['screenShareState']=_0x322f00['screenShareState'],_0x50cdb7&&_0x322f00['screenShareState']&&(_0x50cdb7[_0x2a57e8(0x4b9)]=!![]),_0x322f00[_0x2a57e8(0x4b9)]?maybeStartScreenWhep(_0x2b538d):stopScreenWhep(_0x2b538d),_0x190fe0=!![],pokeIframeAPI(_0x2a57e8(0x63e),_0x322f00[_0x2a57e8(0x4b9)],_0x2b538d);}if(_0x2a57e8(0x3cb)in _0x322f00){if(!_0x435689[_0x2a57e8(0x781)]){if(_0x2a57e8(0x5df)in _0x322f00){if(_0x435689[_0x2a57e8(0xc19)][_0x2a57e8(0x97c)](_0x2b538d)>=0x0){var _0x52aec7=_0x322f00[_0x2a57e8(0x5df)];if(_0x52aec7===!![])_0x435689[_0x2a57e8(0x5b4)]=_0x322f00[_0x2a57e8(0x3cb)];else _0x52aec7 in _0x435689[_0x2a57e8(0xc6c)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x52aec7]['directorVideoMuted']=_0x322f00[_0x2a57e8(0x3cb)],_0x435689['rpcs'][_0x52aec7][_0x2a57e8(0x5b4)]&&_0x435689[_0x2a57e8(0x9ec)](0x0,_0x52aec7),_0x190fe0=!![]);}}}_0x131bef=!![];}_0x2a57e8(0x8d2)in _0x322f00&&(!_0x435689[_0x2a57e8(0x781)]&&(_0x435689[_0x2a57e8(0xc19)][_0x2a57e8(0x97c)](_0x2b538d)>=0x0&&(_0x2b538d in _0x435689[_0x2a57e8(0xc6c)]&&(_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8d2)]=_0x322f00[_0x2a57e8(0x8d2)],_0x435689[_0x2a57e8(0xc6c)][_0x2b538d][_0x2a57e8(0x8d2)]&&(_0x2b538d in _0x435689['rpcs']&&_0x435689[_0x2a57e8(0x9ec)](0x0,_0x2b538d)),_0x190fe0=!![]))),_0x131bef=!![]);if(_0x2a57e8(0x3b9)in _0x322f00){log('requestFile\x20in\x20reverse');try{_0x435689[_0x2a57e8(0x1f2)](_0x2b538d,_0x322f00['requestFile']);}catch(_0x58588e){errorlog(_0x58588e);}}_0x2a57e8(0x514)in _0x322f00&&remoteStats(_0x322f00,_0x2b538d);if(_0x190fe0)setTimeout(function(){updateMixer(),updateUserList();},0x1);else _0x131bef&&updateUserList();},_0x435689['rpcs'][_0x112b4a]['receiveChannel']['onclose']=()=>{var _0x11898e=_0xd72907;warnlog(_0x11898e(0x34c));};},_0x435689[_0x5dc4d4(0xc6c)][_0x112b4a][_0x5dc4d4(0xa51)]=_0x59694c=>{var _0x3886a3=_0x5dc4d4;warnlog('New\x20ON\x20TRACK\x20event'),_0x435689[_0x3886a3(0x750)](_0x59694c,_0x112b4a);},log(_0x5dc4d4(0x269));},_0x435689['setupScreenShareAddon']=function(_0x471687,_0x4cdbea){var _0x191b0f=_0x120577;log(_0x191b0f(0x752));if(!_0x435689[_0x191b0f(0xc6c)][_0x4cdbea][_0x191b0f(0x771)]){(!(_0x4cdbea+'_screen'in _0x435689[_0x191b0f(0xc6c)])||typeof _0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]!==_0x191b0f(0x31f))&&(_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]={});_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xbb3)]=_0x4cdbea,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea]['screenElement']=createVideoElement(),_0x435689['rpcs'][_0x4cdbea][_0x191b0f(0x771)]['needsLoading']=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea][_0x191b0f(0x771)][_0x191b0f(0xc15)]('loadstart',_0x37fa6d=>{var _0x33261b=_0x191b0f;log('incoming\x20screen\x20share\x20started\x20loading'),_0x37fa6d[_0x33261b(0x5df)]['needsLoading']=![];}),_0x435689[_0x191b0f(0xc6c)][_0x4cdbea][_0x191b0f(0x771)][_0x191b0f(0x200)]=createMediaStream(),_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x917)]=_0x435689[_0x191b0f(0xc6c)][_0x4cdbea][_0x191b0f(0x771)],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x857)]=createMediaStream();_0x435689['rpcs'][_0x4cdbea]['streamID']&&(_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['streamID']=_0x435689['rpcs'][_0x4cdbea][_0x191b0f(0x885)]+':s');try{_0x435689[_0x191b0f(0xc6c)][_0x4cdbea]['screenShareState']=!![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]&&(_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x4b9)]=!![]);}catch(_0x4110c6){}_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x5c2)]={},_0x435689[_0x191b0f(0xc6c)][_0x4cdbea][_0x191b0f(0x5c2)]['Audio_Loudness']=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x860)]=function(){return new Promise((_0x353ab3,_0x5bf8be)=>{_0x353ab3([]);});},_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x543)]=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x6a0)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x228)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x9ee)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['defaultSpeaker']=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x33f)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen']['activelySpeaking']=![],_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)]['loudest']=![],_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xb37)]=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xbec)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x237)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['bandwidth']=-0x1,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0xaac)]=![],_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)]['showDirector']=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x4ff)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x6e1)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x832)]=-0x1,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x358)]=![],_0x435689['rpcs'][_0x4cdbea+'_screen'][_0x191b0f(0x7ea)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x566)]=![],_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x1cd)]=_0x435689[_0x191b0f(0xc6c)][_0x4cdbea][_0x191b0f(0x1cd)]||[],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xc02)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x92f)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x5b4)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['virtualHangup']=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x904)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x8b2)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x131)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x6cf)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x98f)]=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x56a)]=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['mutedStateMixer']=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x187)]=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x2f9)]=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x1a4)]=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x4bd)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x9f5)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['scaleSnap']=![],_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x33d)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['volumeControl']=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x8bd)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x4b9)]=!![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x65b)]=0x64,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x536)]=0x0,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xa6a)]=0x0,_0x435689['rpcs'][_0x4cdbea+'_screen'][_0x191b0f(0x340)]='1',_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen']['opacityMuted']='1',_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x882)]=![],_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x29b)]=0x0,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['label']=![],_0x435689['rpcs'][_0x4cdbea+'_screen']['order']=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xb12)]=null,_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xa8d)]=null,_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)]['inboundAudioPipeline']={},_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xafd)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x964)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x6ae)]=Date[_0x191b0f(0x1ab)](),_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x409)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0xa70)]=![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen']['pseudoguest']=![];(_0x435689[_0x191b0f(0x671)]==0x2||_0x435689[_0x191b0f(0x671)]==0x4)&&(_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x180)]=!![]);_0x435689['rpcs'][_0x4cdbea][_0x191b0f(0x9c6)]?_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen'][_0x191b0f(0x9c6)]=!![]:_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x9c6)]=![];_0x435689['rpcs'][_0x4cdbea][_0x191b0f(0x618)]&&(_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['smallScreen']=![]);if(_0x435689['rpcs'][_0x4cdbea][_0x191b0f(0x9ee)]){_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x9ee)]=_0x435689[_0x191b0f(0xc6c)][_0x4cdbea][_0x191b0f(0x9ee)];try{_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['videoElement']&&_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['videoElement'][_0x191b0f(0x2d5)]&&_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x917)][_0x191b0f(0x2d5)]();}catch(_0x403e16){errorlog(_0x403e16);}}_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x917)]['dataset']['UUID']=_0x4cdbea+_0x191b0f(0xac8),_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x917)]['id']=_0x191b0f(0x9e9)+_0x4cdbea+_0x191b0f(0xac8),_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x885)]&&(_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x917)][_0x191b0f(0xc6b)][_0x191b0f(0x952)]=_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+'_screen']['streamID']),_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x917)]['screenshare']=![],_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)]['voiceMeter']=![],setupIncomingScreenTracking(_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x917)],_0x4cdbea+_0x191b0f(0xac8)),_0x471687['forEach'](function(_0x166c8d){var _0x81caf9=_0x191b0f;_0x435689[_0x81caf9(0xc6c)][_0x4cdbea][_0x81caf9(0x771)][_0x81caf9(0x200)][_0x81caf9(0x2e6)](_0x166c8d),_0x435689['rpcs'][_0x4cdbea+_0x81caf9(0xac8)][_0x81caf9(0x857)]['addTrack'](_0x166c8d);}),_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x917)][_0x191b0f(0x317)]=!![],_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)]['videoElement']['setAttribute'](_0x191b0f(0xa0c),''),mediaSourceUpdated(_0x4cdbea+_0x191b0f(0xac8),_0x435689[_0x191b0f(0xc6c)][_0x4cdbea+_0x191b0f(0xac8)][_0x191b0f(0x885)]);}else _0x471687['forEach'](function(_0x5f3d8d){var _0x273572=_0x191b0f,_0x4e9ab4=![];_0x435689[_0x273572(0xc6c)][_0x4cdbea]['screenElement']['srcObject']['getTracks']()[_0x273572(0x3d5)](function(_0x23a4a3){var _0x7d3101=_0x273572;_0x23a4a3['id']==_0x5f3d8d['id']&&_0x23a4a3[_0x7d3101(0x2fe)]==_0x5f3d8d['kind']&&(_0x4e9ab4=!![]);});!_0x4e9ab4&&_0x435689['rpcs'][_0x4cdbea]['screenElement'][_0x273572(0x200)][_0x273572(0x2e6)](_0x5f3d8d);var _0x4e9ab4=![];_0x435689['rpcs'][_0x4cdbea+'_screen'][_0x273572(0x857)][_0x273572(0x81f)]()[_0x273572(0x3d5)](function(_0x52622d){var _0x4bf66c=_0x273572;_0x52622d['id']==_0x5f3d8d['id']&&_0x52622d[_0x4bf66c(0x2fe)]==_0x5f3d8d['kind']&&(_0x4e9ab4=!![]);}),!_0x4e9ab4&&_0x435689['rpcs'][_0x4cdbea+'_screen'][_0x273572(0x857)]['addTrack'](_0x5f3d8d);});try{_0x435689['rpcs'][_0x4cdbea][_0x191b0f(0x4b9)]=!![];}catch(_0x28abde){}try{_0x435689['rpcs'][_0x4cdbea+_0x191b0f(0xac8)]&&(_0x435689['rpcs'][_0x4cdbea+'_screen'][_0x191b0f(0x4b9)]=!![]);}catch(_0x20bed6){}},_0x435689;}());var meshcastServer=![],meshcastServerList=![];const meshcastPingResults=new Map();function selectMeshcast(_0x812f62){var _0x293dd6=_0x380373;meshcastServer={};const _0x5eaba3=_0x812f62[_0x293dd6(0xbcb)][_0x812f62[_0x293dd6(0x6c4)]];meshcastServer[_0x293dd6(0x8e5)]=_0x5eaba3[_0x293dd6(0x8e5)],meshcastServer['code']=_0x5eaba3[_0x293dd6(0x777)]||null,meshcastServer['id']=_0x5eaba3['id']||null;}function _0x4fa9(_0x2bf752,_0x59aff2){var _0x5ad5ca=_0x5ad5();return _0x4fa9=function(_0x4fa91b,_0x23d118){_0x4fa91b=_0x4fa91b-0x11d;var _0x814fc9=_0x5ad5ca[_0x4fa91b];return _0x814fc9;},_0x4fa9(_0x2bf752,_0x59aff2);}async function pingMeshcast(_0xe3781e,_0x3966fa){return new Promise(_0x2b2f94=>{var _0x36675f=_0x4fa9;const _0x35ebe5=new XMLHttpRequest();_0x35ebe5[_0x36675f(0x356)]=function(){var _0x1b8f27=_0x36675f;const _0xa1a3fa=parseFloat(this[_0x1b8f27(0x93d)]);if(_0xa1a3fa>=0x0){meshcastPingResults[_0x1b8f27(0x50e)](_0xe3781e['id']||_0xe3781e[_0x1b8f27(0x777)],{'load':_0xa1a3fa,'failed':![],'option':_0xe3781e});if(_0xa1a3fa>0x46)_0xe3781e[_0x1b8f27(0x5be)]+='\x20(full)';else{if(_0xa1a3fa>0x28)_0xe3781e[_0x1b8f27(0x5be)]+='\x20(fair)';else{if(_0xa1a3fa>0xa)_0xe3781e[_0x1b8f27(0x5be)]+=_0x1b8f27(0x81c);else _0xa1a3fa>0x0?_0xe3781e[_0x1b8f27(0x5be)]+=_0x1b8f27(0x246):handleMeshcastFailure(_0xe3781e);}}_0x2b2f94(!![]);}else handleMeshcastFailure(_0xe3781e),_0x2b2f94(![]);},_0x35ebe5['onerror']=()=>{handleMeshcastFailure(_0xe3781e),_0x2b2f94(![]);},_0x35ebe5[_0x36675f(0x1f4)]=0x7d0,_0x35ebe5[_0x36675f(0xc38)]=()=>{var _0x5371b0=_0x36675f;handleMeshcastFailure(_0xe3781e,_0x5371b0(0x1f4)),_0x2b2f94(![]);},_0x35ebe5[_0x36675f(0x379)](_0x36675f(0xa78),_0x3966fa,!![]),_0x35ebe5[_0x36675f(0xac5)]();});}function handleMeshcastFailure(_0x58172c,_0x1cbeb8='fail'){var _0x322a25=_0x380373;meshcastPingResults['set'](_0x58172c['id']||_0x58172c[_0x322a25(0x777)],{'load':Infinity,'failed':!![],'option':_0x58172c}),_0x58172c[_0x322a25(0x6cd)]=!![],_0x58172c[_0x322a25(0x5be)]+='\x20('+_0x1cbeb8+')';}function sortMeshcastOptions(){var _0x3d0639=_0x380373;const _0x143d52=document['getElementById'](_0x3d0639(0x564)),_0x17e0d6=Array[_0x3d0639(0x1f9)](_0x143d52[_0x3d0639(0xbcb)]);_0x17e0d6[_0x3d0639(0xb9a)]((_0x457c33,_0x501f95)=>{var _0x6d0bf8=_0x3d0639;const _0x37af5c=meshcastPingResults[_0x6d0bf8(0xa1b)](_0x457c33['id']||_0x457c33[_0x6d0bf8(0x777)])||{'load':Infinity,'failed':!![]},_0x4e421a=meshcastPingResults['get'](_0x501f95['id']||_0x501f95[_0x6d0bf8(0x777)])||{'load':Infinity,'failed':!![]};if(_0x37af5c[_0x6d0bf8(0x35e)]&&!_0x4e421a[_0x6d0bf8(0x35e)])return 0x1;if(!_0x37af5c[_0x6d0bf8(0x35e)]&&_0x4e421a['failed'])return-0x1;const _0x52317a=meshcastServerList[_0x6d0bf8(0x6f7)](_0x30803a=>(_0x30803a['id']||_0x30803a[_0x6d0bf8(0x777)])===(_0x457c33['id']||_0x457c33[_0x6d0bf8(0x777)])),_0x12260e=meshcastServerList[_0x6d0bf8(0x6f7)](_0x5b2b73=>(_0x5b2b73['id']||_0x5b2b73['code'])===(_0x501f95['id']||_0x501f95['code'])),_0x1a6b0f=_0x37af5c['load']+(_0x52317a['delta']||0x0)/0x28,_0x434adf=_0x4e421a['load']+(_0x12260e[_0x6d0bf8(0x770)]||0x0)/0x28;return _0x457c33[_0x6d0bf8(0xc6b)][_0x6d0bf8(0x2e7)]=_0x1a6b0f,_0x501f95['dataset'][_0x6d0bf8(0x2e7)]=_0x434adf,_0x457c33[_0x6d0bf8(0xc6b)]['load']=_0x37af5c[_0x6d0bf8(0x27a)],_0x501f95[_0x6d0bf8(0xc6b)][_0x6d0bf8(0x27a)]=_0x4e421a['load'],_0x457c33[_0x6d0bf8(0xc6b)][_0x6d0bf8(0x770)]=(_0x52317a['delta']||0x0)/0x28,_0x501f95[_0x6d0bf8(0xc6b)]['delta']=(_0x12260e[_0x6d0bf8(0x770)]||0x0)/0x28,_0x1a6b0f-_0x434adf;}),_0x17e0d6[_0x3d0639(0x3d5)](_0x4d4ce9=>_0x143d52[_0x3d0639(0x912)](_0x4d4ce9));}function selectBestMeshcastServer(){var _0x2680bd=_0x380373;const _0x4ca6fd=document[_0x2680bd(0x54c)](_0x2680bd(0x564));let _0x7c53d3=Array[_0x2680bd(0x1f9)](_0x4ca6fd[_0x2680bd(0xbcb)])[_0x2680bd(0x6f7)](_0x44543a=>_0x44543a[_0x2680bd(0x14b)]&&!_0x44543a['disabled']);!_0x7c53d3&&(_0x7c53d3=Array[_0x2680bd(0x1f9)](_0x4ca6fd['options'])[_0x2680bd(0x6f7)](_0x45500b=>!_0x45500b[_0x2680bd(0x6cd)])),_0x7c53d3?(_0x7c53d3[_0x2680bd(0x898)]=!![],selectMeshcast(_0x4ca6fd)):console[_0x2680bd(0x4bc)](_0x2680bd(0x39c));}async function queryMeshcastServers(_0x183c17=![]){var _0x1dee18=_0x380373;try{const _0x39b57e=new Date(),_0x212cef=urlParams[_0x1dee18(0x1a5)]('tz')?parseInt(urlParams[_0x1dee18(0xa1b)]('tz')):_0x39b57e['getTimezoneOffset'](),_0x49e242=await fetch(_0x1dee18(0xb24)+Date[_0x1dee18(0x1ab)]()),_0x3f87cf=await _0x49e242[_0x1dee18(0x415)]();meshcastServerList=_0x3f87cf;const _0x15f9a3=typeof session[_0x1dee18(0xc65)]===_0x1dee18(0x332)?session['meshcast'][_0x1dee18(0x8f0)]():session[_0x1dee18(0xc65)],_0x5c8cd9=[_0x1dee18(0x6b4),'audio','video'];let _0x3a952a=session['meshcastCode']||null;typeof _0x3a952a==='string'&&(_0x3a952a=_0x3a952a['trim'](),!_0x3a952a[_0x1dee18(0xb04)]&&(_0x3a952a=null));if(!_0x3a952a&&typeof _0x15f9a3==='string'&&_0x15f9a3[_0x1dee18(0xb04)]){const _0x455908=_0x15f9a3[_0x1dee18(0xa6f)]();!_0x5c8cd9['includes'](_0x455908)&&(_0x3a952a=_0x15f9a3);}if(_0x3a952a){const _0x383f49=_0x3f87cf[_0x1dee18(0x325)](_0x173b6d=>_0x173b6d[_0x1dee18(0x777)]&&_0x173b6d['code']===_0x3a952a||_0x173b6d['id']&&_0x173b6d['id']===_0x3a952a);if(!_0x383f49){const _0x52d5d4=_0x3a952a[_0x1dee18(0x49c)]('.');let _0x118214;_0x52d5d4?_0x118214=_0x3a952a['startsWith'](_0x1dee18(0x6bd))?_0x3a952a['replace'](/\/$/,''):_0x1dee18(0x91e)+_0x3a952a[_0x1dee18(0x586)](/\/$/,''):_0x118214='https://'+_0x3a952a+_0x1dee18(0x650);const _0x260737={'id':_0x3a952a,'code':_0x3a952a,'url':_0x118214,'label':_0x1dee18(0x8a4)+_0x3a952a,'tz':_0x212cef,'penalty':-0x989680,'preferred':!![]};meshcastServerList[_0x1dee18(0x4a0)](_0x260737);}}meshcastServerList=meshcastServerList['map'](_0x4aa92b=>{var _0x5ac36b=_0x1dee18;let _0x3e4931=Math[_0x5ac36b(0x92b)](_0x4aa92b['tz']-_0x212cef);Math[_0x5ac36b(0x92b)](_0x3e4931-0x3c*0x18)<_0x3e4931&&(_0x3e4931=Math[_0x5ac36b(0x92b)](_0x3e4931-0x3c*0x18));_0x4aa92b[_0x5ac36b(0x770)]=_0x3e4931+(_0x4aa92b[_0x5ac36b(0x9db)]||0x0);if(_0x3a952a){const _0x3715ff=_0x4aa92b[_0x5ac36b(0x777)]&&_0x4aa92b[_0x5ac36b(0x777)]===_0x3a952a||_0x4aa92b['id']&&_0x4aa92b['id']===_0x3a952a;!_0x3715ff&&(_0x4aa92b[_0x5ac36b(0x770)]+=0xa1220),_0x4aa92b['preferred']=!!_0x3715ff;}else _0x4aa92b['preferred']=!!(session[_0x5ac36b(0x5ec)]&&(_0x4aa92b['id']===session[_0x5ac36b(0x5ec)]||session[_0x5ac36b(0x5ec)]===_0x4aa92b[_0x5ac36b(0x777)]));return _0x4aa92b;})[_0x1dee18(0xb9a)]((_0x2a4171,_0x16252d)=>_0x2a4171[_0x1dee18(0x770)]-_0x16252d[_0x1dee18(0x770)]);const _0x565531=meshcastServerList[_0x1dee18(0x2da)](_0x17a6c3=>{var _0x2abf4b=_0x1dee18;const _0x26e6d5=document[_0x2abf4b(0x6f0)](_0x2abf4b(0xba6));if(_0x17a6c3[_0x2abf4b(0x777)])_0x26e6d5[_0x2abf4b(0x777)]=_0x17a6c3[_0x2abf4b(0x777)];if(_0x17a6c3['id'])_0x26e6d5['id']=_0x17a6c3['id'];return _0x26e6d5[_0x2abf4b(0x8e5)]=_0x17a6c3[_0x2abf4b(0x8e5)],_0x26e6d5[_0x2abf4b(0x5be)]=_0x17a6c3[_0x2abf4b(0xc69)],_0x26e6d5[_0x2abf4b(0x14b)]=_0x17a6c3[_0x2abf4b(0x14b)],document[_0x2abf4b(0x54c)]('edgelist')[_0x2abf4b(0x912)](_0x26e6d5),_0x26e6d5;}),_0x2db3e6=meshcastServerList[_0x1dee18(0x2da)]((_0x40a544,_0x25a582)=>pingMeshcast(_0x565531[_0x25a582],_0x40a544[_0x1dee18(0x8e5)]+_0x1dee18(0xb43)));await Promise['all'](_0x2db3e6),sortMeshcastOptions(),selectBestMeshcastServer(),_0x183c17&&_0x183c17(),session['director']&&!session['cleanOutput']&&!session[_0x1dee18(0xc33)]&&document['getElementById']('meshcastMenu')[_0x1dee18(0xab1)][_0x1dee18(0xae4)](_0x1dee18(0xc09));}catch(_0x34c9b8){console[_0x1dee18(0x4bc)]('Error\x20fetching\x20meshcast\x20servers:',_0x34c9b8);}}async function meshcast2(){var _0x5e74e1=_0x380373;if(!session[_0x5e74e1(0x93b)])return;if(session[_0x5e74e1(0x50a)]!==![])return;if(!session[_0x5e74e1(0x9be)]&&!session[_0x5e74e1(0x917)][_0x5e74e1(0x200)])return;const _0x301c90=_0x5e74e1(0x25f),_0x31c939=session['meshcast2']===!![]||session[_0x5e74e1(0x93b)]===_0x5e74e1(0x6b4)||session['meshcast2']===_0x5e74e1(0x505)||session[_0x5e74e1(0x7ad)]&&!session[_0x5e74e1(0xc4b)];let _0x3e43ce=null;if(_0x31c939)try{const _0x540bdf=await fetch(_0x301c90+_0x5e74e1(0xa73),{'method':'POST','headers':{'Content-Type':_0x5e74e1(0x63f)},'body':JSON['stringify']({'duration_minutes':0x78})}),_0x27c4e8=await _0x540bdf[_0x5e74e1(0x415)]()[_0x5e74e1(0x501)](()=>({}));if(!_0x540bdf['ok']){session[_0x5e74e1(0xc4b)]=![],session['meshcast2ErrorHandling']=!![],session['meshcast2LastError']=_0x27c4e8&&_0x27c4e8[_0x5e74e1(0x777)]?_0x27c4e8['code']:_0x5e74e1(0x6ba);if(!session[_0x5e74e1(0x326)]){if(session[_0x5e74e1(0x1a0)]===_0x5e74e1(0xa28))promptAlt(_0x5e74e1(0xa3c),![],![],![],0x5);else session[_0x5e74e1(0x1a0)]==='QUOTA_EXCEEDED'?promptAlt(_0x5e74e1(0xb2e),![],![],![],0x5):promptAlt(_0x5e74e1(0xa65),![],![],![],0x5);}session[_0x5e74e1(0x7ad)]=![];return;}_0x3e43ce=_0x27c4e8[_0x5e74e1(0x470)],session[_0x5e74e1(0xc4b)]=!![],session[_0x5e74e1(0x1a0)]=null,session[_0x5e74e1(0x7ad)]=![];}catch(_0x311e8a){errorlog(_0x311e8a);return;}else{if(typeof session[_0x5e74e1(0x93b)]===_0x5e74e1(0x332)&&session['meshcast2']['trim']()['length']){if(session['meshcast2']['trim']()[_0x5e74e1(0xa6f)]()==='anon')return session['meshcast2']='any',meshcast2();_0x3e43ce=session[_0x5e74e1(0x93b)]['trim'](),session[_0x5e74e1(0xc4b)]=_0x3e43ce[_0x5e74e1(0x8da)](_0x5e74e1(0xa53)),session[_0x5e74e1(0x1a0)]=null;}else return;}if(!_0x3e43ce)return;const _0x39fa91=_0x301c90+'/api/gateway/whip/'+_0x3e43ce,_0x50ea58=_0x301c90+_0x5e74e1(0x40d)+_0x3e43ce;session['whipOutput']=_0x39fa91,session[_0x5e74e1(0x50a)]={'type':_0x5e74e1(0x3d2),'url':_0x50ea58,'token':_0x3e43ce,'media':_0x5e74e1(0x596),'started':![]};if(session[_0x5e74e1(0x792)]){const _0xf0c9db=_0x3e43ce+'_s';session[_0x5e74e1(0x7c2)]=_0x301c90+'/api/gateway/whip/'+_0xf0c9db,session[_0x5e74e1(0x853)]={'type':_0x5e74e1(0x3d2),'url':_0x301c90+_0x5e74e1(0x40d)+_0xf0c9db,'token':_0xf0c9db,'media':_0x5e74e1(0xadf),'started':![]},session['screenShareState']&&whipOutScreen();}else session[_0x5e74e1(0x7c2)]=![],session[_0x5e74e1(0x853)]=![];whipOut();}async function meshcast(_0x38d728=![]){var _0x355e81=_0x380373;if(!session[_0x355e81(0xc65)])return;if(_0x38d728){await queryMeshcastServers();return;}if(session[_0x355e81(0x50a)]!==![])return;if(!session[_0x355e81(0x9be)]&&!session[_0x355e81(0x917)][_0x355e81(0x200)])return;session[_0x355e81(0x50a)]=null;const _0x19414d=[],_0x156a5d=session[_0x355e81(0xa1a)](0xe),_0x27aadd=_0x156a5d+'_s';async function _0x18ebf3(){var _0x11617f=_0x355e81;document['getElementById'](_0x11617f(0x564))[_0x11617f(0x6cd)]=!![],document[_0x11617f(0x54c)](_0x11617f(0x564))[_0x11617f(0x967)]=_0x11617f(0x481);!meshcastServer&&meshcastServerList&&meshcastServerList[_0x11617f(0xb04)]&&(meshcastServer=meshcastServerList['shift']());if(!meshcastServer){handleMeshcastError();return;}if(meshcastServer['id']){if(session[_0x11617f(0x509)]&&session[_0x11617f(0x8c9)]&&meshcastServer&&meshcastServer[_0x11617f(0x8e5)])try{var _0x1e0dfa=new URL(meshcastServer[_0x11617f(0x8e5)])[_0x11617f(0x879)];session[_0x11617f(0x8c9)][_0x11617f(0x3ce)]=_0x11617f(0xc65),_0x1e0dfa&&!session['qosData'][_0x11617f(0xb01)][_0x11617f(0x49c)](_0x1e0dfa)&&session[_0x11617f(0x8c9)][_0x11617f(0xb01)][_0x11617f(0x35c)](_0x1e0dfa);}catch(_0x3a7eab){}session[_0x11617f(0x981)]?(session[_0x11617f(0x537)]=meshcastServer[_0x11617f(0x8e5)]+'/'+_0x156a5d+_0x11617f(0xc2d),session['whipoutSettings']={'type':'whep','url':meshcastServer[_0x11617f(0x8e5)]+'/'+_0x156a5d+_0x11617f(0xba5),'token':_0x156a5d,'media':'primary','started':![]},whipOut()):(session[_0x11617f(0x537)]=![],session[_0x11617f(0x50a)]=![]),session[_0x11617f(0x792)]?(session['whipOutputScreen']=meshcastServer['url']+'/'+_0x27aadd+_0x11617f(0xc2d),session['whipoutScreenSettings']={'type':_0x11617f(0x3d2),'url':meshcastServer[_0x11617f(0x8e5)]+'/'+_0x27aadd+_0x11617f(0xba5),'token':_0x27aadd,'media':_0x11617f(0xadf),'started':![]},session[_0x11617f(0x4b9)]&&whipOutScreen()):(session[_0x11617f(0x7c2)]=![],session[_0x11617f(0x853)]=![]);}}!meshcastServerList?await queryMeshcastServers(_0x18ebf3):await _0x18ebf3();}function handleMeshcastError(){var _0x5eae35=_0x380373;errorlog(_0x5eae35(0x6b9));if(!session[_0x5eae35(0x326)]){const _0x484903=window[_0x5eae35(0x652)][_0x5eae35(0x9bc)];_0x484903[_0x5eae35(0x49c)]('?')?warnUser(_0x5eae35(0xbaa)+(_0x5eae35(0x86a)+_0x484903+_0x5eae35(0x552)),![],![]):warnUser(_0x5eae35(0x375));}}function whepSettingsHasStarted(_0x2db684){var _0x3e26ce=_0x380373;if(!_0x2db684||!(_0x3e26ce(0x838)in _0x2db684))return![];const _0x10e019=_0x2db684['started'];if(typeof _0x10e019==='number')return _0x10e019>0x0;if(typeof _0x10e019==='string'){const _0x4ec17f=_0x10e019[_0x3e26ce(0x8f0)]();if(!_0x4ec17f)return![];if(_0x4ec17f==='0')return![];if(_0x4ec17f[_0x3e26ce(0xa6f)]()===_0x3e26ce(0x914))return![];return!![];}if(_0x10e019===!![])return!![];return![];}function whepSettingsMarker(_0x677d47){var _0xd8c572=_0x380373;if(!_0x677d47)return null;if(whepSettingsHasStarted(_0x677d47))return String(_0x677d47['started']);const _0x47c9a6=typeof _0x677d47[_0xd8c572(0x8e5)]===_0xd8c572(0x332)?_0x677d47[_0xd8c572(0x8e5)]:'',_0x1bd3a4=typeof _0x677d47['token']===_0xd8c572(0x332)?_0x677d47['token']:'';if(_0x47c9a6||_0x1bd3a4)return _0x47c9a6+'|'+_0x1bd3a4;return null;}async function whepWatch(_0x4261f4,_0x9db958){var _0x3c0231=_0x380373;if(session[_0x3c0231(0xb6c)])return;console['log'](_0x9db958);if(_0x9db958[_0x3c0231(0x2dd)]==_0x3c0231(0xc65))meshcastWatch(_0x4261f4,_0x9db958);else{if(_0x9db958[_0x3c0231(0x2dd)]==_0x3c0231(0x3d2)){if(_0x9db958&&_0x9db958[_0x3c0231(0x8e5)]){const _0x260f20=_0x9db958['media']===_0x3c0231(0xadf);if(_0x260f20){const _0x1263c0=_0x4261f4+_0x3c0231(0xac8),_0x4c90ee=Object[_0x3c0231(0xa18)]({},_0x9db958);try{!(_0x1263c0 in session[_0x3c0231(0xc6c)])&&(session[_0x3c0231(0xc6c)][_0x1263c0]={});const _0x527040=session['rpcs'][_0x1263c0];_0x527040[_0x3c0231(0xbb3)]=_0x4261f4;const _0x4b6f19=_0x4c90ee[_0x3c0231(0x8e5)]||'',_0x3d71d1=_0x4c90ee[_0x3c0231(0x377)]||'',_0x53d131=whepSettingsMarker(_0x4c90ee),_0x408124=whepSettingsHasStarted(_0x4c90ee),_0x22c045=_0x527040[_0x3c0231(0x9cd)]||'',_0x3ad197=_0x527040[_0x3c0231(0x13f)]||'',_0x1e4c2c=_0x527040[_0x3c0231(0x53e)]||null,_0x3db68c=_0x22c045!==_0x4b6f19||_0x3ad197!==_0x3d71d1||_0x1e4c2c!==_0x53d131;if(_0x3db68c&&_0x527040[_0x3c0231(0xa9e)]){try{_0x527040[_0x3c0231(0x3d2)]&&_0x527040[_0x3c0231(0x3d2)][_0x3c0231(0x757)]&&_0x527040[_0x3c0231(0x3d2)][_0x3c0231(0x757)]();}catch(_0x57174e){warnlog(_0x57174e);}_0x527040[_0x3c0231(0x3d2)]=null,_0x527040['whepRequested']=![],_0x527040[_0x3c0231(0x7bd)]=![],_0x527040[_0x3c0231(0xc07)]=null;}_0x527040[_0x3c0231(0xa20)]=_0x4c90ee,_0x527040['lastWhepUrl']=_0x4b6f19,_0x527040[_0x3c0231(0x13f)]=_0x3d71d1,_0x527040['lastWhepMarker']=_0x53d131,_0x527040[_0x3c0231(0x48b)]=_0x53d131;_0x408124?_0x527040['pendingWhepStarted']=!![]:delete _0x527040[_0x3c0231(0x46d)];_0x527040[_0x3c0231(0x667)]=![],_0x527040[_0x3c0231(0x5c2)]=_0x527040[_0x3c0231(0x5c2)]||{};typeof _0x527040[_0x3c0231(0x4b9)]===_0x3c0231(0xba0)&&(_0x527040[_0x3c0231(0x4b9)]=!!(session[_0x3c0231(0xc6c)][_0x4261f4]&&session[_0x3c0231(0xc6c)][_0x4261f4][_0x3c0231(0x4b9)]));if(session['rpcs'][_0x4261f4]&&session[_0x3c0231(0xc6c)][_0x4261f4][_0x3c0231(0x885)]){const _0x10b6d3=session['rpcs'][_0x4261f4][_0x3c0231(0x885)],_0x43b6f3=_0x10b6d3+':s';_0x527040[_0x3c0231(0x885)]!==_0x43b6f3&&(_0x527040['streamID']=_0x43b6f3),_0x527040[_0x3c0231(0x917)]&&(_0x527040[_0x3c0231(0x917)]['dataset']['sid']=_0x43b6f3),session[_0x3c0231(0xc6c)][_0x4261f4][_0x3c0231(0x771)]&&session[_0x3c0231(0xc6c)][_0x4261f4][_0x3c0231(0x771)]!==_0x527040['videoElement']&&(session['rpcs'][_0x4261f4]['screenElement'][_0x3c0231(0xc6b)]['sid']=_0x43b6f3);}_0x408124&&session['rpcs'][_0x4261f4]&&(session[_0x3c0231(0xc6c)][_0x4261f4]['screenShareState']=!![]),_0x408124&&(_0x527040['screenShareState']=!![]);}catch(_0x29d8a9){}maybeStartScreenWhep(_0x4261f4);}else _0x9db958['token']?whepIn(_0x9db958[_0x3c0231(0x8e5)],_0x9db958[_0x3c0231(0x377)],_0x4261f4):whepIn(_0x9db958[_0x3c0231(0x8e5)],![],_0x4261f4);}}}}function maybeStartScreenWhep(_0x2c5219){var _0xbf5eca=_0x380373;try{if(!session||!session[_0xbf5eca(0xc6c)])return;const _0x691c56=_0x2c5219+_0xbf5eca(0xac8),_0x39d31e=session[_0xbf5eca(0xc6c)][_0x691c56];if(!_0x39d31e||!_0x39d31e[_0xbf5eca(0xa20)])return;const _0x1053f3=session[_0xbf5eca(0xc6c)][_0x2c5219]||null,_0x49552d=_0x39d31e[_0xbf5eca(0xa20)],{url:_0xc140fe,token:_0x548ba4}=_0x49552d;if(!_0xc140fe)return;const _0x59978f=_0x39d31e[_0xbf5eca(0x48b)]||whepSettingsMarker(_0x49552d),_0xe37551=_0x39d31e[_0xbf5eca(0xc07)]||null,_0x905994=whepSettingsHasStarted(_0x49552d)||!!_0x39d31e[_0xbf5eca(0x46d)];_0x905994&&(_0x1053f3&&(typeof _0x1053f3[_0xbf5eca(0x7fc)]===_0xbf5eca(0xba0)&&(_0x1053f3[_0xbf5eca(0x7fc)]=_0x1053f3[_0xbf5eca(0x9c6)]),!_0x1053f3['smallScreen']?(_0x1053f3[_0xbf5eca(0x9c6)]=!![],_0x1053f3['__whepAutoSmallScreen']=!![]):_0x1053f3['__whepAutoSmallScreen']=![]),_0x1053f3&&(_0x1053f3[_0xbf5eca(0x4b9)]=!![]),_0x39d31e[_0xbf5eca(0x4b9)]=!![],_0x1053f3&&_0x1053f3[_0xbf5eca(0x618)]&&(_0x39d31e[_0xbf5eca(0x9c6)]=![]));if(!_0x905994&&(!_0x1053f3||!_0x1053f3['screenShareState']))return;if(_0x39d31e['whepRequested']){if(_0x59978f&&_0xe37551&&_0x59978f!==_0xe37551){try{_0x39d31e[_0xbf5eca(0x3d2)]&&_0x39d31e[_0xbf5eca(0x3d2)][_0xbf5eca(0x757)]&&_0x39d31e[_0xbf5eca(0x3d2)]['close']();}catch(_0x4af008){warnlog(_0x4af008);}_0x39d31e[_0xbf5eca(0x3d2)]=null,_0x39d31e['whepRequested']=![],_0x39d31e[_0xbf5eca(0x7bd)]=![],_0x39d31e[_0xbf5eca(0xc07)]=null;}else return;}if(_0x39d31e['whepRequested'])return;_0x39d31e['whepRequested']=!![],_0x39d31e[_0xbf5eca(0x667)]=![],_0x39d31e[_0xbf5eca(0x4b9)]=!![],_0x39d31e['activeWhepMarker']=_0x59978f||(_0x905994?_0xbf5eca(0x985):null),_0x548ba4?whepIn(_0xc140fe,_0x548ba4,_0x691c56):whepIn(_0xc140fe,![],_0x691c56);}catch(_0x550c73){errorlog(_0x550c73);}}function stopScreenWhep(_0x52c2b8){var _0x19312b=_0x380373;try{const _0x495ced=_0x52c2b8+_0x19312b(0xac8),_0x11f08b=session[_0x19312b(0xc6c)]&&session[_0x19312b(0xc6c)][_0x495ced]?session[_0x19312b(0xc6c)][_0x495ced]:null;if(!_0x11f08b)return;_0x11f08b['pendingWhepSettings']=null,_0x11f08b[_0x19312b(0xa9e)]=![],_0x11f08b['suppressReconnect']=!![],_0x11f08b[_0x19312b(0x7bd)]=![],_0x11f08b[_0x19312b(0x4b9)]=![],_0x11f08b['lastWhepUrl']=null,_0x11f08b[_0x19312b(0x13f)]=null,_0x11f08b[_0x19312b(0x53e)]=null,_0x11f08b[_0x19312b(0x48b)]=null,_0x11f08b[_0x19312b(0xc07)]=null,delete _0x11f08b[_0x19312b(0x46d)];try{session[_0x19312b(0xc6c)][_0x52c2b8]&&(session[_0x19312b(0xc6c)][_0x52c2b8]['screenShareState']=![]);}catch(_0x36cec6){errorlog(_0x36cec6);}try{session['rpcs'][_0x52c2b8]&&typeof session[_0x19312b(0xc6c)][_0x52c2b8]['__whepPrevSmallScreen']!==_0x19312b(0xba0)&&(session['rpcs'][_0x52c2b8][_0x19312b(0x9c6)]=session[_0x19312b(0xc6c)][_0x52c2b8][_0x19312b(0x7fc)],delete session[_0x19312b(0xc6c)][_0x52c2b8][_0x19312b(0x7fc)],delete session[_0x19312b(0xc6c)][_0x52c2b8][_0x19312b(0x618)]);}catch(_0x475e39){errorlog(_0x475e39);}try{_0x11f08b[_0x19312b(0x3d2)]&&_0x11f08b[_0x19312b(0x3d2)][_0x19312b(0x757)]&&_0x11f08b['whep'][_0x19312b(0x757)]();}catch(_0x547cce){warnlog(_0x547cce);}_0x11f08b[_0x19312b(0x3d2)]=null;}catch(_0xcded05){errorlog(_0xcded05);}}async function meshcastWatch(_0x54e978,_0x1df101){var _0x3298b8=_0x380373;console[_0x3298b8(0x2c6)](_0x3298b8(0xb9b));!(_0x54e978 in session['rpcs'])&&(session[_0x3298b8(0xc6c)][_0x54e978]={},session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x5c2)]={},session[_0x3298b8(0xc6c)][_0x54e978]['allowGraphs']=![],session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x9ee)]=![],session['rpcs'][_0x54e978]['inboundAudioPipeline']={},session[_0x3298b8(0xc6c)][_0x54e978]['channelOffset']=![],session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x6e1)]=![],session['rpcs'][_0x54e978][_0x3298b8(0x409)]=![],session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x38e)]=![],session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x417)]=![],session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x2f9)]=null,session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x1a4)]=null,session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x33f)]=![],session['rpcs'][_0x54e978][_0x3298b8(0x131)]=![],session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x6cf)]=![],session[_0x3298b8(0xc6c)][_0x54e978]['buffer']=![],session[_0x3298b8(0xc6c)][_0x54e978]['manualBandwidth']=![],session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x543)]=null,session['rpcs'][_0x54e978][_0x3298b8(0x9c6)]=![],session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x15d)]=![],errorlog(_0x3298b8(0x2c4)));var _0x27dece=!![],_0x3e9e4c=!![];if(session[_0x3298b8(0x316)]!==![]&&!session[_0x3298b8(0x316)]['includes'](session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x885)]))_0x27dece=![];else session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x409)]&&!session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x409)]['video']&&(_0x27dece=![]);if(session[_0x3298b8(0x333)]!==![]&&!session[_0x3298b8(0x333)][_0x3298b8(0x49c)](session['rpcs'][_0x54e978]['streamID']))_0x3e9e4c=![];else{if(session['excludeaudio']&&session['excludeaudio'][_0x3298b8(0x49c)](session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x885)]))_0x3e9e4c=![];else session['rpcs'][_0x54e978][_0x3298b8(0x409)]&&!session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x409)]['audio']&&(_0x3e9e4c=![]);}if(!_0x3e9e4c&&!_0x27dece){errorlog(_0x3298b8(0x581));return;}disableQualityDirector(_0x54e978);!session['configuration']&&await chooseBestTURN();var _0x282c6d={...session[_0x3298b8(0x238)]};_0x282c6d[_0x3298b8(0x570)]&&delete _0x282c6d[_0x3298b8(0x570)];_0x282c6d[_0x3298b8(0x8e7)]&&delete _0x282c6d[_0x3298b8(0x8e7)];session['encodedInsertableStreams']&&console[_0x3298b8(0x4bc)](_0x3298b8(0xb83));try{session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x3d2)]=new RTCPeerConnection(_0x282c6d);try{session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x3d2)][_0x3298b8(0x626)]=_0x51fd34=>{warnlog(_0x51fd34);};}catch(_0xed2b9c){warnlog(_0xed2b9c);}}catch(_0x308f4c){!session[_0x3298b8(0x326)]&&warnUser(_0x3298b8(0x1f0));}session[_0x3298b8(0xc6c)][_0x54e978][_0x3298b8(0x3d2)]['ontrack']=function(_0x57ed99){var _0xfa1ec2=_0x3298b8;session[_0xfa1ec2(0x750)](_0x57ed99,_0x54e978);let _0x425b52=null;if(_0x57ed99[_0xfa1ec2(0x796)]&&_0x57ed99['streams'][0x0])try{let _0x1394b0=_0x57ed99[_0xfa1ec2(0x796)][0x0];_0x425b52=_0x1394b0[_0xfa1ec2(0x4df)]()[0x0];}catch(_0x1077b2){}else _0x57ed99[_0xfa1ec2(0xb26)]&&_0x57ed99['track'][_0xfa1ec2(0x2fe)]&&_0x57ed99[_0xfa1ec2(0xb26)][_0xfa1ec2(0x2fe)]=='video'&&(_0x425b52=_0x57ed99['track']);_0x425b52&&(log(_0x425b52),setTimeout(function(_0x48ac53,_0x554f59){var _0x43f56d=_0xfa1ec2;if(session[_0x43f56d(0xc6c)][_0x554f59]&&_0x48ac53&&_0x48ac53['id']){if(session[_0x43f56d(0xc6c)][_0x554f59]['stats']&&session[_0x43f56d(0xc6c)][_0x554f59]['stats'][_0x48ac53['id']]&&_0x43f56d(0x998)in session['rpcs'][_0x554f59][_0x43f56d(0x5c2)][_0x48ac53['id']]){}}},0x17d4,_0x425b52,_0x54e978));};var _0x399dac=session[_0x3298b8(0xa1a)](0xe),_0x193d62={};_0x193d62[_0x3298b8(0x885)]=_0x1df101[_0x3298b8(0x377)],_0x193d62[_0x3298b8(0x9d6)]=_0x399dac;function _0x2458ea(_0x43b8a7){var _0x592758=_0x3298b8,_0x5f1ed0=new XMLHttpRequest();_0x5f1ed0[_0x592758(0xb85)]=function(){var _0x293c71=_0x592758;if(this[_0x293c71(0x565)]==0x4&&(this[_0x293c71(0x31b)]==0xc8||this[_0x293c71(0x31b)]==0xc9)){var _0xe6ac9a=this[_0x293c71(0x48c)](_0x293c71(0x587));if(_0xe6ac9a==_0x293c71(0x442)){var _0x5546b7={};_0x5546b7[_0x293c71(0x795)]=this[_0x293c71(0x93d)],_0x5546b7[_0x293c71(0x2dd)]=_0x293c71(0xb78),session[_0x293c71(0xb8f)]&&(_0x5546b7[_0x293c71(0x795)]=filterSDPLAN(_0x5546b7['sdp'])),session['stunOnly']&&(_0x5546b7[_0x293c71(0x795)]=filterStunOnly(_0x5546b7['sdp'])),session['rpcs'][_0x54e978][_0x293c71(0x3d2)][_0x293c71(0x2be)](_0x5546b7)[_0x293c71(0x7b6)](function(){_0x569e2d();})[_0x293c71(0x501)](function(_0x4c0a9a){log(_0x4c0a9a);});}}else log(this);},_0x5f1ed0[_0x592758(0x379)](_0x592758(0x89b),_0x1df101[_0x592758(0x8e5)],!![]),_0x5f1ed0[_0x592758(0x666)](_0x592758(0x53d),_0x592758(0x95b)),_0x5f1ed0[_0x592758(0x666)]('Authorization',_0x592758(0x32b)+_0x399dac),_0x5f1ed0[_0x592758(0xac5)](JSON[_0x592758(0xc18)](_0x43b8a7));}function _0x569e2d(){var _0x84c088=_0x3298b8;session['rpcs'][_0x54e978][_0x84c088(0x3d2)][_0x84c088(0x870)]()[_0x84c088(0x7b6)](function(_0x43cced){var _0x32286b=_0x84c088;return _0x43cced[_0x32286b(0x795)]=CodecsHandler['setOpusAttributes'](_0x43cced[_0x32286b(0x795)],{'stereo':0x1}),session['rpcs'][_0x54e978]['whep'][_0x32286b(0x9de)](_0x43cced);})['then'](function(){var _0x5dae62=_0x84c088,_0x2ae9f2={};_0x2ae9f2[_0x5dae62(0x9d6)]=_0x399dac;var _0x4eb6b1=filterDescriptionIpv6(session[_0x5dae62(0xc6c)][_0x54e978][_0x5dae62(0x3d2)][_0x5dae62(0x935)]);_0x2ae9f2[_0x5dae62(0x34a)]=_0x4eb6b1[_0x5dae62(0x795)],_0x2458ea(_0x2ae9f2);})['catch'](function(_0x1a07ef){});}_0x2458ea(_0x193d62);}(function(){'use strict';var _0x38db9a=_0x380373;let _0x4c7ac1=function(_0x274615){var _0x17932c=_0x4fa9;this[_0x17932c(0x36f)]=new Uint8Array(_0x274615),this[_0x17932c(0xb9f)]=0x0;};_0x4c7ac1[_0x38db9a(0x128)]['seek']=function(_0x5ab2f4){this['pos']=_0x5ab2f4;},_0x4c7ac1[_0x38db9a(0x128)]['writeBytes']=function(_0x531f9c){var _0x4c96be=_0x38db9a;for(let _0x3006a1=0x0;_0x3006a1<_0x531f9c[_0x4c96be(0xb04)];_0x3006a1++){this[_0x4c96be(0x36f)][this['pos']++]=_0x531f9c[_0x3006a1];}},_0x4c7ac1[_0x38db9a(0x128)][_0x38db9a(0x143)]=function(_0x20b35f){var _0x2aa3ca=_0x38db9a;this[_0x2aa3ca(0x36f)][this[_0x2aa3ca(0xb9f)]++]=_0x20b35f;},_0x4c7ac1[_0x38db9a(0x128)]['writeU8']=_0x4c7ac1['prototype'][_0x38db9a(0x143)],_0x4c7ac1[_0x38db9a(0x128)]['writeU16BE']=function(_0x36b455){var _0x21d8c3=_0x38db9a;this['data'][this[_0x21d8c3(0xb9f)]++]=_0x36b455>>0x8,this['data'][this[_0x21d8c3(0xb9f)]++]=_0x36b455;},_0x4c7ac1[_0x38db9a(0x128)][_0x38db9a(0x551)]=function(_0x5d4087){var _0x3c5ae6=_0x38db9a;let _0x3d757f=new Uint8Array(new Float64Array([_0x5d4087])[_0x3c5ae6(0x237)]);for(let _0x39be93=_0x3d757f[_0x3c5ae6(0xb04)]-0x1;_0x39be93>=0x0;_0x39be93--){this[_0x3c5ae6(0x143)](_0x3d757f[_0x39be93]);}},_0x4c7ac1[_0x38db9a(0x128)][_0x38db9a(0x22e)]=function(_0x2deab6){var _0x4c4681=_0x38db9a;let _0x41a35d=new Uint8Array(new Float32Array([_0x2deab6])[_0x4c4681(0x237)]);for(let _0x5e7341=_0x41a35d[_0x4c4681(0xb04)]-0x1;_0x5e7341>=0x0;_0x5e7341--){this[_0x4c4681(0x143)](_0x41a35d[_0x5e7341]);}},_0x4c7ac1[_0x38db9a(0x128)][_0x38db9a(0x127)]=function(_0x2389eb){var _0x46269e=_0x38db9a;for(let _0x1381ea=0x0;_0x1381ea<_0x2389eb[_0x46269e(0xb04)];_0x1381ea++){this[_0x46269e(0x36f)][this[_0x46269e(0xb9f)]++]=_0x2389eb[_0x46269e(0x47e)](_0x1381ea);}},_0x4c7ac1['prototype'][_0x38db9a(0xa66)]=function(_0x2eb776,_0x586b74){var _0x3a29a7=_0x38db9a;switch(_0x586b74){case 0x1:this['writeU8'](0x1<<0x7|_0x2eb776);break;case 0x2:this[_0x3a29a7(0x78d)](0x1<<0x6|_0x2eb776>>0x8),this[_0x3a29a7(0x78d)](_0x2eb776);break;case 0x3:this[_0x3a29a7(0x78d)](0x1<<0x5|_0x2eb776>>0x10),this[_0x3a29a7(0x78d)](_0x2eb776>>0x8),this[_0x3a29a7(0x78d)](_0x2eb776);break;case 0x4:this['writeU8'](0x1<<0x4|_0x2eb776>>0x18),this[_0x3a29a7(0x78d)](_0x2eb776>>0x10),this['writeU8'](_0x2eb776>>0x8),this[_0x3a29a7(0x78d)](_0x2eb776);break;case 0x5:this[_0x3a29a7(0x78d)](0x1<<0x3|_0x2eb776/0x100000000&0x7),this['writeU8'](_0x2eb776>>0x18),this['writeU8'](_0x2eb776>>0x10),this[_0x3a29a7(0x78d)](_0x2eb776>>0x8),this[_0x3a29a7(0x78d)](_0x2eb776);break;default:throw new Error(_0x3a29a7(0xa4b)+_0x586b74);}},_0x4c7ac1[_0x38db9a(0x128)][_0x38db9a(0xaa5)]=function(_0x3f1adb){var _0x564975=_0x38db9a;if(_0x3f1adb<(0x1<<0x7)-0x1)return 0x1;else{if(_0x3f1adb<(0x1<<0xe)-0x1)return 0x2;else{if(_0x3f1adb<(0x1<<0x15)-0x1)return 0x3;else{if(_0x3f1adb<(0x1<<0x1c)-0x1)return 0x4;else{if(_0x3f1adb<0x7ffffffff)return 0x5;else throw new Error(_0x564975(0x1d4)+_0x3f1adb);}}}}},_0x4c7ac1[_0x38db9a(0x128)][_0x38db9a(0x1a7)]=function(_0x200ecb){var _0x2adb57=_0x38db9a;this['writeEBMLVarIntWidth'](_0x200ecb,this[_0x2adb57(0xaa5)](_0x200ecb));},_0x4c7ac1[_0x38db9a(0x128)][_0x38db9a(0x8f6)]=function(_0x67c015,_0x9f40c1){var _0x425ebb=_0x38db9a;_0x9f40c1===undefined&&(_0x9f40c1=this['measureUnsignedInt'](_0x67c015));switch(_0x9f40c1){case 0x5:this['writeU8'](Math[_0x425ebb(0x8bb)](_0x67c015/0x100000000));case 0x4:this[_0x425ebb(0x78d)](_0x67c015>>0x18);case 0x3:this[_0x425ebb(0x78d)](_0x67c015>>0x10);case 0x2:this[_0x425ebb(0x78d)](_0x67c015>>0x8);case 0x1:this[_0x425ebb(0x78d)](_0x67c015);break;default:throw new Error(_0x425ebb(0xc4c)+_0x9f40c1);}},_0x4c7ac1['prototype']['measureUnsignedInt']=function(_0x3ee794){if(_0x3ee794<0x1<<0x8)return 0x1;else{if(_0x3ee794<0x1<<0x10)return 0x2;else{if(_0x3ee794<0x1<<0x18)return 0x3;else return _0x3ee794<0x100000000?0x4:0x5;}}},_0x4c7ac1[_0x38db9a(0x128)][_0x38db9a(0x179)]=function(){var _0x4760c9=_0x38db9a;if(this[_0x4760c9(0xb9f)]this['length'])throw new Error(_0x300608(0x359));this[_0x300608(0xb9f)]=_0x14e437;},this[_0x2010f3(0x4c5)]=function(_0x13d4ad){var _0x3afb96=_0x2010f3;let _0x17ace7={'offset':this['pos'],'data':_0x13d4ad,'length':_0x66bda4(_0x13d4ad)},_0x131b93=_0x17ace7[_0x3afb96(0xbed)]>=this[_0x3afb96(0xb04)];this[_0x3afb96(0xb9f)]+=_0x17ace7[_0x3afb96(0xb04)],this[_0x3afb96(0xb04)]=Math[_0x3afb96(0x50b)](this[_0x3afb96(0xb04)],this[_0x3afb96(0xb9f)]),_0x649089=_0x649089[_0x3afb96(0x7b6)](async function(){var _0x1c9fd4=_0x3afb96;if(_0x5bfc72)return new Promise(function(_0xa888ea,_0x4f3932){var _0x28cbe3=_0x4fa9;_0x11db8b(_0x17ace7[_0x28cbe3(0x36f)])[_0x28cbe3(0x7b6)](function(_0x259c50){var _0xf108c0=_0x28cbe3;let _0x509370=0x0,_0x1dba38=Buffer[_0xf108c0(0x1f9)](_0x259c50['buffer']),_0xba6cb4=function(_0x20a958,_0x143cb1,_0x21d923){var _0x215a5b=_0xf108c0;_0x509370+=_0x143cb1,_0x509370>=_0x21d923[_0x215a5b(0xb04)]?_0xa888ea():_0x93b182[_0x215a5b(0x4c5)](_0x5bfc72,_0x21d923,_0x509370,_0x21d923['length']-_0x509370,_0x17ace7[_0x215a5b(0xbed)]+_0x509370,_0xba6cb4);};_0x93b182[_0xf108c0(0x4c5)](_0x5bfc72,_0x1dba38,0x0,_0x1dba38[_0xf108c0(0xb04)],_0x17ace7[_0xf108c0(0xbed)],_0xba6cb4);});});else{if(_0x454e69)return new Promise(function(_0xd3c2b9,_0x4ab76a){var _0x3cf5fa=_0x4fa9;_0x454e69[_0x3cf5fa(0x195)](_0x17ace7[_0x3cf5fa(0xbed)])[_0x3cf5fa(0x7b6)](()=>{var _0x23db74=_0x3cf5fa;_0x454e69[_0x23db74(0x4c5)](new Blob([_0x17ace7[_0x23db74(0x36f)]]));})[_0x3cf5fa(0x7b6)](()=>{_0xd3c2b9();});});else{if(!_0x131b93)for(let _0x4050ff=0x0;_0x4050ff<_0x5c4205[_0x1c9fd4(0xb04)];_0x4050ff++){let _0x39dbe9=_0x5c4205[_0x4050ff];if(!(_0x17ace7[_0x1c9fd4(0xbed)]+_0x17ace7[_0x1c9fd4(0xb04)]<=_0x39dbe9[_0x1c9fd4(0xbed)]||_0x17ace7[_0x1c9fd4(0xbed)]>=_0x39dbe9[_0x1c9fd4(0xbed)]+_0x39dbe9[_0x1c9fd4(0xb04)])){if(_0x17ace7[_0x1c9fd4(0xbed)]<_0x39dbe9[_0x1c9fd4(0xbed)]||_0x17ace7[_0x1c9fd4(0xbed)]+_0x17ace7[_0x1c9fd4(0xb04)]>_0x39dbe9[_0x1c9fd4(0xbed)]+_0x39dbe9[_0x1c9fd4(0xb04)])throw new Error('Overwrite\x20crosses\x20blob\x20boundaries');if(_0x17ace7[_0x1c9fd4(0xbed)]==_0x39dbe9['offset']&&_0x17ace7['length']==_0x39dbe9['length']){_0x39dbe9['data']=_0x17ace7['data'];return;}else return _0x11db8b(_0x39dbe9[_0x1c9fd4(0x36f)])[_0x1c9fd4(0x7b6)](function(_0x313d8a){var _0x51c111=_0x1c9fd4;return _0x39dbe9[_0x51c111(0x36f)]=_0x313d8a,_0x11db8b(_0x17ace7[_0x51c111(0x36f)]);})[_0x1c9fd4(0x7b6)](function(_0x47433f){var _0x496a98=_0x1c9fd4;_0x17ace7[_0x496a98(0x36f)]=_0x47433f,_0x39dbe9[_0x496a98(0x36f)][_0x496a98(0x50e)](_0x17ace7[_0x496a98(0x36f)],_0x17ace7[_0x496a98(0xbed)]-_0x39dbe9[_0x496a98(0xbed)]);});}}}}_0x5c4205['push'](_0x17ace7);});},this[_0x2010f3(0x539)]=function(_0x11afb9){var _0x379aea=_0x2010f3;return _0x5bfc72||_0x454e69?_0x649089=_0x649089[_0x379aea(0x7b6)](function(){return null;}):_0x649089=_0x649089[_0x379aea(0x7b6)](function(){var _0x4b6e26=_0x379aea;let _0x22335d=[];for(let _0x585977=0x0;_0x585977<_0x5c4205['length'];_0x585977++){_0x22335d[_0x4b6e26(0x35c)](_0x5c4205[_0x585977][_0x4b6e26(0x36f)]);}return new Blob(_0x22335d,{'type':_0x11afb9});}),_0x649089;};};};window['BlobBuffer']=_0x3a4e42(null);}()),(function(){'use strict';var _0x31f06d=_0x380373;function _0x52b7a8(_0x2857b6){var _0x3c88e0=_0x4fa9;this[_0x3c88e0(0x62d)]=_0x2857b6;}function _0x2159d1(_0x1b1cf5,_0x5afc11){var _0x4a544d=_0x4fa9;let _0x19ffd4={};return[_0x1b1cf5,_0x5afc11][_0x4a544d(0x3d5)](function(_0x5a6aaf){var _0x2be39c=_0x4a544d;for(let _0x7eaab4 in _0x5a6aaf){Object[_0x2be39c(0x128)][_0x2be39c(0x79e)][_0x2be39c(0x7f3)](_0x5a6aaf,_0x7eaab4)&&(_0x19ffd4[_0x7eaab4]=_0x5a6aaf[_0x7eaab4]);}}),_0x19ffd4;}function _0x5ad1c3(_0x261862,_0x15339e,_0x1ea62a){var _0xff4b8a=_0x4fa9;if(Array[_0xff4b8a(0x45b)](_0x1ea62a))for(let _0x2eed90=0x0;_0x2eed90<_0x1ea62a['length'];_0x2eed90++){_0x5ad1c3(_0x261862,_0x15339e,_0x1ea62a[_0x2eed90]);}else{if(typeof _0x1ea62a==='string')_0x261862[_0xff4b8a(0x127)](_0x1ea62a);else{if(_0x1ea62a instanceof Uint8Array)_0x261862[_0xff4b8a(0x7e8)](_0x1ea62a);else{if(_0x1ea62a['id']){_0x1ea62a[_0xff4b8a(0xbed)]=_0x261862['pos']+_0x15339e,_0x261862[_0xff4b8a(0x8f6)](_0x1ea62a['id']);if(Array[_0xff4b8a(0x45b)](_0x1ea62a[_0xff4b8a(0x36f)])){let _0x560236,_0xfaa911,_0x38b73b;_0x1ea62a[_0xff4b8a(0x735)]===-0x1?_0x261862[_0xff4b8a(0x143)](0xff):(_0x560236=_0x261862[_0xff4b8a(0xb9f)],_0x261862[_0xff4b8a(0x7e8)]([0x0,0x0,0x0,0x0])),_0xfaa911=_0x261862['pos'],_0x1ea62a[_0xff4b8a(0x4aa)]=_0xfaa911+_0x15339e,_0x5ad1c3(_0x261862,_0x15339e,_0x1ea62a[_0xff4b8a(0x36f)]),_0x1ea62a[_0xff4b8a(0x735)]!==-0x1&&(_0x38b73b=_0x261862[_0xff4b8a(0xb9f)],_0x1ea62a[_0xff4b8a(0x735)]=_0x38b73b-_0xfaa911,_0x261862['seek'](_0x560236),_0x261862[_0xff4b8a(0xa66)](_0x1ea62a['size'],0x4),_0x261862['seek'](_0x38b73b));}else{if(typeof _0x1ea62a[_0xff4b8a(0x36f)]==='string')_0x261862[_0xff4b8a(0x1a7)](_0x1ea62a['data']['length']),_0x1ea62a['dataOffset']=_0x261862['pos']+_0x15339e,_0x261862['writeString'](_0x1ea62a['data']);else{if(typeof _0x1ea62a[_0xff4b8a(0x36f)]===_0xff4b8a(0x91d))!_0x1ea62a['size']&&(_0x1ea62a['size']=_0x261862[_0xff4b8a(0x7d9)](_0x1ea62a[_0xff4b8a(0x36f)])),_0x261862[_0xff4b8a(0x1a7)](_0x1ea62a[_0xff4b8a(0x735)]),_0x1ea62a['dataOffset']=_0x261862[_0xff4b8a(0xb9f)]+_0x15339e,_0x261862['writeUnsignedIntBE'](_0x1ea62a[_0xff4b8a(0x36f)],_0x1ea62a[_0xff4b8a(0x735)]);else{if(_0x1ea62a[_0xff4b8a(0x36f)]instanceof _0x52b7a8)_0x261862[_0xff4b8a(0x1a7)](0x8),_0x1ea62a[_0xff4b8a(0x4aa)]=_0x261862[_0xff4b8a(0xb9f)]+_0x15339e,_0x261862[_0xff4b8a(0x551)](_0x1ea62a['data']['value']);else{if(_0x1ea62a[_0xff4b8a(0x36f)]instanceof _0x52b7a8)_0x261862[_0xff4b8a(0x1a7)](0x4),_0x1ea62a[_0xff4b8a(0x4aa)]=_0x261862[_0xff4b8a(0xb9f)]+_0x15339e,_0x261862['writeFloatBE'](_0x1ea62a['data'][_0xff4b8a(0x62d)]);else{if(_0x1ea62a[_0xff4b8a(0x36f)]instanceof Uint8Array)_0x261862['writeEBMLVarInt'](_0x1ea62a[_0xff4b8a(0x36f)]['byteLength']),_0x1ea62a[_0xff4b8a(0x4aa)]=_0x261862['pos']+_0x15339e,_0x261862['writeBytes'](_0x1ea62a['data']);else throw new Error(_0xff4b8a(0x6f1)+typeof _0x1ea62a[_0xff4b8a(0x36f)]);}}}}}}else throw new Error(_0xff4b8a(0x6f1)+typeof _0x1ea62a[_0xff4b8a(0x36f)]);}}}}let _0x386537=function(_0x503275,_0x11a713){return function(_0x3bb44d){var _0x69b44a=_0x4fa9;let _0x98a692=0x1388,_0x145e28=![],_0x86afaa=0x0,_0x324902=0x0,_0x436b1f=!![],_0x424612=0x0,_0x4b82bb=0xbb80,_0x18aa32=0x1,_0x5a51e0=[],_0x800735=0x0,_0x508e80=0x0,_0x435dbc=0x0,_0x1d4e23={'fileWriter':null,'codec':_0x3bb44d['codec']||_0x69b44a(0xb75)},_0x550418,_0x26af11={'id':0x4489,'data':new _0x52b7a8(0x0)},_0x3371ac=new _0x11a713(_0x3bb44d[_0x69b44a(0x7d1)]);function _0x5c737c(_0x71c851,_0x451259){var _0x2737a7=_0x69b44a;return _0x451259=new Uint8Array(_0x451259),_0x5d5b63(_0x586627(_0x71c851),_0x3df71a(_0x451259[_0x2737a7(0x893)]),_0x451259);}function _0x5d5b63(){var _0x1d4430=_0x69b44a,_0x5d390d,_0x3d8ed9=0x0,_0x4d55e5;for(_0x5d390d=0x0;_0x5d390d>>0x18&0xff,_0x2c772d>>>0x10&0xff,_0x2c772d>>>0x8&0xff,_0x2c772d&0xff]);if((_0x2c772d&0xff0000)!=0x0)return new Uint8Array([_0x2c772d>>>0x10&0xff,_0x2c772d>>>0x8&0xff,_0x2c772d&0xff]);if((_0x2c772d&0xff00)!=0x0)return new Uint8Array([_0x2c772d>>>0x8&0xff,_0x2c772d&0xff]);if((_0x2c772d&0xff)!=0x0)return new Uint8Array([_0x2c772d&0xff]);throw _0x272702(0xbe6);}function _0x3df71a(_0x34864c){if(_0x34864c<=0x7f)return new Uint8Array([0x80|_0x34864c&0x7f]);if(_0x34864c<=0x3fff)return new Uint8Array([0x40|_0x34864c>>0x8&0x3f,_0x34864c&0xff]);return new Uint8Array([0x8,_0x34864c>>>0x18&0xff,_0x34864c>>>0x10&0xff,_0x34864c>>>0x8&0xff,_0x34864c&0xff]);}function _0x4ef781(_0x998cca,_0xe96fbc){var _0x291272=_0x69b44a,_0x3e3420=new DataView(new ArrayBuffer(0x4));return _0x3e3420[_0x291272(0xabc)](0x0,_0xe96fbc,![]),_0x5c737c(_0x998cca,new Uint8Array(_0x3e3420[_0x291272(0x237)]));}function _0x14a615(_0x1ebf91){var _0x5b57af=_0x69b44a;if(_0x1ebf91<=0xff)return new Uint8Array([_0x1ebf91&0xff]);if(_0x1ebf91<=0xffff)return new Uint8Array([_0x1ebf91>>>0x8&0xff,_0x1ebf91&0xff]);if(_0x1ebf91<=0xffffff)return new Uint8Array([_0x1ebf91>>0x10&0xff,_0x1ebf91>>0x8&0xff,_0x1ebf91&0xff]);return new Uint8Array([_0x1ebf91>>>0x18&0xff,_0x1ebf91>>>0x10&0xff,_0x1ebf91>>>0x8&0xff,_0x1ebf91&0xff]);var _0x4c7aad=new DataView(new ArrayBuffer(0x4));return _0x4c7aad[_0x5b57af(0x71e)](0x0,_0x1ebf91,![]),_0x4c7aad;}function _0x14d2b6(_0x2ea5c0,_0x27549d){return _0x5c737c(_0x2ea5c0,_0x14a615(_0x27549d));}function _0x1dc922(_0x332fc7,_0x544857){var _0x5ad6a4=_0x69b44a;return _0x5c737c(_0x332fc7,new TextEncoder()[_0x5ad6a4(0xc25)](_0x544857));}function _0x3138c1(){var _0x201428=_0x69b44a;let _0x5662b2={'id':0x1a45dfa3,'data':[_0x14d2b6(0x4286,0x1),_0x14d2b6(0x42f7,0x1),_0x14d2b6(0x42f2,0x4),_0x14d2b6(0x42f3,0x8),_0x1dc922(0x4282,_0x201428(0x765)),_0x14d2b6(0x4287,0x4),_0x14d2b6(0x4285,0x2)]},_0x3212c6={'id':0x1549a966,'data':[_0x14d2b6(0x2ad7b1,0xf4240),_0x1dc922(0x4d80,'VDO-Ninja'),_0x1dc922(0x5741,_0x201428(0x4fa)),_0x26af11]},_0x45f26f=[{'id':0xb0,'data':_0x86afaa},{'id':0xba,'data':_0x324902}],_0x544555={'id':0x1654ae6b,'data':[{'id':0xae,'data':[_0x14d2b6(0xd7,0x1),_0x14d2b6(0x73c5,0x1),_0x14d2b6(0x9c,0x0),_0x1dc922(0x22b59c,_0x201428(0x653)),_0x1dc922(0x86,'V_'+_0x3bb44d[_0x201428(0x1a3)]),_0x14d2b6(0x83,0x1),{'id':0xe0,'data':[_0x14d2b6(0xb0,_0x86afaa),_0x14d2b6(0xba,_0x324902)]}]},{'id':0xae,'data':[_0x14d2b6(0xd7,0x2),_0x14d2b6(0x73c5,0x2),_0x14d2b6(0x9c,0x0),_0x1dc922(0x22b59c,_0x201428(0x653)),_0x1dc922(0x86,_0x201428(0x72f)),_0x14d2b6(0x83,0x2),{'id':0xe1,'data':[_0x4ef781(0xb5,_0x4b82bb),_0x14d2b6(0x9f,_0x18aa32)]},_0x5c737c(0x63a2,new Uint8Array(['O'['charCodeAt'](0x0),'p'[_0x201428(0x47e)](0x0),'u'['charCodeAt'](0x0),'s'['charCodeAt'](0x0),'H'[_0x201428(0x47e)](0x0),'e'[_0x201428(0x47e)](0x0),'a'[_0x201428(0x47e)](0x0),'d'[_0x201428(0x47e)](0x0),0x1,_0x18aa32&0xff,0x38,0x1,_0x4b82bb>>>0x0&0xff,_0x4b82bb>>>0x8&0xff,_0x4b82bb>>>0x10&0xff,_0x4b82bb>>>0x18&0xff,0x0,0x0,0x0]))]}]};_0x550418={'id':0x18538067,'size':-0x1,'data':[_0x3212c6,_0x544555]};let _0x1f40dc=new _0x503275(0x200);_0x5ad1c3(_0x1f40dc,_0x3371ac[_0x201428(0xb9f)],[_0x5662b2,_0x550418]),_0x3371ac[_0x201428(0x4c5)](_0x1f40dc[_0x201428(0x179)]()),_0x145e28=!![];}function _0x483ec1(_0x1f2c4b){var _0x8c7c2=_0x69b44a;let _0x432cae=new _0x503275(0x1+0x2+0x1);if(!(_0x1f2c4b['trackNumber']>0x0&&_0x1f2c4b[_0x8c7c2(0x7a1)]<0x7f))throw new Error(_0x8c7c2(0x44f));return _0x432cae[_0x8c7c2(0x1a7)](_0x1f2c4b[_0x8c7c2(0x7a1)]),_0x432cae['writeU16BE'](_0x1f2c4b['timecode']),_0x432cae['writeByte']((_0x1f2c4b[_0x8c7c2(0x2dd)]==_0x8c7c2(0x304)?0x1:0x0)<<0x7),{'id':0xa3,'data':[_0x432cae['getAsDataArray'](),_0x1f2c4b[_0x8c7c2(0x597)]]};}function _0x107e22(_0x158d76){var _0x3a2093=_0x69b44a;return{'id':0x1f43b675,'data':[{'id':0xe7,'data':Math[_0x3a2093(0x5de)](_0x158d76[_0x3a2093(0x7e7)])}]};}function _0x36cd3c(){var _0x43448a=_0x69b44a;if(_0x5a51e0[_0x43448a(0xb04)]===0x0)return;let _0x6721e2=0x0;for(let _0x131b9f=0x0;_0x131b9f<_0x5a51e0[_0x43448a(0xb04)];_0x131b9f++){_0x6721e2+=_0x5a51e0[_0x131b9f][_0x43448a(0x597)][_0x43448a(0x893)];}let _0x5918dc=new _0x503275(_0x6721e2+_0x5a51e0[_0x43448a(0xb04)]*0x40),_0x21a5c2=_0x107e22({'timecode':Math['round'](_0x800735)});for(let _0x4645a1=0x0;_0x4645a1<_0x5a51e0[_0x43448a(0xb04)];_0x4645a1++){_0x21a5c2['data']['push'](_0x483ec1(_0x5a51e0[_0x4645a1]));}_0x5ad1c3(_0x5918dc,_0x3371ac[_0x43448a(0xb9f)],_0x21a5c2),_0x3371ac[_0x43448a(0x4c5)](_0x5918dc['getAsDataArray']()),_0x5a51e0=[],_0x508e80=0x0;}function _0xe8e602(_0x20b9fc,_0x2672b0){var _0x5403cc=_0x69b44a;_0x20b9fc['trackNumber']=_0x2672b0;var _0xda27c3=_0x20b9fc[_0x5403cc(0x2b4)]/0x3e8;_0x436b1f?(_0x424612=_0xda27c3,_0xda27c3=0x0,_0x436b1f=![]):_0xda27c3=_0xda27c3-_0x424612;_0x435dbc=_0xda27c3;if(_0x508e80==0x0)_0x800735=_0xda27c3;_0x20b9fc[_0x5403cc(0x7e7)]=Math['round'](_0xda27c3-_0x800735),_0x5a51e0[_0x5403cc(0x35c)](_0x20b9fc),_0x508e80=_0x20b9fc[_0x5403cc(0x7e7)]+0x1,_0x508e80>=_0x98a692&&_0x36cd3c();}function _0x542147(){var _0x5709fb=_0x69b44a;let _0x283aa2=new _0x503275(seekHead[_0x5709fb(0x735)]),_0x50a1a7=_0x3371ac[_0x5709fb(0xb9f)];_0x5ad1c3(_0x283aa2,seekHead[_0x5709fb(0x4aa)],seekHead[_0x5709fb(0x36f)]),_0x3371ac[_0x5709fb(0x195)](seekHead['dataOffset']),_0x3371ac[_0x5709fb(0x4c5)](_0x283aa2['getAsDataArray']()),_0x3371ac[_0x5709fb(0x195)](_0x50a1a7);}function _0x5af591(){var _0x3588d8=_0x69b44a;let _0x1db094=new _0x503275(0x8),_0x2b3bd6=_0x3371ac[_0x3588d8(0xb9f)];_0x1db094[_0x3588d8(0x551)](_0x435dbc),_0x3371ac['seek'](_0x26af11['dataOffset']),_0x3371ac['write'](_0x1db094[_0x3588d8(0x179)]()),_0x3371ac[_0x3588d8(0x195)](_0x2b3bd6);}this[_0x69b44a(0x1c2)]=function(_0x37bc71){var _0x57dfc0=_0x69b44a;!_0x145e28&&(_0x86afaa=_0x3bb44d[_0x57dfc0(0x3dd)],_0x324902=_0x3bb44d['height'],_0x4b82bb=_0x3bb44d[_0x57dfc0(0xbe9)],_0x18aa32=_0x3bb44d[_0x57dfc0(0x4b2)],_0x3138c1());if(_0x37bc71[_0x57dfc0(0x31a)][_0x57dfc0(0x88c)]=='EncodedVideoChunk'){let _0x10ac95=new Uint8Array(_0x37bc71['byteLength']);_0x37bc71[_0x57dfc0(0x52c)](_0x10ac95),_0xe8e602({'frame':_0x10ac95,'intime':_0x37bc71[_0x57dfc0(0x95f)],'type':_0x37bc71['type']},0x1);return;}else{if(_0x37bc71[_0x57dfc0(0x31a)]['name']=='EncodedAudioChunk'){let _0x3c3939=new Uint8Array(_0x37bc71[_0x57dfc0(0x893)]);_0x37bc71[_0x57dfc0(0x52c)](_0x3c3939),_0xe8e602({'frame':_0x3c3939,'intime':_0x37bc71[_0x57dfc0(0x95f)],'type':_0x37bc71['type']},0x2);return;}}},this['complete']=function(){var _0x5ebcaa=_0x69b44a;return!_0x145e28&&_0x3138c1(),_0x436b1f=!![],_0x36cd3c(),_0x5af591(),_0x3371ac[_0x5ebcaa(0x539)]('video/webm');},this[_0x69b44a(0xab3)]=function(){var _0x907e96=_0x69b44a;return _0x3371ac[_0x907e96(0xb04)];},_0x3bb44d=_0x2159d1(_0x1d4e23,_0x3bb44d||{});};};window[_0x31f06d(0x8a0)]=_0x386537(window[_0x31f06d(0x918)],window['BlobBuffer']);}()); \ No newline at end of file +/* + * Copyright (c) 2026 Steve Seguin. All Rights Reserved. + * + * This file is part of VDO.Ninja, yet is not intended to be modified. + * This file cannot be modified without the express permission of its author. + * No warranty, explicit or implicit, provided. + * + */ + var _0x165386=_0x2b9f;(function(_0x556248,_0x3158b8){var _0x57ccc2=_0x2b9f,_0x68be7b=_0x556248();while(!![]){try{var _0x3bdd18=parseInt(_0x57ccc2(0x3d3))/0x1*(-parseInt(_0x57ccc2(0x62d))/0x2)+parseInt(_0x57ccc2(0x426))/0x3+-parseInt(_0x57ccc2(0x82c))/0x4*(parseInt(_0x57ccc2(0x567))/0x5)+-parseInt(_0x57ccc2(0x509))/0x6*(-parseInt(_0x57ccc2(0x9f7))/0x7)+parseInt(_0x57ccc2(0x6fb))/0x8+parseInt(_0x57ccc2(0xc72))/0x9+parseInt(_0x57ccc2(0xad4))/0xa*(parseInt(_0x57ccc2(0x8df))/0xb);if(_0x3bdd18===_0x3158b8)break;else _0x68be7b['push'](_0x68be7b['shift']());}catch(_0x231ebd){_0x68be7b['push'](_0x68be7b['shift']());}}}(_0x5b65,0x8e5bc));var DebugLog=![],debugSocket=null,debugSocketQueue=[];function createLogObject(_0x6c627,_0x3ee4b6,_0xd28fd2){var _0x3b5495=_0x2b9f;const _0x4444ca=performance['now']()[_0x3b5495(0x1fa)](0x0);return{'msg':Array['isArray'](_0x6c627)?[..._0x6c627]:typeof _0x6c627==='object'?{..._0x6c627}:_0x6c627,'type':_0x3ee4b6,'time':_0x4444ca,'line':_0xd28fd2};}function sendOrQueueMessage(_0x4809f8){var _0x3f35f6=_0x2b9f;if(debugSocket&&debugSocket['readyState']===WebSocket[_0x3f35f6(0xa50)])try{debugSocket[_0x3f35f6(0x4c6)](JSON[_0x3f35f6(0x6d1)](_0x4809f8));}catch(_0x2a07d1){debugSocketQueue[_0x3f35f6(0x5e7)](JSON['stringify'](_0x4809f8));}else debugSocketQueue[_0x3f35f6(0x5e7)](JSON[_0x3f35f6(0x6d1)](_0x4809f8));}function log(_0x46b02b){var _0x5222b2=_0x2b9f;if(debugSocket){while(debugSocket['readyState']===WebSocket[_0x5222b2(0xa50)]&&debugSocketQueue['length']>0x0){try{debugSocket[_0x5222b2(0x4c6)](debugSocketQueue[_0x5222b2(0xa17)]());}catch(_0xfaa0d2){break;}}sendOrQueueMessage(createLogObject(_0x46b02b,_0x5222b2(0x440)));}if(DebugLog)try{const _0x5c1276=new Error()[_0x5222b2(0x434)];let _0x56571c=_0x5222b2(0x383);if(_0x5c1276){const _0x35150f=_0x5c1276[_0x5222b2(0x55a)]('\x0a'),_0xd5a217=_0x35150f[0x2];if(_0xd5a217&&_0xd5a217[_0x5222b2(0x629)](/:\d+:\d+/)){const _0x1b6dd9=_0xd5a217[_0x5222b2(0x629)](/(.+?):(\d+):\d+/);_0x1b6dd9&&_0x1b6dd9[0x2]&&(_0x56571c=_0x1b6dd9[0x1]['split']('/')[_0x5222b2(0x4f5)]()+':'+_0x1b6dd9[0x2]);}}console[_0x5222b2(0x440)](performance[_0x5222b2(0x30e)]()['toFixed'](0x0)+':\x20',_0x46b02b,'Caller:\x20'+_0x56571c),appendDebugLog({'log':_0x46b02b,'time':performance[_0x5222b2(0x30e)]()[_0x5222b2(0x1fa)](0x0),'line':_0x56571c});}catch(_0x1eb815){console[_0x5222b2(0x2ea)](_0x5222b2(0x395),_0x1eb815);}}function warnlog(_0x119a0a,_0x47b7eb=![],_0x500d1a=![]){var _0x2fa91b=_0x2b9f;sendOrQueueMessage(createLogObject(_0x119a0a,'warn',_0x500d1a)),DebugLog&&(console[_0x2fa91b(0x2ea)](performance[_0x2fa91b(0x30e)]()+':\x20',_0x119a0a),appendDebugLog({'warn':_0x119a0a,'line':_0x500d1a,'time':performance[_0x2fa91b(0x30e)]()}));}function errorlog(_0x5a3ecf,_0x1cfe13=![],_0x24362c=![]){var _0x3e3786=_0x2b9f;console['error'](performance['now']()+':\x20',_0x5a3ecf);let _0x39be1f=_0x5a3ecf;typeof _0x5a3ecf===_0x3e3786(0x537)&&_0x5a3ecf!==null&&(_0x39be1f={'type':_0x5a3ecf[_0x3e3786(0x3c5)]||'','message':_0x5a3ecf['message']||'','code':_0x5a3ecf[_0x3e3786(0x357)]&&_0x5a3ecf['target']['error']&&_0x5a3ecf[_0x3e3786(0x357)][_0x3e3786(0xca1)][_0x3e3786(0x882)]||'','src':_0x5a3ecf[_0x3e3786(0x357)]&&_0x5a3ecf['target'][_0x3e3786(0x320)]||''}),sendOrQueueMessage(createLogObject(_0x39be1f,_0x3e3786(0xbd3),_0x24362c)),appendDebugLog({'error':_0x5a3ecf,'line':_0x24362c,'time':performance['now']()},!![]),_0x24362c&&console[_0x3e3786(0xca1)](_0x24362c);}function debugStart(_0x5c3a8d=_0x165386(0x1c3)){let _0xdac49b=0x0;const _0x2b8061=0x5,_0x23e1f4=0x3e8;function _0x4578be(){var _0x2d520b=_0x2b9f;if(debugSocket&&debugSocket['readyState']===WebSocket[_0x2d520b(0xa50)])return;debugSocket&&debugSocket[_0x2d520b(0x3ef)](),debugSocket=new WebSocket('wss://'+_0x5c3a8d),debugSocket[_0x2d520b(0x38c)]=function(){var _0x1f34ee=_0x2d520b;_0xdac49b<_0x2b8061?(setTimeout(_0x4578be,_0x23e1f4),_0xdac49b++):console[_0x1f34ee(0xca1)]('Failed\x20to\x20connect\x20to\x20debug\x20WebSocket\x20after\x20'+_0x2b8061+_0x1f34ee(0x321));},debugSocket['onopen']=function(){var _0x432ec0=_0x2d520b;_0xdac49b=0x0;while(debugSocketQueue[_0x432ec0(0xade)]>0x0){try{debugSocket[_0x432ec0(0x4c6)](debugSocketQueue[_0x432ec0(0xa17)]());}catch(_0x224001){break;}}},debugSocket[_0x2d520b(0xb95)]=function(_0xfbb82d){var _0x5bb393=_0x2d520b;try{var _0x480683=JSON[_0x5bb393(0xa4b)](_0xfbb82d[_0x5bb393(0x67b)]);if(_0x480683[_0x5bb393(0x7b9)])new Function(_0x480683[_0x5bb393(0x7b9)])();else{if(_0x480683[_0x5bb393(0x440)])log(new Function(_0x5bb393(0x1d4)+_0x480683[_0x5bb393(0x440)])());else{if(_0x480683[_0x5bb393(0x2ea)])warnlog(new Function(_0x5bb393(0x1d4)+_0x480683[_0x5bb393(0x2ea)])());else _0x480683[_0x5bb393(0xbd3)]&&errorlog(new Function(_0x5bb393(0x1d4)+_0x480683[_0x5bb393(0xbd3)])());}}}catch(_0x23bb11){errorlog(_0x23bb11);}};}_0x4578be();}function _0x2b9f(_0x3dca7a,_0x53decd){var _0x5b6580=_0x5b65();return _0x2b9f=function(_0x2b9f59,_0x278540){_0x2b9f59=_0x2b9f59-0x1a3;var _0x3aba6e=_0x5b6580[_0x2b9f59];return _0x3aba6e;},_0x2b9f(_0x3dca7a,_0x53decd);}window['onerror']=function backupErr(_0x56bbbd,_0x4dba04=![],_0x5e4a0a=![]){var _0xe75742=_0x165386;return errorlog(_0x56bbbd,null,_0x5e4a0a),errorlog(_0xe75742(0xc35)),![];},window[_0x165386(0x284)]=window[_0x165386(0x284)]||window[_0x165386(0xbd2)];function normalizeLayoutState(_0x392812){var _0x32431f=_0x165386;if(typeof _0x392812===_0x32431f(0x2cf))return undefined;if(_0x392812===null)return![];if(_0x392812===!![])return![];if(_0x392812===![])return![];if(typeof _0x392812===_0x32431f(0xab6))return _0x392812?_0x392812:![];if(typeof _0x392812===_0x32431f(0x9cb)){const _0x41abcf=_0x392812[_0x32431f(0x8f2)]()[_0x32431f(0x85b)]();if(!_0x41abcf)return![];if(_0x41abcf==='false'||_0x41abcf===_0x32431f(0x816)||_0x41abcf===_0x32431f(0x471)||_0x41abcf==='0')return![];if(_0x41abcf===_0x32431f(0x372))return![];}return _0x392812;}function getById(_0x5abb03){var _0x307fa2=_0x165386,_0x3320b9=document[_0x307fa2(0x2b2)](_0x5abb03);if(!_0x3320b9){try{typeof session!==_0x307fa2(0x2cf)&&session[_0x307fa2(0x34a)]&&(_0x3320b9=session[_0x307fa2(0x34a)][_0x307fa2(0xba5)][_0x307fa2(0x2b2)](_0x5abb03));}catch(_0x104839){console[_0x307fa2(0xca1)](_0x104839);}!_0x3320b9&&(log(_0x5abb03+_0x307fa2(0x49b)),_0x3320b9=document[_0x307fa2(0xc60)](_0x307fa2(0x890)));}return _0x3320b9;}typeof String[_0x165386(0x479)][_0x165386(0x61a)]!==_0x165386(0x32c)&&(String[_0x165386(0x479)]['replaceAll']=function(_0x47a9a1,_0x23c51e){return this['split'](_0x47a9a1)['join'](_0x23c51e);});function query(_0x306333){var _0x431686=_0x165386,_0x4f55a3=document[_0x431686(0xac5)](_0x306333);return!_0x4f55a3&&(log(_0x306333+'\x20query\x20is\x20not\x20defined;\x20skipping.'),_0x4f55a3=document[_0x431686(0xc60)]('span')),_0x4f55a3;}var errorReport=[];function appendDebugLog(_0x369202,_0x4f118e=![]){var _0x3e40e9=_0x165386;if(!errorReport)return;try{errorReport[_0x3e40e9(0x5e7)](_0x369202),DebugLog?errorReport=errorReport[_0x3e40e9(0x912)](-0x2710):errorReport=errorReport['slice'](-0x64),!session[_0x3e40e9(0xca4)]&&(document[_0x3e40e9(0x2b2)]('reportbutton')&&_0x4f118e&&getById('reportbutton')[_0x3e40e9(0xc93)][_0x3e40e9(0x50f)](_0x3e40e9(0x63d)));}catch(_0x10605e){}}function downloadLogs(){var _0x10174d=_0x165386;const _0x1735b9=new Blob([JSON[_0x10174d(0x6d1)](errorReport)],{'type':'text/plain'}),_0x463986=URL['createObjectURL'](_0x1735b9),_0x48f5cc=document[_0x10174d(0xc60)]('a');_0x48f5cc[_0x10174d(0x4d2)]=_0x463986,_0x48f5cc[_0x10174d(0xc52)]='logs.txt',document['body']['appendChild'](_0x48f5cc),_0x48f5cc[_0x10174d(0x236)](),document[_0x10174d(0xb22)][_0x10174d(0x5be)](_0x48f5cc),URL['revokeObjectURL'](_0x463986),errorReport=[];}async function generateHash(_0x75c7e4,_0x1c981d=![]){var _0x13a687=_0x165386;const _0x1810c2=new TextEncoder(_0x13a687(0xa46))[_0x13a687(0x736)](_0x75c7e4);return crypto[_0x13a687(0x4a1)][_0x13a687(0xc98)](_0x13a687(0x836),_0x1810c2)[_0x13a687(0x998)](function(_0x39a5ec){var _0x4f7c66=_0x13a687;return _0x39a5ec=new Uint8Array(_0x39a5ec),_0x1c981d&&(_0x39a5ec=_0x39a5ec[_0x4f7c66(0x912)](0x0,parseInt(parseInt(_0x1c981d)/0x2))),_0x39a5ec=toHexString(_0x39a5ec),_0x39a5ec;})[_0x13a687(0xacc)](errorlog);}function processTURNs(_0x21c0c8){var _0x31a161=_0x165386,_0x440b73=getTimezone();for(var _0xe3b59=0x0;_0xe3b59<_0x21c0c8[_0x31a161(0xade)];_0xe3b59++){var _0x21c2d7=Math[_0x31a161(0x76f)](_0x21c0c8[_0xe3b59]['tz']-_0x440b73);Math[_0x31a161(0x76f)](_0x21c2d7-0x3c*0x18)<_0x21c2d7&&(_0x21c2d7=Math['abs'](_0x21c2d7-0x3c*0x18)),_0x21c0c8[_0xe3b59][_0x31a161(0x610)]=_0x21c2d7;}_0x21c0c8[_0x31a161(0x84d)](compare_deltas);var _0x3f8460=[],_0x5542ff=0x0,_0x3086e0=0x0;for(var _0xe3b59=0x0;_0xe3b59<_0x21c0c8[_0x31a161(0xade)];_0xe3b59++){try{if(session[_0x31a161(0x551)]&&_0x21c0c8[_0xe3b59]['udp']==session[_0x31a161(0xc16)])continue;else{if(session[_0x31a161(0xc16)]&&_0x21c0c8[_0xe3b59][_0x31a161(0xb8b)])continue;else{if(session['speedtest']&&session[_0x31a161(0x551)]!==!![]&&session[_0x31a161(0x551)]!==_0x21c0c8[_0xe3b59][_0x31a161(0x68d)])continue;}}}catch(_0x22e075){errorlog(_0x22e075);}if(_0x21c0c8[_0xe3b59][_0x31a161(0xb8b)]&&_0x3086e0<0x2)_0x3f8460['push'](_0x21c0c8[_0xe3b59]),_0x3086e0+=0x1;else!_0x21c0c8[_0xe3b59][_0x31a161(0xb8b)]&&_0x5542ff<0x1&&(_0x3f8460['push'](_0x21c0c8[_0xe3b59]),_0x5542ff+=0x1);}return _0x3f8460;}async function setupSpeedtest(){var _0x765942=_0x165386;isIFrame&&session[_0x765942(0x551)]&&await chooseBestTURN();}async function getTURNList(){var _0x292a7e=_0x165386,_0x52aaf8=[],_0x47ffd8=Date[_0x292a7e(0x30e)]()-0x180f0b4b67c,_0x5b84e8='',_0x11b89a='https://turnservers.vdo.ninja/';if(location['hostname']===_0x292a7e(0x4c0))_0x11b89a=_0x292a7e(0xc0e);else location[_0x292a7e(0x9f3)]==='vdo.socialstream.ninja'&&(_0x11b89a=_0x292a7e(0x8f9));if(session[_0x292a7e(0x551)])_0x11b89a+='speedtest',typeof session[_0x292a7e(0x551)]==_0x292a7e(0x9cb)&&(_0x5b84e8=_0x292a7e(0x2b4)+session[_0x292a7e(0x551)]);else{if(session[_0x292a7e(0x486)]&&typeof session[_0x292a7e(0x486)]==_0x292a7e(0x9cb))_0x5b84e8=_0x292a7e(0x2b4)+session[_0x292a7e(0x486)];else try{_0x52aaf8=getStorage(_0x292a7e(0x511))||![];if(_0x52aaf8)return!session[_0x292a7e(0x972)]&&(session[_0x292a7e(0x972)]=[]),_0x52aaf8=processTURNs(_0x52aaf8),!_0x52aaf8&&(_0x52aaf8=[]),session[_0x292a7e(0x7d3)]={'iceServers':session[_0x292a7e(0x972)],'sdpSemantics':session[_0x292a7e(0x76d)]},session[_0x292a7e(0x486)]&&(session[_0x292a7e(0x7d3)]['iceTransportPolicy']=_0x292a7e(0x731)),session[_0x292a7e(0x7d3)]['iceServers']=session['configuration'][_0x292a7e(0xae4)]['concat'](_0x52aaf8),session[_0x292a7e(0xa60)]=typeof buildQosTurnAllowlist===_0x292a7e(0x32c)?buildQosTurnAllowlist(_0x52aaf8):[],!![];else _0x52aaf8=[];}catch(_0x591c74){errorlog(_0x591c74),_0x52aaf8=[];}}return await fetchWithTimeout(_0x11b89a+_0x292a7e(0xce2)+_0x47ffd8+_0x5b84e8,0x7d0)[_0x292a7e(0x998)](_0x34ecd6=>_0x34ecd6[_0x292a7e(0x4e8)]())[_0x292a7e(0x998)](function(_0x3660b5){var _0xa83c1a=_0x292a7e;_0x3660b5[_0xa83c1a(0x8bf)]['forEach'](_0x95f5c8=>{var _0x230c70=_0xa83c1a;try{if(session[_0x230c70(0xc16)]&&_0x95f5c8[_0x230c70(0xb8b)]){}else _0x52aaf8[_0x230c70(0x5e7)](_0x95f5c8);}catch(_0x3d5af5){errorlog(_0x3d5af5);}});if(isIFrame&&_0x3660b5[_0xa83c1a(0x7fc)]&&session[_0xa83c1a(0x551)]&&!session[_0xa83c1a(0x7c7)])pokeIframeAPI(_0xa83c1a(0xc56),_0x3660b5[_0xa83c1a(0x7fc)]);else!session['speedtest']&&setStorage('turnlist',_0x3660b5['servers'],0x1);})[_0x292a7e(0xacc)](function(_0x2f0fbd){var _0x2d9157=_0x292a7e;warnlog(_0x2f0fbd),_0x52aaf8=[{'username':_0x2d9157(0x5af),'credential':_0x2d9157(0xb89),'urls':[_0x2d9157(0x7d5)],'tz':0x12c,'udp':![],'locale':'cae1'},{'username':'steve','credential':_0x2d9157(0xb89),'urls':[_0x2d9157(0x6a4)],'tz':0x12c,'udp':!![],'locale':_0x2d9157(0xc58)},{'username':_0x2d9157(0xc1e),'credential':_0x2d9157(0x4d4),'urls':[_0x2d9157(0xbbc)],'tz':0x1e0,'udp':!![],'locale':_0x2d9157(0x897)},{'username':_0x2d9157(0xc1e),'credential':'PolandPirat','urls':[_0x2d9157(0xbf3)],'tz':-0x46,'udp':!![],'locale':_0x2d9157(0x5a5)},{'username':'steve','credential':_0x2d9157(0xb89),'urls':[_0x2d9157(0x8fd)],'tz':-0x3c,'udp':![],'locale':_0x2d9157(0xb33)},{'username':_0x2d9157(0x5af),'credential':_0x2d9157(0xb89),'urls':[_0x2d9157(0x545)],'tz':-0x3c,'udp':!![],'locale':_0x2d9157(0xb33)},{'username':_0x2d9157(0xc1e),'credential':_0x2d9157(0x3dc),'urls':[_0x2d9157(0x23f)],'tz':0x12c,'udp':!![],'locale':'use1'}],_0x52aaf8=processTURNs(_0x52aaf8);}),!session['stunServers']&&(session[_0x292a7e(0x972)]=[]),session[_0x292a7e(0x7d3)]={'iceServers':session[_0x292a7e(0x972)],'sdpSemantics':session[_0x292a7e(0x76d)]},session[_0x292a7e(0x486)]&&(session[_0x292a7e(0x7d3)][_0x292a7e(0x766)]='relay'),!_0x52aaf8&&(_0x52aaf8=[]),session['configuration'][_0x292a7e(0xae4)]=session[_0x292a7e(0x7d3)][_0x292a7e(0xae4)]['concat'](_0x52aaf8),session[_0x292a7e(0xa60)]=typeof buildQosTurnAllowlist==='function'?buildQosTurnAllowlist(_0x52aaf8):[],log('Remote\x20TURN\x20LIST\x20Loaded\x20**\x20'),!![];}var TURNPromise=null;function _0x5b65(){var _0x38b5db=['primary','recording_audio_compressor_type','rtc\x20data\x20channel\x20error:\x20','limitTotalBitrate','plant','videoOptions','train','noREMB','EncodedVideoChunk','chunkedRecorder','GOT\x20CHUNKED\x20DETAILS','insect','shout','descriptors','broadcastChannelID','virtualcam','predAudio','slotmode','webkitAudioContext','err','chart','hash','remember','fecParityBlocks','stopPropagation','aec_url','segment','muteStateTemplate','transferSettings','which','waitPage','obsControls','autorecord','drop','allowScreenAudio','nofullwindowbutton','reconnectPeer','PUBLISHER\x27s\x20RTC\x20Connection\x20seems\x20to\x20be\x20dead?\x202','rpc\x20datachannel\x20closed','lady','room_init','activeSpeaker','equalizer','lockWindowSize','createGain','operate','audiobitrate','getChannelData','no\x20video/audio\x20config','Someone\x20is\x20trying\x20to\x20transfer\x20a\x20guest','getReader','turn:turn-eu4.vdo.ninja:3478','Seeding\x20with\x20real\x20stream\x20ID:\x20','toward','major','decrypted','location','Failed\x20to\x20determine\x20size\x20of\x20element','Media','chunkchunksize','optimizedBitrate','defaultIframeSrc','follow','group-set-updated','youtubeKey','stand','Refreshing\x20scale','pipe','byteLength','canvasIntervalAction','send\x20channel\x20open\x20pcs','https://www.youtube.com/','woman','Peer-to-Peer_Connection','whepRequested','content-type','baseLatency','human','https://turnservers.rtc.ninja/','onconnectionstatechange\x20pcs\x20ice\x20--\x20disconnected,\x20but\x20not\x20yet\x20closed?\x20','directorChat','voiceIsolation','maxBufferSize','activeSpeakerInterval','realTimeAudio','screenShareState','forceTcpMode','draw','change','loadoutID','ride','song','postInterval','stream_key','vdoninja','valley','pendingWhepMarker','encodeRemote','vp09.00.10.08','pushLoudness','setAttribute','restartIce','RTC\x20Connection\x20seems\x20to\x20be\x20dead\x20or\x20not\x20yet\x20open?\x204','resume','whipOutputScreen','fire','listPromise','box','vdav','ICE\x20restart\x20failed\x20for\x20rpcs:\x20','GOT\x20ICE!!','seat','chunkIframe','bit','remote-group-change','prefer-hardware','active','Unhandled\x20Error\x20occurred','micIsolated','Remote\x20peer\x20disconnected.\x20Due\x20to\x20enhanced\x20security,\x20please\x20refresh\x20to\x20create\x20a\x20new\x20connection.','allowMIDI','session.provideFileList','UUID\x20not\x20found;\x20can\x27t\x20close','screenShareLabel','must','uwxixfldkii1xpt','sent','I\x27m\x20not\x20sure\x20if\x20I\x20should\x20hang\x20up\x20the\x20whip\x20Output\x20or\x20not','An\x20RTC\x20error\x20occurred','chunkadapt','New\x20ON\x20TRACK\x20event','STREAM\x20ID\x20desalted\x202:','print','round','Bad\x20EBML\x20datatype\x20','chunkcache','ceil','webcamonly','mirrorGuestTarget','option','gun','writeEBMLVarIntWidth','control','anon','unshift','little','download','inboundAudioPipeline','drive','mutedState','available-speedtest-servers','alt','cae1','rock','room-is-claimed-codirector','codirectorSettings','quality','directorHash','score','her','createElement','river','audioInputChannels','\x20queued=','describe','User\x20blocked\x20from\x20room','allowwhipout','outputLatency','videoMutedFlag','was','seedPlz','mono\x20enabled','came','audioHeaderSent','chunkbuffer','Chunked\x20queue\x20overflow,\x20dropping\x20all\x20entries\x20(no\x20metadata\x20for\x20alignment)','getOBSOptimization','acceptsTips','3186bAclWb','cross','resumeClock','autoSync','mids','skipped','about','\x20(fair)','toString','OBSNINJAFORLIFE','suggest','phrase','chunkedtransfer\x20OPEN','ICE\x20GATHER\x20COMPLETED','autochannelmode','small','condition','drink','/api/gateway/whep/','alpha','shore','limitaudio','sand','back','VP8','wonder','die','channel','noiseSuppression','second','directorSettings','The\x20other\x20end\x20is\x20just\x20being\x20a\x20keener.\x20Ignore\x20it:\x20','limiting\x20AudioEncoder','classList','subarray','plane','header','audioEncoder','digest','time_second\x20missing','imageElement','limitAudioEncoder','intime','BlobBuffer','bypass','waiting\x20for\x20keyframe','disabled','error','fine','filterOBSscenes','cleanOutput','east','enterThreshold','streamSrc','ariaPressed','doNotSeed','roomclaimed','science','remote-control-failed','googleDriveRecord','let','refreshVideo','whipCallback','recordWindow','thick','minLead','new-push-connection','autoSyncCallback','video_init_width','forceios','other','directorDisplayMute','addEventListener','did','never','keyframeSent','though','rebufferTimer','pcm','ice\x20restart\x20real','bit\x20rate\x20being\x20munged','#obsRemotePassword>input','Room\x20is\x20full','nackAttempts','outboundAudioBitrate','\x27\x20target=\x27_blank\x27>','grand','nothing','hair','yourDirectorStatus','soloVideo','Guest:\x20','screenshareid','updateDrawing','midiIn','kind','iframeSrcs','Someone\x20Joined\x20the\x20Room\x20with\x20a\x20video','watch','nodownloads','tilt','lockedAudioBitrate','none','requestVideoRecord','energy','\x20UUID(s)\x20from\x20the\x20director\x27s\x20list.','solve','nation','whipOutCodec','Remote\x20user\x20is\x20a\x20director','system','bank','?ts=','need','wrong','nomirror','disableNACK','ondatachannel','moon','iOS\x2013\x20and\x20below\x20do\x20not\x20support\x20dynamic\x20bitrates\x20correctly;\x20skipping','getParameters','layout_array','receiveTips','tipId','begin','provide','claim','guest','mid','showConnections','hangupDirector','arm','style','pay','anger','changeURL','retransmits','director-denied','scaleSnap','Performing\x20ICE\x20restart\x20for\x20viewer\x20','result','natural','directorEnabledPPT','querySelectorAll','hiddenSceneViewBitrate','unmuted','are','rejoinMargin','charAt','copyTo','your','debug.vdo.ninja','what\x20is\x20this?','experience','Created\x20transfer\x20channel','micDelay','requestStats','maxframeRate','controls_','support','calculateOptimalBufferSize','miss','cool','chunkedInQueue','Trying\x20to\x20set\x20','opacityDisconnect','videoMuted:\x20','studioSoftware','return\x20','lyraCodecModule','unit','recording_audio_gain','buffer_outputLatency','remote','performanceStart','allowGraphs','wont','pendingApprovalStreamIDs','coast','onreadystatechange','restartChunkedMode','widgetSrc','join','sit','nextParityIndex','IPv6\x20filtering\x20error\x20(WHIP\x20gather):','obsControl','requestChangeMicDelay','cleanish','removeOrientationFlag','resolution','sendMessage','BROWER\x20DID\x20NOT\x20SUPPORT\x20LIMIT\x20BITRATE','often','directorDisplayMuted','framerate','fun','/whip','requestAudioRateLimit','grass','initialPublish','Remote\x20request\x20failed\x20to\x20decode;\x20continuing\x20still.','stopping\x20some\x20preload\x20bitrate\x20','image/webp','sendChunks','stun:stun.l.google.com:19302','toFixed','stashes','still','forceNoVideoWhipIn','village','needKeyFrame','led','syncDrawOnVideo','overlay','addALabel','maxviewers_url','figure','listing','WHY\x20ARE\x20YOU\x20GOD\x20DAMN\x20BEEPING','from','audioChannels','whepSettings','showControls','pair','engine','outgoing','truck','done\x20setting\x20degrad','Offset\x20may\x20not\x20be\x20negative','compressSDP','enough','downloads','teeth','wss://wss.vdo.ninja:443','hunt','retransmit','closePC','chunkadaptceil','chunkedStream','FORCING\x20A\x20KEY\x20FRAME:\x20','tipServer','CLOSED','https://','best','been','self','right','reliability','selfBrowserSurface','setValueAtTime','chunkSize','port','ICE\x20FAILed.\x20bad?','ID:\x20','recordingInterval','video_muted_init','.battery-level','tokenDirector','rollback','fit','transparent','disableWebAudio','much','can\x27t\x20change\x20bitrate;\x20no\x20video\x20senders\x20found','wheel','click','office','some','tallyStyleDefault','midiIframe','incoming','defaultSpeaker','requestVideoHack','Bearer\x20','turn:turn-use1.vdo.ninja:3478','noisegateSettings','url','GDRIVE_CLIENT_ID','writeByte','already\x20connected\x20to\x20websocket\x20server','please','Chunkcast\x20WebSocket\x20disconnected','innerHTML','already\x20connected\x202.\x20disconnecting..','roomTimer','hssConnection','reduce','afraid','had','rather','setScale','transportType','fake\x20ice\x20restart\x20faked','fruit','isInteger','displayMute','final','quite','allowDirectorGraph','speakerMute','legacyFrameId','allowscreenvideo','cacheOrder','dream','allowBroadcast','mutedStateMixer','edge','board','sending\x20message\x20via\x20WSS\x20as\x20WebRTC\x20failed\x20to\x20send\x20message','null\x20ice\x20rpcs','changeMicrophone','wish','simple','INITIAL\x20PUBLISH\x20START:\x20','scaleResolutionDownBy','scene','RTC\x20Connection\x20seems\x20to\x20be\x20dead\x20or\x20not\x20yet\x20open?\x203','chunked-inbound','updateTime','fast','chunkcast','chance','excite','writeString','sharperScreen','paragraph','guess','PASSED','iframe','setOpusAttributes','shouldn\x27t\x20happen','rotated','approval-accept','authMode','power','setVideoBitrates','getLocalStream','requestChangeMicPanning','getOpusBitrate','smell','NOT\x20VIEW\x20TARGET','rpcs\x20onconnectionstatechange\x20Disconnected;\x20retry\x20in\x205s','avatar','AudioContext','sensorDataFilter','closedCaptions','stats','encodedInsertableStreams','than','stretch','speakerMuted_default','sendKeyFrameScenes','token-not-director','width','whipServerURL','lin','raise','yard','vDav','autohide','allowVideo','while','ice\x20timer\x20no\x20longer\x20exists','pushEffectsData','pendingWhepStarted','least','someonejoined','plugged_in','directorActions','setBitrate','controlRoomBitrate','director-share','CONNECTED\x20TO\x20FIRST\x20PEER','viewwidth','openscene','filtered','quiet','children','crypto','realUUID','needsLoading','\x20:\x20','appear','publicKey','metadata','bufferSize','initial_group','Chunked_video','million','getElementById','those','&code=','rampUpTime','windowed','popupChat','encodering\x20being\x20kicked','new-view-connection','enhanceAudio','videoDevice','RUNNING\x20CALLBACK:\x20','storeReliabilityEntry','changeCamera','GDRIVE_FOLDERNAME','showmeta','accept_layouts','five','last_underflow','son','micIsolatedAutoMute','joinRoom','noisegate','broadcast=','autoSyncObject','chunksQueue','offerSDP','createAnswer','enhanceaudio','directorBlindAllGuests','undefined','Transfer\x20was\x20cnacelled\x20by\x20remote\x20user;\x20parital\x20file\x20saved.','delayTime','refreshMicrophone','viewDirectorOnly','screenSrc','\x20(ok)','joining-room','wss://api.vdo.ninja:443','setClock','against','importKey','winter','this\x20unverified\x20director\x20was\x20already\x20connected;\x20not\x20going\x20to\x20send\x20my\x20director\x20state\x20to\x20them','load','pruneReliabilityCache','samplingFrequency','blood','degrade','directorBlue','icefilter','audioMutedOverride','directorBlindButton','getOptimizedScale:\x20','setParameters','sid','codirector_changeURL','warn','iFramesAllowed','already\x20watching\x20stream','block','third','bigPlayButton','msg','random','chunkadaptinterval','nochunk','waitingWatchList','has','whipOutSetScale','wife','recordConfig','seed','maxptime','several','ON\x20FOCUS\x20NOT\x20FOUND','midi','debug','value\x20there','whipOutKeyframe','stone','own','hangupbutton','ORIGIN_NOT_ALLOWED','p2p','forEach','flow','pliCount','SDP\x20Sessions\x20Match.\x20I\x20assume\x20ADDING\x20TRACKS.\x20RPCS','gray','screen','badStreamList','createBufferSource','now','resolve','realStreamID','addTrack','writeDoubleBE','tip','defaultMedia','similar','inch','RECONNECTING\x20to\x20HSS;\x20DISCONNECTING\x20FROM\x20TRANSFERRED\x20ROOM','vDAv','heard','getConnectionMap','justResetting','danger','face','moment','sat','currentSrc','\x20attempts','copy','note','whose','minipreview','huge','wire','colony','CriOS','highlightDirector','HANG\x20UP\x202\x20COMPLETE','function','screenStream','boy','dear','av01.0.04M.08','meshcastWatch\x20called\x20-\x20this\x20meshcast\x20version\x20is\x20deprecated\x20in\x20favour\x20of\x20WHEP.','preloadbitrate','feed','startTime','sendframes','ptime','PCS:\x20ICE\x20Disconnected;\x20wait\x20for\x20retry?\x20pcs','isView','Bad\x20UINT\x20size\x20','long','dynamicScale','iframeVideo','disableViewerWebAudioPipeline','visible','hidehome','chunked_mode_audio','Shadow\x20mode:\x20not\x20seeding','encodings','added\x20video\x20track','trip','closing\x207','try','decryptMessage','refreshAll','lowerhand','pipWindow','frames_dropped','webrtc\x20connectioned\x20closed-event','mount','group','ruleOfThirds','mainmenu','hot','coDirector','rotate_video','idea','penalty','meat','target','Restarting\x20since\x20closed','rotate','resources','replace','shoe','running','with','closeRPC','subtract','endsWith','recording','hands_','widgetURL','soloChatUUID','createDelay','scaleFactor','limitAudioBitrate','shoulder','town','showUnMuteState','requestUpload','minptime','processDescription2','api','skin','time_seconds','true','difficult','Stream\x20ID\x20is\x20already\x20in\x20use.','display','promise','fullscreen','mute','limitTotalBitrate_defaultMax','ctrlKey','audioTime','seven','prompt-access-request','turn:','disableREMB','whipOutputToken','local','forceScreenShareAspectRatio','unknown','past','webrtc-is-blocked','cacheBytes','versus.cam','home','PUBLISHER\x27s\x20RTC\x20Connection\x20seems\x20to\x20be\x20dead?\x20','nextFrameId','Opened\x20transfer\x20channel','onclose','requestAs','localNetworkOnly','timeout','Pinging','Unmute\x20video','RPCS\x20for\x20MESHCAST\x20ISNT\x20MADE\x20YET??','Rotated\x20TURN\x20order\x20(simple)','restricted','Error\x20in\x20debug\x20logging:','art','iceBundle','successfully\x20requested\x20audio\x20and\x20video?\x20maybe?','motion','scaleWidth','[data-action-type=\x22remove-queue\x22][data--u-u-i-d=\x22','setFloat32','bar','born','syncState','cent','Video\x20File','virtualHangup','message','quick','position','audioPromise','nodirectorvideo','learn','handlePublisherMessage','subject','ease','directVideoMuted','eight','capital','changeOrder','seedStream','sun','race','pendingViewers','dataChunks','[queue-sync]\x20','answer','noPLIs','channelWidth','targetBandwidth','sendMsg','chunked_mode_video','sing','FAIL\x20rpcs\x20onconnectionstatechange','bright','set-video-scale','orderby','clear','king','overlayNinja','record','type','postMessage','cow','preferAudioCodec','lowBitrateCutoff','mix','Guest','postURL','disableBackground','bandwidth','tipAmounts','createWriteStream','parentNode','filename','83079GVMGmj','stunOnly','clock','savedVolume','pos','you-are-a-codirector','Keyframe\x20requested','post','shadowBanned','EastSideRepresentZ','session.watchTimeoutList\x20no\x20longer\x20exists;\x20won\x27t\x20retry.','chunkadaptfloor','/api/publish/anonymous','above','\x20oldest\x20chunks','setUint32','getWrittenSize','Connected\x20to\x20Chunkcast','opacityMuted','nacks_sent','viewerHealth','An\x20RTC\x20error\x20occurred.','UUID\x20not\x20found;\x20can\x27t\x20close.','connect','requestChangeGating','table','allowVideos','bandwidthMuted','close','coDirectorEnable','.webm','maxFrameDrop','choose','Update\x20Mixer\x20Event\x20on\x20Resize\x20SET','white','his','getSenders','young','camp','way','anyrequest','vowel','since','Audio\x20Bitrate\x20is\x20locked;\x20can\x27t\x20update','base','Mute\x20video\x203306','request-rejected-obs','savedBitrate','rejoining\x20room','rpcs','finger','twenty','ICE:\x20','Guest\x20','save','directorState','remoteTilt','inputBuffer','responseText','ICE\x20GATHER\x20START','ensureReliabilityFrameId','north','proaudio','high','iceConnectionState','especially','blow','fail','water','GOT\x20ICES!!','pretty','statsInterval','hat','mother','seeding','obsStateSync','reload','flipOutput','dataReceived','__whepAutoSmallScreen','stopWriter','study','closeTimeout\x20cancelled;\x204','3170868AbxuVt','scene-connected','readAsArrayBuffer','restartWhip','remoteHash','death','seeding-started','prepare','widgetleft','bitrate','sticky','Someone','screenshareContentHint','application/sdp','stack','push-connection','roomTimerGlobal','remoteFocus','deleting\x20watch\x20list','Transfer\x20ended','look','chunkbufferceil','fullscreenButton','set','happen','height','log','Restarting\x20WHIP\x20connection\x20per\x20director\x20request','approval_popup','question','obsState','maxCacheBytes','farm','scaleHeight','brought','closing\x202','audioCtx','codec','outboundSampleRate','.hidden2','queryQueuedStatus','street','like','applyIsolatedVolume','stream','frameRate','process','better','thus','videoMargin','lockedVideoBitrate','activeWhepMarker','light','RSASSA-PKCS1-v1_5','ptz','directorMutedState','ontrack','decodeQueueSize:\x20','candidates','under','wood','Does\x20Local\x20Stream\x20Source\x20EXIST?','sceneType2','bought','getStats','wall','getPCM','Round_Trip_Time_ms','can\x27t\x20change\x20bitrate;\x20no\x20video\x20sender\x20found','speed','optimize','allowmeshcast','lyra','\x20---\x20PC\x20TIMED\x20OUT,\x20but\x20still\x20alive.\x20Killing\x20it.\x20via\x20disconnected\x20state','updateOnSlotChange','auto','sensors','chick','buffer_lead','lastPingToken','decodeRemote','opacity','recording_audio_pipeline','prototype','expect','hole','indicate','scaleResolutionDownBy\x20set\x202a!\x20','focus','announceCoDirector','broadcastTransfer','power_level','silence','writable','cpu','No\x20meshcast\x20server\x20found\x20that\x20worked','privacy','UN-MUTED','createResourceChannel','Shadow\x20mode:\x20not\x20watching\x20streams','ifs','maintain-framerate','keyname','degradationPreference','remoteDescription','gdrive','orientation','http://','ICE\x20DISCONNECTED','encryptedToReal','flagship','rotation','once','connected','getSettings','enhance_audio','\x20---\x20PC\x20TIMED\x20OUT\x20and\x20already\x20deleted.\x20shouldn\x27t\x20happen','\x20is\x20not\x20defined;\x20skipping.','**\x20connected','failed\x20to\x20disconnect','not\x20allowed\x20to\x20show\x20the\x20director','whipOutputUserSet','no\x20video\x20track\x20to\x20control','subtle','activatedStreamsQueue','currentCameraConstraints','https://tip.vdo.ninja','come','concat','channelOffset','noTipQR','RTC\x20already\x20connected','activeSpeakerTimeout','enhanceAudioEncoder','chunkedNacksHandled','know','video_init_frameRate','hand','applyQueueStateChange','invalid-remote-code','bitrate_set','widgetwidth','optionalMicOnly','also','motionSwitch','no\x20pc[UUID]\x20found','effectValue_default','spend','decode','Answer\x20is\x20a\x20data-channel\x20only\x20SDP','keyframeTimeout','basic','currentTarget','incoming\x20screen\x20share\x20started\x20loading','rtc.ninja','mystery-message-recieved','getAsDataArray','unsafe','GET','instrument','send','signalMeter','rail','closing\x206','else','parityIndex','privateKey','chunked-outbound','empty\x20ice..','buffer_timestamp','PCM\x20STARTED','screenshareAutogain','href','Second\x20Thread\x20Waiting\x20for\x20TURN\x20LIST\x20to\x20load','theyBeSharksHere','three','mirrorState','saturation','session.chunkedRecorder\x20is\x20not\x20false','signData','optimizeBitrate','you','imagine','whipOutScreenShareBitrate','timedelta','TOO\x20MANY\x20PUBLISHING\x20PEERS','state','labelstyle','broadcast','window','alert','thousand','quart','Encryption\x20is\x20required,\x20but\x20none\x20found.\x20Cancelling.','json','whitelistDomain','sourceActive','down','endViewConnection','coat','PCS\x20WINS\x20ICE','part','Not\x20supported;\x20expected\x20\x27filetransfer\x27','view_set','settings','Failed\x20to\x20connect\x20to\x20Meshcast.\x0a\x0aCheck\x20your\x20connection\x20or\x20switch\x20to\x20peer-to-peer\x20mode\x20instead.','borderColor','pop','audiobitratePRO','noScaling','candidate','remoteVideoMuted','meshcastMenu','measureUnsignedInt','frameDropBudget','edgelist','novideo','requestSceneUpdate','exclusiveLayoutAudio','metal','floor','\x20as\x20preferred\x20video\x20codec\x20by\x20viewer\x20via\x20API\x20(offer)','maxvideobitrate','createChunkedVideoController','ended','not\x20record\x20button\x20detected;\x20can\x27t\x20update\x20time\x20since\x20started\x20recording','company','756186pCrVUi','joiningRoom','directorSpeakerMuted','nack','track','reason','remove','person','turnlist','maxMobileBitrate','strong','nose','playsinline','Valid\x20co\x20director\x20trying\x20to\x20transfer\x20a\x20guest','broad','wide','realtime','oxygen','proaudio_init','numberOfChannels','groupStart','divide','obs','USD','Authorization','PROBLEM,\x20Senders\x20is\x20more\x20than\x200:\x20','limitBitrate','streams','text','retransmitChunkedStream','decodeQueueSize','setVideoBitrate','midiChannel','isolation_url','whipOut','addIceCandidate',',\x20mc?:\x20','realTimeVideo','fec_repairs','\x20---\x20we\x20will\x20ask\x20again','through','chunkedTransferChannels','city','RTCRtpSender','contentType','viewslot','object','sessionUri','processPCSOnMessage','audioDevice','Track\x20stopped','size','parityDescriptors','maxBandwidth','remoteMuteState','minimumRoomBitrate','agree','Sent\x20connection\x20map\x20to\x20director:\x20','mediafileShare','audio','turn:turn-eu1.vdo.ninja:3478','VP9','remoteMuteState_','refreshConnection','hold','noScreenShare','hideDirector','password','generateStreamID','buffer_buffer','find','effectValue','speedtest','nature','brother','leg','direct','Should\x20we\x20ask\x20to\x20play\x20the\x20stream\x20Again?','audioConstraints','slots','limitTotalBitrateGuests','split','createdAt','promptApproval','suppressLocalAudioPlayback','notifyScreenShare','setConfiguration','cleanup','limitTotalBitrateAll','Cancelling?\x20no\x20more\x20chunked\x20connections.\x20I\x20probalby\x20shouldn\x27t\x20be\x20stopping\x20if\x20recording\x20also.','UNKNOWN','sheet','indexOf','theirtime','335465CNiiTa','getAudioTracks','visibility','midiDevice','muted_savedState','time','suit','whipOutAudioBitrate','whepHost','legacywebrtc','offsetChannel','screenShareBitrate','main-director','deep','mouth','welcomeImage','batteryState','Chunkcast\x20WebSocket\x20not\x20ready,\x20queuing\x20chunks','TrackNumber\x20must\x20be\x20>\x200\x20and\x20<\x20127','lastUnderflow','raisehands','Removed\x20','volume','element','h264profile\x20being\x20modified','opposite','midiDelay','surprise','disableOBS','guest-info','solo','showClock','IPv6\x20filtering\x20error:','vdoAuth','protect','offset','AES','directorMirror','desaltStreamID','lastWhepUrl','hanging\x20up','adjustBitrate','showList','product','stood','lastWhepToken','prove','spell','STREAM\x20ENDED','legacyMode','custom\x20layout\x20being\x20applied','girl','generator','warnUserTriggered','cameraConstraints','highPriorityPressure','start','this','labelSetByDirector','stopping\x20old\x20track','school','ctrl','pol1','VDO-Ninja','permaMirrored','poor','chunkadaptmaxdrop','noaudio','safe','chunkjitterslack','Performing\x20offer-based\x20ICE\x20restart\x20for\x20viewer\x20','handleNack','steve','buffer_level','isArray','whipOutVideoBitrate','requestRateLimit','getAudioSettings','cloneNode','pcs\x20RTC\x20CLOSED','modifyDescLyra','usual','Decryption\x20error:','vb_url','destination','remoteMuteElement','RTC\x20Connection\x20seems\x20to\x20be\x20dead\x20or\x20not\x20yet\x20open?\x202','removeChild','&start=','stable','SET\x20SCALING\x20IS\x20FIRING,\x20which\x20is\x20GOOD\x20!!!!!!\x20','onended','notice','No\x20realtime','totalBitrate:\x20','streamid-already-published','getVideoTracks','mirrorExclude','remote-label-changed','create\x20offer\x20worked','-kbps','sendChannel','hit','sendingBuffer','what','point','during','continue','scale\x20scale','redAudio','substance','container_director','scale\x20set!\x20','supported','closeTimeout\x20cancelled;\x202','create','appendChild','done\x20clearing\x20audio','description','mixMinus','queueType','label','video_2_init_width','fell','Room\x20is\x20full.\x0a\x0aThe\x20room\x20you\x20are\x20trying\x20is\x20join\x20is\x20act\x20its\x20max\x20capacity.','miconly','obs.ninja/','src','push','closed','micSampleRate','completed','live','give','AES-CBC','smid','infocusForceMode','EBML\x20VINT\x20size\x20not\x20supported\x20','systemAudio','teach','chunkReliability','screenShareStartPaused','getVideoBitrates','srcObject','found','wss://pipe.vdo.ninja:9001/','fly','complete','ab_url','chord','first','Chromium-based\x20v','totalSceneBitrate','design','frameType','startsWith','burn','experiment','glad','shall','maintain-resolution','watchTimeoutList','ICE\x20restart\x20triggered\x20for\x20rpcs:\x20','cancel','media','Notice:\x20Meshcast\x20does\x20not\x20support\x20Insertable\x20Streams\x20(or\x20E2EE)\x20at\x20the\x20moment','setTargetAtTime','straight','__started__','delta','set-audio-bitrate','nacks_per_second','loadend','codecGroupFlag','TFJSModel','sky','voice','closing\x204','packetLoss','replaceAll','previewToggleState','videoWriter','#guestFeeds\x20[data--u-u-i-d=\x27','allowResources','Members\x20in\x20Room','webp','pipeTo','tie','meshcastCode','layout','whip','iceGatheringState','createMediaStreamDestination','videosource_','match','buffer_timedelta','bandwidth\x20set\x20i!\x20','went','26UvXWgn','lowerVolume','buffer_delta','tfliteModule','eye','[data-action-type=\x22order-value\x22][data--u-u-i-d=\x22','two','iframeDetails_','webAudios','onerror','disableHotKeys','stop','couldn\x27t\x20send\x20a\x20request\x20to\x20specified\x20publishe\x20via\x20p2p:\x20','held','exposure','creating\x20answer','hidden','remoteStats','micSampleSize','student','infocus2','max','air','null','approval-deny','unspecified','noExitPrompt','autostart','wssid','requestChangeLowcut','disableIpv6','recorder','approval-','ICE\x20closed?','totalBytes','status','deviceId','bufferFullness','autoplay','webCodecAudio','./media/bg_sample2.webp','reject\x20co','buffer','screenWhepAllowed','Max\x20bandwidth\x20NOT\x20being\x20capped:\x20','not-the-director','lastSend','rmid','hash\x20is\x20','road','have','consider','each','bear','wrote','ontimeout','onnegotiationneeded\x20triggered;\x20creating\x20offer','get','animatedMoves','care','grabFaceData','pingTimeout','playChannel','chunkedDroppedFrames','Failed\x20attempt\x20to\x20connect\x20as\x20co-director','screenWhepPreference','container_','allowWidget','guide','near','swim','currentTime','root','rtc\x20state:\x20','gotGenericData','oniceconnectionstatechange','where','could\x20not\x20be\x20sent;\x20queuing\x20it','data','displaySurface','play','discordHook','video/webm','stream\x20ID\x20is\x200\x20length','obsSceneTriggers','nopreview','totalRoomBitrate_default','bitrate\x20timeout;\x20ios/firefox\x20specific:\x20','south','upstreamChannel','decimal','releaseLock','createBuffer','black','pitch','\x20as\x20preferred\x20codec\x20by\x20viewer\x20via\x20API','locale','current','page','AndroidFix','quality_room','remote-peer-connected','disconnected;\x20no\x20reconnect\x20even\x20after\x205s;\x20closing','Bitrate\x20request:\x20','socialstream','stun:stun.cloudflare.com:3478','maxCacheMs','https://meshcast.io/servers.json?ts=','nextQueue','whipoutSettings','temperature','Video\x20encoding\x20failed.\x20Please\x20try\x20refreshing\x20your\x20browser.','createJavaScriptNode','firefox','modern','preferCurrentTab','taintedSession','letter','__whepPrevSmallScreen','turn:turn-cae1.vdo.ninja:3478','nacksSent','statsMenu','recieveFile','RTC\x20closed','chair','party','room','chunkadaptthreshold','remoteExposure','videoaddedtoroom','encrypt','selectedIndex','chunkedVideoEnabled','audioCtxOutbound','qosEnabled','screensharebutton','view-connection','ignoreNextSpeakerToggle','layoutState','remoteInterfaceAPI','example','noon','audioBitrate','frameWriter','adaptivePtime','Assumed:\x20','screenShareElement','offer','backup.vdo.ninja/','Chunkcast\x20WebSocket\x20Error:','child','plan','hundred','max_bandwidth_capped_kbps','ten','steam','tone','retryScenes','trouble','videoMuted','store','directorSpeakerMute','autochannels','world','stringify','added\x20audio\x20track','Change\x20Label','started','name','Chunked\x20queue\x20overflow,\x20dropping\x20','roomhost','permaid','signalingState','processIce2','work','getUint32','iceCandidatesPromise','house','WHIP\x20restart\x20requested\x20but\x20no\x20WHIP\x20connection\x20active','Meshcast2\x20anonymous\x20bandwidth\x20limit\x20reached.\x20Premium\x20account\x20required.','weather','ready','modifyDescPCM','no\x20UUID\x20in\x20msg','showlabels','limitAudio','requestChangeSubGain','pendingApproval','createDataChannel','delete','hostedTransfers','paint','canvasCtx','doNotSeed!','music','tire','gathering','test','_screen','silent','[data-action-type=\x22solo-video\x22].pressed','safemode','sent\x20via\x20relay\x20wss\x20anyways','KEY\x20FRAME\x20will\x20be\x20requested\x20from\x20the\x20seeder\x20on\x20behalf\x20of\x20a\x20seeder\x20...','canvas','and','1081880TGVFOX','jump','assign','changeSpeaker','hasOwnProperty','iOS\x20devices\x20do\x20not\x20support\x20dynamic\x20bitrates\x20correctly;\x20skipping','screensharecursor','circle','scaleResolutionDownBy\x20set\x202b!','obsstudio','initialDirectorSync','fileList','aspectRatio','resolution\x20scale:\x20','whepWait','extra','readyState','meet','roomid','canvasWebGL','dollar','hangupFallbackTimeout','restartWhipConnection','charCodeAt','enhance','autoPiPPrompt','glass','requestStatsContinuous','Failed\x20to\x20add\x20ICE\x20candidate:\x20','locate','approvalPrompted','tree','sceneDisplay','video_bitrate_kbps','GOT\x20ICEs!!','IPv6\x20filtering\x20error\x20(WHIP):','consecutiveHigh','frames','height_url','team','tell','no\x20upstreamChannel\x202','requestRateLimit\x20RUN:\x20','mirrored','recieveChunkedStream','sdp','createOffer','Connection\x20to\x20Control\x20Server\x20lost.\x0a\x0aWill\x20try\x20to\x20reconnect\x20in\x202\x20seconds.','\x20until\x20validated','videoEncoder','setupIncoming','compressor','soft','recordings','relay','CHUNKED\x20DETAILS','approved:\x20','necessary','verify','encode','tuning','audioNode','leavetone','We\x20will\x20not\x20request\x20the\x20meshcast\x20as\x20no\x20audio\x20or\x20video\x20is\x20requested','blindAllGuests','cleaning\x20up\x20lost\x20connection','triangle','defaultBackgroundImages','parity','recordedBlobs','apple','remote-video-mute-state','week','manual','range','lift','visit','numeral','BYE\x20RPCS','showSlider','init_video','beauty','newViewConnection','migrate','screenVideoOverride','removeTrack','ground','binaryType','behind','great','startWriter','\x20this\x20a\x20scene?','director-connected','a=extmap:3\x20urn:3gpp:video-orientation\x0d\x0a','Max\x20bandwidth\x20being\x20capped:\x20','writeFloatBE',')\x20failed\x20due\x20to\x20permissions\x20or\x20it\x20was\x20rejected\x20by\x20the\x20user','material','chunked','whipPublishScreen','thought','Chrome\x20iOS','allowmidi','anysend','changeParams','problem','Transfer\x20was\x20completed\x20successfully','warm','fair','updateurl','playbackheader','nohistory','metaKey','nackCount','/api/gateway/whip/','enqueue','cbid','code','roll','remote-token-rejected','dark','noNacks','directorStreamID','loudest','morning','iceConnectionState\x20==\x20connected','who','set-meshcast-video-bitrate','good','supply','ever','span','how','noWidget','Websockets\x20timed\x20out;\x2030000ms','done','TRANSFERRING?','stream_configAudio','usw2','changeLabel','ccColored','busy','maxviewers','bandwidth\x20set\x20c!\x20','You\x20might\x20already\x20be\x20connected\x20to\x20this\x20chunked\x20video\x20stream','stashed','enc','network_type','URL','queued','directorList','outputDevice','bind','done\x20setting\x20degrad\x20to\x20','outboundVideoBitrate','testtone','bundlePolicy','ori','this-is-you','createMediaStreamSource','spread','path','people','new-main-director','allowscreenshares','muted','lowMobileBitrate','directorVideoMuted','sceneMute','Waiting\x20for\x20keyframe\x20/\x20header\x20before\x20sending\x20delta\x20/\x20raw\x20video\x20data','dropped\x20candidate\x20due\x20to\x20filter','successfully\x20sent\x20message\x20vis\x20WebRTC\x20instead\x20of\x20WSS\x20to\x20all\x20RTC\x20Peers','ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789','joinroom','charging','flushPendingApprovals','chunkIndex','word','servers','allowAudio','Audio_Loudness','sendErrors','RETRANSMIT\x20chunkedtransfer\x20OPEN','single','constructor','voiceMeterTemplate','last','noMeshcast','closeTimeout\x20cancelled;\x207','Someone\x20sent\x20us\x20an\x20ANSWER\x20sdp??','chief','far','closeTimeout\x20cancelled;\x205','vp8','widget','resending\x20message','scaleDueToBitrate','Raised\x20hand','buy','were','pseudoguest','defaultOverlayMedia','him','this.connectionState:\x20','streamid-already-published-obvious','config','pendingResend','Auth\x20mode:\x20Deferring\x20media\x20for\x20','money','PCMSource','1107051LTSzAX','undo','even','layouts','fecRate','enabled','configure','vision-disabled','band','enqueueFrame','label_','pfecAudio','requestResolution','bat','RE\x20TRANSMISSIONS\x20STARTED','parityChunks','getWriter','speakerMuted','allowscreenaudio','trim','preventDefault','dress','well','vdo_block_','connectionSuccesses','love','https://turnservers.socialstream.ninja/','currentSlots','queue=false','[queue-sync]\x20sending\x20directorState\x20update\x20(','turns:turn.obs.ninja:443','climb','night','recieveResourcesChannel','ring','hidedirector','ship','gyro','share','vp9','molecule','head4','requesting\x20via\x20relaywss','either','no\x20audio\x20track\x20to\x20poke','HEADER\x20SENT?','sendFile','language','useragent','instant','writeU16BE','slice','micPanning','screenshareStyle','security','meshcastServersUsed','green','allowDownloads','whepInputToken','playback_audio_pipeline','govern','gone','border','noise','BITRATE\x201:\x20','directMigrateIssue','Handshake\x20has\x20a\x20vector?\x20But\x20we\x20don\x27t\x20have\x20a\x20password.\x20This\x20is\x20probably\x20going\x20to\x20fail...','disconnected','rejected','bottom','No\x20vector?\x20uh\x20oh\x20--\x20might\x20be\x20raspberry\x20ninja\x20or\x20some\x20other\x20simpler\x20implementation,\x20so\x20lets\x20move\x20on.\x20We\x27re\x20using\x20the\x20default\x20password,\x20so\x20we\x27re\x20going\x20to\x20allow\x20it','whipoutSettingsUserSet','chat','not\x20an\x20object\x20or\x20array','gather','whipCallback2','totalRoomBitrate','sending\x20message\x20via\x20WSS\x20as\x20WebRTC\x20failed\x20to\x20send\x20message;\x20RTC\x20peers\x20only','industry','closing\x208','pastSlots','m\x20:\x20','videosource','watchStream','forceRetry','depend','plural','sendRequest','checking','stick','welcomeHTML','stalled','recordDefault','setLocalDescription','noise\x20gate\x20on','retrying..','gain','infocus','877147493034-67tq62ds8cj54it6cr0ut24irm7t7q5g.apps.googleusercontent.com','dropboxAccessToken','hidesololinks','fecRepairs','keep','room\x20rate\x20restriction\x20detected.\x20No\x20videos\x20will\x20be\x20published\x20to\x20other\x20guests','hurry','encoder','famous','session','tallyOverride','locked','want','zoom','iframetarget','chunkedViewerSkips','equal','requestFile','video_2_init_frameRate','splice','configVideo','animal','qosStatsTimeout','connectionMap','directorPassword','alignRight','dtx','exact','sctpCauseCode','ceiling','frameReader.read().then(function','Keyframe\x20inserted','isolateChannel','vdo.ninja','throw','miniInfo','land','sight','mode','chunked-mode\x20KEY\x20FRAME\x20REQUESTED\x20BY\x20A\x20VIEWER','publish','forceMediaSettings','audioEffects','QUOTA_EXCEEDED','slot','reply','bandwidth\x20set\x20b!\x20','lastWhepMarker','activelySpeaking','stunServers','directorView','setResolution\x20triggered;\x20','processIceBundle','threshold','connectPeer','New\x20Label:\x20','caught','SCREENS','setResolution','autofocus','onopen','whep','that','couldn\x27t\x20set\x20preferred\x20audio\x20codec','enhacing\x20audio\x20encoder','codirector\x20request\x20hash\x20failed','nackEnabled','lot','seeding\x20!!','Safari\x20','whipOutputScreenUserSet','anon_','targetAudioBitrate','needsPublishing','priority','Target\x20for\x20scene\x20not\x20found;\x20retrying\x20in\x203\x20seconds','localMuteElement','customWSS','audioBuffer','showDirector','Found\x20target\x20for\x20scene\x20change','','read','Chunked\x20mode\x20failed.\x20Attempting\x20to\x20restart...','view-connection-info','hour','pose','then','pip','desert','videoPromise','getRemoteNow','available_outgoing_bitrate_kbps','onconnectionstatechange','stereo_url','EOF1','false','chunks','Can\x27t\x20change\x20the\x20location\x20once\x20started\x20streaming','application/json;\x20charset=utf-8','proper','ice','Offset\x20may\x20not\x20be\x20NaN','mean','broadcastChannel','createOscillator','\x20queued\x20entries\x20prior\x20to\x20last\x20keyframe','dry','Chunked\x20mode\x20restarted\x20successfully','order','vdo.ninja/','rtc\x20data\x20channel\x20error\x202:\x20','level','cbr','ago','perhaps','shape','manualBandwidth','reject','evening','msg\x20size\x20error','does\x20any\x20audio\x20exist?','iron','nochunkaudio','boat','applySoloChat','vdAv','pound','randomize','Clean\x20up','roombitrate','brightness','seedAttempts','addVsSentRate','Video\x20encdoder\x20closed','Peer_Connection','assembled','remoteVideoMuteElement','string','chunkedResends','constant','/status','transferred','them','symbol','video_session','double','feet','requestscenes','Shadow\x20mode:\x20blocked\x20from\x20room\x20-\x20not\x20joining','frameId','abc123','dataOffset','requestedStatsInterval','raw','alreadyJoinedMembers','processFrameVideo','put','soloVideoMode','success','allowDrawing','among','measure','Switching\x20to\x20limitTotalBitrateAll','widget-src','audioLatency','invalid-remote-code-obs','isFinite','radio','mobile','directorVolumeState','activatedStreams','Not\x20a\x20scene','row','eventPlayActive','quality_url','label=','slow','hostname','codirectorRequested','timestamp','selfVolume','28jaSMwP','delayIceSend','maxframeRate_q2','screenshareType','closing\x2019','queue','allowIframe','refreshScale','paper','consonant','obsSceneSync','videoWorker','forcePLI','oil','scaleResolution','qosData','RPC\x20connection\x20failed\x20-\x20closing\x20after\x20timeout','chunkedDetails','forceNoAudioWhipIn','meshcast2LastError','Chunkcast\x20queue\x20too\x20large,\x20clearing\x20to\x20last\x20keyframe','red','action','yes','showTime','New\x20viewers:\x20','slave','promptAccess','includeRTT','foregroundImg','contentHint','dimension:\x20','shift','dataset','bandwidth\x20set\x20g!\x20','preferredVideoErrorCorrection','heavy','nocaptionlabels','bye','\x27]\x20.midi-controls','timecode','automute','autorecordremote','stream_configVideo','onload','space','audioCodec','requesterUUID','key','requestScenes','pauseClock','whole','Max\x20bandwidth\x20controlling\x20bitrate:\x20','stream-id-detected','chunknack','late','maxvb_url','WebMWriter','Publisher\x20is\x20being\x20sent\x20a\x20video\x20stream???\x20NOT\x20EXPECTED!','dictionary','invent','ROOMID\x20ENABLED','reconnected','A\x20guest\x20is\x20waiting\x20to\x20be\x20admitted.\x0a\x0a','chunkfec','WEBRTC\x20CONNECTION\x20OPEN','canvasOverlay','requested-stream','screenElement','mirrorOutput','rotateIceServersSimple','represent','audioOptions','failed','discard','preferVideoCodec','rope','connection\x20state\x20->\x20failed;\x20will\x20try\x20ice\x20reconnect\x20or\x20such','allowScreen','utf-8','A_OPUS','open','mediamtx','frameMeta','parse','preferChannel','trackNumber','short','includes','OPEN','others','case','scale','joy','video','decodeInvite','Setting\x20Codec\x20to\x20vp8','meshcast2ErrorHandling','middle','Timestamp\x20duplicated','relaywss','sampleRate','switchMode','Failed\x20to\x20store\x20block:\x20','settle','qosTurnAllowlist','obs_control','setAudioBitrate','disconnect','writer','screenIndexes','poke','setRemoteDescription','playing','remove-queue','chunkedChannels','include','requestChangeEQ','candidate\x20callback\x20finished\x20in\x20totalilty','allow','Lowered\x20hand','createUniversalToken','dog','batteryMeter','defaultPassword','invite','buffered','tall','fromCharCode','chunkRates','newMainDirectorSetup','midiRemote','requestCoMigrate','Notice:\x20Alpha\x20chunked-mode\x20encoding\x20is\x20not\x20supported\x20by\x20this\x20browser.\x0a\x0aThe\x20vidoe\x20encoder\x20is\x20falling\x20back\x20to\x20non-alpha\x20mode','WebRTC\x20Connection\x20Closed.\x20Clean\x20up.\x20657','resolveStream','allowscreen','fill','began','info','connectionState','rows','vector','lastPongToken','channelCount','smallScreen','RTC\x20Connection\x20seems\x20to\x20be\x20dead\x20or\x20not\x20yet\x20open?\x201','line','season','guestFeeds','plugged','heat','bigmutebutton','nosettings','configAudio','mind','requestPublisherUpdate','LOADING\x20UP\x20WAITING\x20WATCH\x20STREAM:\x20','sail','streamID','probable','fear','allowchunked','connections','application/json','Video\x20Bitrate\x20is\x20locked;\x20can\x27t\x20update','MAKING\x20A\x20NEW\x20RPCS\x20RTC\x20CONNECTION','apiSocket','hostedFiles','mystery-message-recieved-2','cost','introOnClean','allowscreenwhipout','excludeaudio','mutedStateScene','https://app.meshcast.io','audioMeterGuest','does','h264','spring','preferred','disablePLI','strange','liquid','new','micIsolate','timer','half','soil','borderRadius','encryptMessage','number','meta','startClock','UUID','hangup','layout-updated','pushDirectorStateUpdate','cry','repeat','age','token','contrast','pptControls','loadstart','clearDrawing','querySelector','writeUnsignedIntBE','realTime','develop','codec_url','color','maxsamplerate','catch','queueDepth','sending\x20message\x20via\x20server','keepIncomingVideosInLandscape','wss://proxywss.rtc.ninja:443','audienceToken','wss','bandwidth\x20set\x20h!\x20','30xBqHXa','pattern','processRPCSOnMessage','have-remote-offer','obsCommand','session.newMainDirectorSetup','add','saw','ICE\x20restart\x20triggered\x20for\x20pcs:\x20','whipOutScale','length','dedicatedControlBarSpace','original','innerText','allowChunked','sendPeers','iceServers','sister','sync','feel','pixelFix','Adjusting\x20Gain;\x20only\x20track\x200\x20in\x20all\x20likely\x20hood,\x20unless\x20more\x20than\x20track\x200\x20support\x20is\x20added.','flower','vosc','wss://whip.vdo.ninja','travel','gpGPU','mountain','bitrateTimeout','waitForCandidates','version','PINGED','altUUID','Chrome\x20for\x20iOS','ArrayBufferDataStream','h264profile','noFEC','limitMeshcastBitrate','mono','thin','preferCodec','crease','Deny\x20this\x20guest?','div','friend','userAgent','meshcast2Anonymous','min','localDescription','\x20x\x20','whipOutScreenShareCodec','sign','inFlight','map','believe','wash','cover','eat','tool','controls','updateLocalStatsInterval','measureEBMLVarInt','cut','flipped','onicecandidateerror','men','CONNECTEED!','decrypt','piece','lie','chunkedAudioEnabled','their','voiceMeter','redirectHangupTimer','prefer-software','cpuLimited','requestStream','showall','body','disconnectedTimeout','rest','grow','whipoutScreenSettings','ball','quality_wb','setRequestHeader','bufferState','allowWebp','beepToNotify','should','Room\x20is\x20already\x20claimed\x20by\x20someone\x20else.','dataMode','flipState','\x20/\x20','gridlayout','de1','cause','reconnecting','setVideoScale','createScriptProcessor','directorUUID','arrayBuffer','exclude','kill','queueList','sell','hybrid','allowScreenVideo','[data-action-type=\x22mirror-guest\x22]','codecs','checked','interval','wear','chunkJitterSlack','win','audioGain','objectFit','rebuffering','Remote\x20request\x20decoded\x20successfully','A\x20Guest\x20joined\x20the\x20room','recordLocal','pick','RPCS\x20WINS\x20ICE','screenAudioOverride','spot','allowdrawing','sugar','done\x20setting\x20degrad\x20to\x20maintain-framerate','screensharequality','Publisher\x20will\x20be\x20ignored\x20due\x20to\x20max\x20connections\x20already\x20hit','room=','gentle','value','maxBitrate','session.limitMaxBandwidth\x20running:\x20','soldier','focusDistance','message\x20could\x20not\x20be\x20sent;\x20queuing\x20it','ping','noiframe','fact','approved','set-video-bitrate','next','showMuteState','denoise_url','tokens-did-not-match','networkPriority','streaming','This\x20stream\x20token\x20is\x20already\x20connected.\x20Are\x20you\x20having\x20a\x20CORS\x20issue?\x20Also,\x20ensure\x20SSL\x20if\x20enforced\x20on\x20your\x20host\x20everywhere.','six','stopClock','maxconnections','limitMaxBandwidth','cleanDirector','summer','muteState','cache','side','hear','candidateType_local','buffer2','request\x20rate\x20limit:\x20','calculateScale','melody','bufferedAmount','turns:','pong','iceRestarts','myVideo.webm','call','cleanViewer','requestCoDirector','push-connection-info','Overwrite\x20crosses\x20blob\x20boundaries','buffer_vals','hss-connection','platform','forceRetryTimeout','meshcast','fecAudio','setupYourOwnPlease','writeEBMLVarInt','udp','AV1','only-main-director','bed','could','video_encoder','Browser','Failed\x20to\x20restart\x20chunked\x20mode:','getTracks','opus','onmessage','motionDetectionInterval','fakeUser','break','delayNode','onnegotiationneeded','chunkedtransfer','stereo\x20inbound\x20enabled','sea','bone','our','webCodec','writeBytes','clock24','tipCurrency','broadcast=false','document','successfully\x20sent\x20message\x20vis\x20WebRTC\x20instead\x20of\x20WSS','applyIsolatedChat','ICE\x20restart\x20failed\x20for\x20pcs:\x20','transcript','iceTimer','fadein','jitter','force','allowresources','can','providing\x20answer','possible','title','Unknown','contain','allowNoGroup','Waiting\x20for\x20audio\x20header\x20before\x20sending\x20raw\x20audio\x20data','roomenc','wait','POST','decoder','allowwebp','turn:turn-usw2.vdo.ninja:3478','remote-mute-state','currentRate','closeTimeout\x20cancelled;\x203'];_0x5b65=function(){return _0x38b5db;};return _0x5b65();}async function chooseBestTURN(){var _0x4a650b=_0x165386;if(session[_0x4a650b(0x7d3)])return;return!TURNPromise?TURNPromise=getTURNList():warnlog(_0x4a650b(0x4d3)),await TURNPromise;}var WebRTC={};WebRTC[_0x165386(0xbfa)]=(function(){var _0xf92ab0=_0x165386,_0xab9a9c={};function _0x1c8003(){var _0xb88bf8=_0x2b9f,_0x1962b1,_0x3af792,_0x3a9d1f=new Promise((_0x1de4cc,_0x26ac5b)=>{_0x1962b1=_0x1de4cc,_0x3af792=_0x26ac5b;});return _0x3a9d1f['resolve']=_0x1962b1,_0x3a9d1f[_0xb88bf8(0x9b7)]=_0x3af792,_0x3a9d1f;}_0xab9a9c['generateStreamID']=function(_0x2d9839=0x7){var _0x4fcd10=_0x2b9f,_0x3a41d7='',_0x31b75b='ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';for(var _0x3f89c5=0x0;_0x3f89c5<_0x2d9839;_0x3f89c5++){_0x3a41d7+=_0x31b75b[_0x4fcd10(0x1c0)](Math[_0x4fcd10(0x502)](Math[_0x4fcd10(0x2f1)]()*_0x31b75b['length']));}try{_0x3a41d7=_0x3a41d7[_0x4fcd10(0x61a)]('AD',_0x4fcd10(0x318)),_0x3a41d7=_0x3a41d7[_0x4fcd10(0x61a)]('Ad',_0x4fcd10(0x9bf)),_0x3a41d7=_0x3a41d7[_0x4fcd10(0x61a)]('ad',_0x4fcd10(0xc2c)),_0x3a41d7=_0x3a41d7[_0x4fcd10(0x61a)]('aD','vDav');}catch(_0x4413bf){errorlog(_0x4413bf);}return log(_0x3a41d7),_0x3a41d7;},_0xab9a9c[_0xf92ab0(0x811)]=function(_0x40337d=0x7){var _0x5c884b=_0xf92ab0,_0x58acc2='',_0xcacd35=['the','of','to',_0x5c884b(0x6fa),'a','in','is','it',_0x5c884b(0x4db),_0x5c884b(0x97f),'he',_0x5c884b(0xc69),'for','on',_0x5c884b(0x1be),_0x5c884b(0x35e),'as','I',_0x5c884b(0x3f6),'they','be','at',_0x5c884b(0x7c6),_0x5c884b(0x65f),_0x5c884b(0x5a0),_0x5c884b(0x208),'or',_0x5c884b(0x24d),'by',_0x5c884b(0x8be),'but',_0x5c884b(0x5cf),_0x5c884b(0x238),'we',_0x5c884b(0xbaf),_0x5c884b(0x775),_0x5c884b(0xcb8),_0x5c884b(0x8d4),'all','there','when','up','use',_0x5c884b(0x1c2),_0x5c884b(0x891),'said','an',_0x5c884b(0x661),'she',_0x5c884b(0xbdd),'do',_0x5c884b(0xb1b),_0x5c884b(0x56c),'if','will',_0x5c884b(0x3fa),_0x5c884b(0xc78),'many',_0x5c884b(0x998),_0x5c884b(0x9d0),'write',_0x5c884b(0x82a),_0x5c884b(0x450),'so','these',_0x5c884b(0xc5f),_0x5c884b(0x33a),'make',_0x5c884b(0x812),'see',_0x5c884b(0x8d7),_0x5c884b(0x633),'has',_0x5c884b(0x43a),'more',_0x5c884b(0x863),_0x5c884b(0xb8f),'go',_0x5c884b(0x4a5),_0x5c884b(0xcbb),_0x5c884b(0xab6),_0x5c884b(0x7b6),'no',_0x5c884b(0x7c5),_0x5c884b(0x8af),'my',_0x5c884b(0x7bd),_0x5c884b(0x4ad),_0x5c884b(0x417),_0x5c884b(0x289),_0x5c884b(0xb7e),_0x5c884b(0x5fd),_0x5c884b(0x88b),'may',_0x5c884b(0x4eb),_0x5c884b(0xb72),_0x5c884b(0x221),_0x5c884b(0x30e),'find','any',_0x5c884b(0xaaf),_0x5c884b(0x6db),_0x5c884b(0x4ef),'take',_0x5c884b(0x666),'place',_0x5c884b(0x83c),_0x5c884b(0x5eb),_0x5c884b(0x679),_0x5c884b(0x7a2),_0x5c884b(0xc89),_0x5c884b(0xc51),'only',_0x5c884b(0xc45),'man','year',_0x5c884b(0xc6c),_0x5c884b(0x84c),'every',_0x5c884b(0x88d),'me',_0x5c884b(0x5ec),_0x5c884b(0xb9f),_0x5c884b(0x461),_0x5c884b(0x6d5),'very',_0x5c884b(0x531),_0x5c884b(0x7d0),'form','sentence',_0x5c884b(0x754),'think','say','help',_0x5c884b(0x7f5),_0x5c884b(0xa8a),_0x5c884b(0x798),'turn',_0x5c884b(0xb34),_0x5c884b(0x233),_0x5c884b(0x9a8),'before',_0x5c884b(0x779),_0x5c884b(0x223),_0x5c884b(0x32e),'old','too','same',_0x5c884b(0x723),_0x5c884b(0xaa8),_0x5c884b(0x43d),_0x5c884b(0x4d5),_0x5c884b(0x94d),_0x5c884b(0x643),_0x5c884b(0x8f5),_0x5c884b(0x4b5),_0x5c884b(0x67d),_0x5c884b(0xc81),'end',_0x5c884b(0x9de),_0x5c884b(0x388),_0x5c884b(0x993),_0x5c884b(0x4af),_0x5c884b(0x228),'large',_0x5c884b(0x596),_0x5c884b(0xada),_0x5c884b(0x8e1),_0x5c884b(0x965),'here',_0x5c884b(0xc3c),'big',_0x5c884b(0x412),'such',_0x5c884b(0xbfe),_0x5c884b(0x7ce),'why','ask',_0x5c884b(0xb15),_0x5c884b(0xc18),_0x5c884b(0x62c),_0x5c884b(0x45a),_0x5c884b(0xcd1),_0x5c884b(0x816),_0x5c884b(0xce3),_0x5c884b(0x6de),'picture',_0x5c884b(0x346),'us','again',_0x5c884b(0x956),_0x5c884b(0x5d0),_0x5c884b(0x41c),_0x5c884b(0x6d0),_0x5c884b(0x672),_0x5c884b(0x7ff),_0x5c884b(0x222),'earth','father','head',_0x5c884b(0xc01),_0x5c884b(0x302),_0x5c884b(0x68f),_0x5c884b(0xb2d),'country',_0x5c884b(0x5f7),_0x5c884b(0x3b6),_0x5c884b(0x5a3),_0x5c884b(0xb25),_0x5c884b(0x424),_0x5c884b(0x1fc),_0x5c884b(0x3a8),_0x5c884b(0xbc4),_0x5c884b(0xb0c),_0x5c884b(0x853),_0x5c884b(0x3b1),'four',_0x5c884b(0x784),_0x5c884b(0x4e0),_0x5c884b(0x945),_0x5c884b(0x631),_0x5c884b(0xcbc),_0x5c884b(0x8c7),_0x5c884b(0xcae),_0x5c884b(0x871),_0x5c884b(0x533),_0x5c884b(0x71a),_0x5c884b(0xc73),_0x5c884b(0x446),'hard','start','might','story',_0x5c884b(0xadb),_0x5c884b(0x8cc),_0x5c884b(0xb9d),_0x5c884b(0xc17),'left',_0x5c884b(0xa2e),'run','dont',_0x5c884b(0x296),'press',_0x5c884b(0x3ef),_0x5c884b(0x8ff),_0x5c884b(0x85a),'life','few',_0x5c884b(0x410),_0x5c884b(0xa48),_0x5c884b(0x809),'together',_0x5c884b(0xb63),_0x5c884b(0x3f5),_0x5c884b(0x2a6),_0x5c884b(0x1a8),_0x5c884b(0x781),'walk',_0x5c884b(0x6b9),_0x5c884b(0x3ab),_0x5c884b(0x9ff),_0x5c884b(0x34e),'always',_0x5c884b(0x6ef),_0x5c884b(0x2b3),_0x5c884b(0x7dd),_0x5c884b(0x833),_0x5c884b(0x1ed),_0x5c884b(0x6a2),'until','mile',_0x5c884b(0xc61),'car',_0x5c884b(0x9d4),_0x5c884b(0x668),_0x5c884b(0xc8f),_0x5c884b(0x796),'carry','took',_0x5c884b(0xcab),_0x5c884b(0xb0d),_0x5c884b(0x6ab),_0x5c884b(0xb00),_0x5c884b(0xa81),_0x5c884b(0x354),'fish',_0x5c884b(0xaef),'stop',_0x5c884b(0x496),_0x5c884b(0x3ff),_0x5c884b(0xb73),_0x5c884b(0x760),_0x5c884b(0xb12),'sure',_0x5c884b(0xcd4),_0x5c884b(0xaca),_0x5c884b(0x31d),_0x5c884b(0x462),'main',_0x5c884b(0x213),'plain',_0x5c884b(0x59a),_0x5c884b(0x5b8),_0x5c884b(0x3f8),_0x5c884b(0x6e2),_0x5c884b(0x3e0),_0x5c884b(0x88f),'red','list',_0x5c884b(0xcbe),_0x5c884b(0xae7),'talk','bird','soon',_0x5c884b(0xb22),_0x5c884b(0xa71),'family',_0x5c884b(0x555),_0x5c884b(0x997),'leave',_0x5c884b(0xc1b),_0x5c884b(0x9e3),'door',_0x5c884b(0x592),_0x5c884b(0x68a),_0x5c884b(0xa4e),_0x5c884b(0x748),'class','wind',_0x5c884b(0x443),_0x5c884b(0x43e),_0x5c884b(0x5fa),_0x5c884b(0x903),'area',_0x5c884b(0xab2),_0x5c884b(0xc59),_0x5c884b(0x9ae),_0x5c884b(0xc29),_0x5c884b(0x685),_0x5c884b(0x876),_0x5c884b(0xb18),'told','knew',_0x5c884b(0x7d9),_0x5c884b(0x3fd),'top',_0x5c884b(0xa2a),_0x5c884b(0x3c2),_0x5c884b(0xa24),_0x5c884b(0x319),_0x5c884b(0x220),_0x5c884b(0x996),_0x5c884b(0x455),_0x5c884b(0x5d1),_0x5c884b(0x6c5),_0x5c884b(0x2c2),_0x5c884b(0xbd6),_0x5c884b(0x7b5),'early',_0x5c884b(0x549),'west',_0x5c884b(0x751),'interest','reach',_0x5c884b(0x26c),'verb',_0x5c884b(0x3bc),'listen',_0x5c884b(0xb6a),_0x5c884b(0x3ec),_0x5c884b(0xaed),_0x5c884b(0x81d),_0x5c884b(0x889),_0x5c884b(0x6c7),_0x5c884b(0x265),_0x5c884b(0x2fb),_0x5c884b(0x3fc),_0x5c884b(0xbf5),_0x5c884b(0x799),'lay',_0x5c884b(0x2d9),_0x5c884b(0xad5),_0x5c884b(0x9f2),'center',_0x5c884b(0x8f8),_0x5c884b(0x510),_0x5c884b(0x8dd),'serve',_0x5c884b(0x2ab),_0x5c884b(0x65e),'map','rain','rule',_0x5c884b(0x91b),'pull','cold',_0x5c884b(0x5c3),_0x5c884b(0x617),_0x5c884b(0x1d6),_0x5c884b(0x27b),_0x5c884b(0x36a),_0x5c884b(0xca2),'certain',_0x5c884b(0x5f9),'fall','lead',_0x5c884b(0xabd),_0x5c884b(0x885),'machine',_0x5c884b(0x323),_0x5c884b(0xbb8),_0x5c884b(0x6c4),_0x5c884b(0x205),'star',_0x5c884b(0xc2b),'noun','field',_0x5c884b(0xb24),'correct','able',_0x5c884b(0x9c0),_0x5c884b(0x894),_0x5c884b(0x74c),_0x5c884b(0xc54),_0x5c884b(0x593),'contain',_0x5c884b(0x849),_0x5c884b(0x5f2),_0x5c884b(0x743),_0x5c884b(0x255),'gave',_0x5c884b(0x917),'oh',_0x5c884b(0x3a4),_0x5c884b(0xac8),'ocean',_0x5c884b(0x878),'free','minute',_0x5c884b(0x513),_0x5c884b(0x802),_0x5c884b(0xa92),_0x5c884b(0x753),'clear','tail','produce',_0x5c884b(0xb60),_0x5c884b(0x44f),_0x5c884b(0x316),'multiply',_0x5c884b(0xcc9),_0x5c884b(0x7f3),_0x5c884b(0x819),_0x5c884b(0x235),'full',_0x5c884b(0xbad),'blue','object',_0x5c884b(0x7b7),'surface',_0x5c884b(0x574),_0x5c884b(0xce8),_0x5c884b(0x77b),_0x5c884b(0x76c),_0x5c884b(0xce0),_0x5c884b(0x89a),'test',_0x5c884b(0x3c4),_0x5c884b(0x9bd),'common','gold',_0x5c884b(0xbb1),_0x5c884b(0xc95),'stead',_0x5c884b(0x9ac),_0x5c884b(0xc8b),_0x5c884b(0x83e),_0x5c884b(0x4e5),_0x5c884b(0x9b3),'ran','check','game',_0x5c884b(0x9b5),'equate',_0x5c884b(0x351),_0x5c884b(0x1cd),_0x5c884b(0x448),_0x5c884b(0xa8e),_0x5c884b(0x77d),_0x5c884b(0x6f0),'bring',_0x5c884b(0xa0e),'distant',_0x5c884b(0xa80),_0x5c884b(0xca5),_0x5c884b(0x6ec),_0x5c884b(0x90e),_0x5c884b(0x9e2),_0x5c884b(0xcc8),_0x5c884b(0xb27),'yet','wave',_0x5c884b(0xbe1),'heart','am','present',_0x5c884b(0xa1b),_0x5c884b(0x76e),_0x5c884b(0x20d),_0x5c884b(0x3a5),_0x5c884b(0x1af),_0x5c884b(0x518),_0x5c884b(0xa95),_0x5c884b(0x86e),_0x5c884b(0x53c),'vary',_0x5c884b(0xa5f),'speak',_0x5c884b(0x7ed),'general',_0x5c884b(0x9a6),_0x5c884b(0x78a),_0x5c884b(0x702),_0x5c884b(0x20c),_0x5c884b(0xa6b),_0x5c884b(0x51e),_0x5c884b(0x7da),'felt',_0x5c884b(0x9b4),_0x5c884b(0xb4d),'sudden',_0x5c884b(0x7a1),'square',_0x5c884b(0x50e),_0x5c884b(0xade),_0x5c884b(0xa3e),_0x5c884b(0x396),_0x5c884b(0x3aa),'region',_0x5c884b(0xcda),_0x5c884b(0x217),_0x5c884b(0xa97),_0x5c884b(0xb8e),_0x5c884b(0x553),'egg',_0x5c884b(0xc1a),'cell',_0x5c884b(0xb0a),_0x5c884b(0x866),'forest',_0x5c884b(0x1e3),_0x5c884b(0x3b2),_0x5c884b(0x4e3),_0x5c884b(0x6cd),_0x5c884b(0xb6f),_0x5c884b(0xbc6),'sleep',_0x5c884b(0x595),'lone',_0x5c884b(0x554),_0x5c884b(0x80c),_0x5c884b(0x467),_0x5c884b(0xacc),_0x5c884b(0x34d),_0x5c884b(0x264),_0x5c884b(0x616),_0x5c884b(0x260),_0x5c884b(0xa54),_0x5c884b(0x2db),_0x5c884b(0x31f),'written','wild',_0x5c884b(0x4c5),'kept',_0x5c884b(0x715),_0x5c884b(0x1f3),_0x5c884b(0x3c7),'job',_0x5c884b(0x25f),'sign',_0x5c884b(0x747),_0x5c884b(0x384),_0x5c884b(0x72f),_0x5c884b(0x1f0),_0x5c884b(0x3be),'gas',_0x5c884b(0x6e1),'month',_0x5c884b(0x2b1),_0x5c884b(0x662),'finish',_0x5c884b(0x793),'hope',_0x5c884b(0xaea),'clothe',_0x5c884b(0xaad),_0x5c884b(0x91c),_0x5c884b(0x6fc),'baby',_0x5c884b(0x3ad),_0x5c884b(0x1fe),_0x5c884b(0x70c),_0x5c884b(0x675),_0x5c884b(0x8d3),_0x5c884b(0x291),_0x5c884b(0xcdc),_0x5c884b(0x501),'whether','push',_0x5c884b(0x37c),_0x5c884b(0x272),_0x5c884b(0x2ee),_0x5c884b(0x606),_0x5c884b(0x63a),_0x5c884b(0xcca),_0x5c884b(0xc64),'cook','floor',_0x5c884b(0x90a),'result',_0x5c884b(0x603),'hill',_0x5c884b(0x5ab),_0x5c884b(0x794),_0x5c884b(0x834),_0x5c884b(0x660),_0x5c884b(0x3c5),'law',_0x5c884b(0xc31),_0x5c884b(0x1de),_0x5c884b(0x322),_0x5c884b(0xc7d),_0x5c884b(0x6f4),_0x5c884b(0xa76),_0x5c884b(0xc88),_0x5c884b(0xab3),_0x5c884b(0x883),_0x5c884b(0x69b),_0x5c884b(0x405),_0x5c884b(0x92d),'value','fight',_0x5c884b(0xb19),'beat',_0x5c884b(0x26f),_0x5c884b(0x1b9),'view','sense','ear',_0x5c884b(0x4ca),_0x5c884b(0x256),_0x5c884b(0x7f0),_0x5c884b(0xa52),_0x5c884b(0xa59),_0x5c884b(0xb3b),_0x5c884b(0x2c4),'lake',_0x5c884b(0x31e),_0x5c884b(0xa53),'loud',_0x5c884b(0xaaa),'observe',_0x5c884b(0x6c3),_0x5c884b(0x60e),_0x5c884b(0xa00),_0x5c884b(0xcdd),_0x5c884b(0xa32),_0x5c884b(0x84a),_0x5c884b(0x46b),_0x5c884b(0x79b),'organ',_0x5c884b(0x1b1),_0x5c884b(0xabf),'section',_0x5c884b(0x8f4),'cloud',_0x5c884b(0x582),_0x5c884b(0x2a5),_0x5c884b(0x301),_0x5c884b(0x78e),_0x5c884b(0x8fe),_0x5c884b(0x1ce),_0x5c884b(0x600),_0x5c884b(0x5a8),_0x5c884b(0x984),_0x5c884b(0x604),_0x5c884b(0x924),_0x5c884b(0xa27),_0x5c884b(0x9bb),_0x5c884b(0x8c4),_0x5c884b(0x938),_0x5c884b(0x846),_0x5c884b(0x406),_0x5c884b(0x370),'smile',_0x5c884b(0xafd),_0x5c884b(0x47b),_0x5c884b(0x791),_0x5c884b(0xb78),_0x5c884b(0x344),_0x5c884b(0x237),'receive',_0x5c884b(0x9ee),_0x5c884b(0x575),_0x5c884b(0x95c),_0x5c884b(0x9d1),_0x5c884b(0xc8c),_0x5c884b(0x29a),_0x5c884b(0x6cb),_0x5c884b(0xbcc),'except',_0x5c884b(0x663),'seed',_0x5c884b(0x6c9),_0x5c884b(0x1e2),_0x5c884b(0xc7c),'clean',_0x5c884b(0xb98),_0x5c884b(0xbe7),_0x5c884b(0x292),'rise',_0x5c884b(0x84e),_0x5c884b(0x415),_0x5c884b(0xa04),_0x5c884b(0x2e0),'touch','grew',_0x5c884b(0x3a0),_0x5c884b(0x3ca),_0x5c884b(0x722),_0x5c884b(0x327),_0x5c884b(0xaa1),'lost','brown',_0x5c884b(0xb44),'garden',_0x5c884b(0x951),_0x5c884b(0xc3e),_0x5c884b(0x3f3),_0x5c884b(0x5e2),_0x5c884b(0x230),_0x5c884b(0x307),_0x5c884b(0x879),_0x5c884b(0xce1),'collect',_0x5c884b(0x409),_0x5c884b(0xc4e),_0x5c884b(0x687),_0x5c884b(0xb57),_0x5c884b(0xc08),'captain',_0x5c884b(0x800),'separate',_0x5c884b(0x373),'doctor',_0x5c884b(0x245),_0x5c884b(0x589),_0x5c884b(0x6ba),_0x5c884b(0x324),_0x5c884b(0x718),_0x5c884b(0x901),'character',_0x5c884b(0xbcb),_0x5c884b(0x979),'period',_0x5c884b(0x47c),_0x5c884b(0x9e9),'spoke','atom',_0x5c884b(0xc0d),'history','effect','electric',_0x5c884b(0x47a),'crop',_0x5c884b(0x69f),_0x5c884b(0x57e),_0x5c884b(0x5cd),_0x5c884b(0x640),'corner',_0x5c884b(0x6aa),_0x5c884b(0x88e),_0x5c884b(0xb9e),_0x5c884b(0x4c8),_0x5c884b(0x4dc),_0x5c884b(0x1a9),_0x5c884b(0x541),_0x5c884b(0x456),_0x5c884b(0x3ae),_0x5c884b(0x1dc),_0x5c884b(0x6a9),_0x5c884b(0x31c),_0x5c884b(0x252),'rich',_0x5c884b(0xcb2),_0x5c884b(0xb5b),_0x5c884b(0x454),_0x5c884b(0xbed),_0x5c884b(0x273),_0x5c884b(0x734),'sharp','wing',_0x5c884b(0x5da),'neighbor',_0x5c884b(0xb0b),_0x5c884b(0x8ec),_0x5c884b(0x24e),_0x5c884b(0x788),'corn','compare','poem',_0x5c884b(0x9cb),'bell',_0x5c884b(0x934),_0x5c884b(0x356),'rub','tube',_0x5c884b(0x949),_0x5c884b(0x70f),_0x5c884b(0x452),_0x5c884b(0xa98),_0x5c884b(0x966),_0x5c884b(0xafb),_0x5c884b(0x73d),'planet',_0x5c884b(0x947),_0x5c884b(0x8cb),_0x5c884b(0x328),_0x5c884b(0x3d5),'mine',_0x5c884b(0x622),'enter',_0x5c884b(0xbf6),'fresh','search',_0x5c884b(0x4c6),'yellow',_0x5c884b(0xc4c),_0x5c884b(0xa6e),_0x5c884b(0xc44),'dead',_0x5c884b(0xb50),_0x5c884b(0x99a),_0x5c884b(0x56d),_0x5c884b(0x68e),_0x5c884b(0x746),_0x5c884b(0x7ba),_0x5c884b(0x5d2),_0x5c884b(0x2ed),_0x5c884b(0xbd4),_0x5c884b(0x41b),_0x5c884b(0xb3d),_0x5c884b(0x9e0),_0x5c884b(0x508),_0x5c884b(0x360),_0x5c884b(0x79a),'particular','deal',_0x5c884b(0x673),'term',_0x5c884b(0x580),_0x5c884b(0x2f7),_0x5c884b(0x35c),_0x5c884b(0x369),_0x5c884b(0x8ad),'arrange',_0x5c884b(0x3f9),_0x5c884b(0xa33),'cotton',_0x5c884b(0x39e),'determine',_0x5c884b(0x4e6),_0x5c884b(0x7fa),_0x5c884b(0x20f),_0x5c884b(0x91e),_0x5c884b(0x9b1),_0x5c884b(0x26e),_0x5c884b(0x929),'shop',_0x5c884b(0x28a),_0x5c884b(0x963),_0x5c884b(0x792),_0x5c884b(0x774),'column',_0x5c884b(0x907),_0x5c884b(0x7ad),_0x5c884b(0xce4),_0x5c884b(0x30a),_0x5c884b(0xabe),_0x5c884b(0x859),_0x5c884b(0x517),_0x5c884b(0x42d),_0x5c884b(0x7a9),_0x5c884b(0x514),_0x5c884b(0x935),_0x5c884b(0x1b2),'claim','continent',_0x5c884b(0x51a),_0x5c884b(0xb52),_0x5c884b(0x42b),_0x5c884b(0x419),'skill','women',_0x5c884b(0xa8b),'solution',_0x5c884b(0x7a3),_0x5c884b(0x810),'thank','branch',_0x5c884b(0x629),_0x5c884b(0x825),_0x5c884b(0x414),'fig',_0x5c884b(0x24c),_0x5c884b(0x326),_0x5c884b(0xae5),'steel','discuss','forward',_0x5c884b(0x315),_0x5c884b(0x671),_0x5c884b(0x1c5),_0x5c884b(0xc5e),_0x5c884b(0x741),_0x5c884b(0x465),_0x5c884b(0x200),_0x5c884b(0x68b),_0x5c884b(0x4ed),'mass','card',_0x5c884b(0x8e7),_0x5c884b(0xa43),'slip',_0x5c884b(0xb46),_0x5c884b(0x25c),_0x5c884b(0x9b8),_0x5c884b(0xc82),_0x5c884b(0x333),_0x5c884b(0xb0e),'total',_0x5c884b(0x4bd),_0x5c884b(0x280),_0x5c884b(0xc1f),'nor',_0x5c884b(0x9d3),_0x5c884b(0xc2f),'arrive','master',_0x5c884b(0x50d),'parent',_0x5c884b(0xc86),'division',_0x5c884b(0x564),_0x5c884b(0x5d5),'favor','connect',_0x5c884b(0x3da),_0x5c884b(0x4b9),_0x5c884b(0x5fc),'fat',_0x5c884b(0x605),_0x5c884b(0xae0),_0x5c884b(0x905),'station','dad','bread','charge',_0x5c884b(0x9a5),_0x5c884b(0x39d),_0x5c884b(0x6c0),_0x5c884b(0xbda),_0x5c884b(0xa11),'duck',_0x5c884b(0x910),'market','degree','populate',_0x5c884b(0x473),_0x5c884b(0x32f),'enemy',_0x5c884b(0x96e),_0x5c884b(0xc83),'occur',_0x5c884b(0x1cb),'speech',_0x5c884b(0x552),_0x5c884b(0x745),_0x5c884b(0x6c8),_0x5c884b(0x399),_0x5c884b(0x8ae),_0x5c884b(0xaae),_0x5c884b(0x440),'meant','quotient',_0x5c884b(0x215),_0x5c884b(0x762),'neck'];for(var _0x374675=0x0;_0x374675<0x2;_0x374675++){try{var _0x4359f5=parseInt(Math[_0x5c884b(0x2f1)]()*0x3e8);_0x58acc2+=_0xcacd35[_0x4359f5];}catch(_0x156b01){}}var _0x42a3d8=_0x5c884b(0x8b9);_0x58acc2+=_0x42a3d8[_0x5c884b(0x1c0)](Math['floor'](Math['random']()*_0x42a3d8['length']));while(_0x58acc2[_0x5c884b(0xade)]<_0x40337d){_0x58acc2+=_0x42a3d8['charAt'](Math[_0x5c884b(0x502)](Math[_0x5c884b(0x2f1)]()*_0x42a3d8[_0x5c884b(0xade)]));}try{_0x58acc2=_0x58acc2[_0x5c884b(0x61a)]('AD',_0x5c884b(0x318)),_0x58acc2=_0x58acc2[_0x5c884b(0x61a)]('Ad',_0x5c884b(0x9bf)),_0x58acc2=_0x58acc2['replaceAll']('ad','vdav'),_0x58acc2=_0x58acc2['replaceAll']('aD',_0x5c884b(0x293));}catch(_0x26dbbf){errorlog(_0x26dbbf);}return log(_0x58acc2),_0x58acc2;},_0xab9a9c['apiserver']=_0xf92ab0(0x2d7),_0xab9a9c[_0xf92ab0(0xa9e)]=null,_0xab9a9c[_0xf92ab0(0x36f)]=![],_0xab9a9c[_0xf92ab0(0x5aa)]=![],_0xab9a9c[_0xf92ab0(0x4fe)]=![],_0xab9a9c[_0xf92ab0(0xbe9)]=![],_0xab9a9c[_0xf92ab0(0xc13)]=null,_0xab9a9c[_0xf92ab0(0x4aa)]=0xbb8,_0xab9a9c[_0xf92ab0(0x690)]=![],_0xab9a9c[_0xf92ab0(0x971)]=!![],_0xab9a9c[_0xf92ab0(0xbee)]=![],_0xab9a9c[_0xf92ab0(0x4f6)]=0x100,_0xab9a9c[_0xf92ab0(0x667)]=0x64,_0xab9a9c[_0xf92ab0(0x209)]=0x8,_0xab9a9c[_0xf92ab0(0x53a)]=![],_0xab9a9c[_0xf92ab0(0x8a4)]=![],_0xab9a9c[_0xf92ab0(0x9dc)]=![],_0xab9a9c[_0xf92ab0(0xa45)]=![],_0xab9a9c[_0xf92ab0(0x54a)]=![],_0xab9a9c['screenVideoOverride']=null,_0xab9a9c[_0xf92ab0(0xb4f)]=null,_0xab9a9c[_0xf92ab0(0x3ed)]=![],_0xab9a9c[_0xf92ab0(0x9e1)]=![],_0xab9a9c['allowGraphs']=![],_0xab9a9c[_0xf92ab0(0x61e)]=![],_0xab9a9c['resources']=[],_0xab9a9c[_0xf92ab0(0xb47)]=![],_0xab9a9c['autoadd']=![],_0xab9a9c[_0xf92ab0(0x6cf)]=![],_0xab9a9c[_0xf92ab0(0xc80)]='leastused',_0xab9a9c['autochannelIndex']=0x0,_0xab9a9c[_0xf92ab0(0xa4c)]=![],_0xab9a9c['autoSyncObject']=![],_0xab9a9c[_0xf92ab0(0xc85)]=![],_0xab9a9c[_0xf92ab0(0x557)]={},_0xab9a9c[_0xf92ab0(0xaa7)]=!![],_0xab9a9c[_0xf92ab0(0x96b)]=null,_0xab9a9c[_0xf92ab0(0xc62)]=![],_0xab9a9c[_0xf92ab0(0xbe0)]=![],_0xab9a9c[_0xf92ab0(0xa21)]=![],_0xab9a9c['autorecordlocal']=![],_0xab9a9c[_0xf92ab0(0x648)]=![],_0xab9a9c['audience']=![],_0xab9a9c[_0xf92ab0(0xad1)]=![],_0xab9a9c[_0xf92ab0(0x9ec)]=new Set([]),_0xab9a9c[_0xf92ab0(0x4a2)]={},_0xab9a9c[_0xf92ab0(0x719)]=new Set([]),_0xab9a9c[_0xf92ab0(0x6e8)]=new Set([]),_0xab9a9c['pendingApprovalQueries']=new Set([]);function _0x2c6ff3(){return;}_0xab9a9c['promptApproval']=function(_0x3c1e7d){var _0x243aa6=_0xf92ab0;try{if(!_0xab9a9c[_0x243aa6(0x827)]||!_0xab9a9c['approval_popup'])return;var _0x4724b1=(''+'approval-'+_0x3c1e7d)[_0x243aa6(0x35b)](/["<>]/g,'');if(document[_0x243aa6(0xac5)]('.promptModal[data-context=\x22'+_0x4724b1+'\x22]'))return;var _0x258d94=_0xab9a9c[_0x243aa6(0x40a)]!==![];if(_0xab9a9c[_0x243aa6(0xb2c)])try{var _0x3064e3=_0xab9a9c['knockToneEnabled']?'knocktone':_0x243aa6(0x8a8);playtone(![],_0x3064e3);}catch(_0x5c17f9){errorlog(_0x5c17f9);}var _0x497588=_0xab9a9c[_0x243aa6(0x404)][_0x3c1e7d]&&_0xab9a9c[_0x243aa6(0x404)][_0x3c1e7d]['label']||_0x243aa6(0x408)+_0x3c1e7d[_0x243aa6(0x7a4)](0x0,0x8),_0x207bc4=_0xab9a9c[_0x243aa6(0x404)][_0x3c1e7d]&&_0xab9a9c['rpcs'][_0x3c1e7d][_0x243aa6(0xa96)]||_0x3c1e7d;try{_0x497588=(''+_0x497588)[_0x243aa6(0x35b)](/[<>]/g,''),_0x207bc4=(''+_0x207bc4)[_0x243aa6(0x35b)](/[<>]/g,'');}catch(_0x380acc){}var _0x1dbec0=_0x243aa6(0xa36)+_0x243aa6(0xccd)+_0x497588+'\x0a'+_0x243aa6(0x22a)+_0x207bc4+'\x0a\x0a'+'Approve?';confirmAlt(_0x1dbec0,![],_0x243aa6(0x64d)+_0x3c1e7d)[_0x243aa6(0x998)](function(_0x3fc70e){var _0xee9db3=_0x243aa6;if(_0x3fc70e)try{_0x258d94?(_0xab9a9c[_0xee9db3(0x920)](_0xab9a9c[_0xee9db3(0x70d)],{'justResetting':!![]},_0x3c1e7d),_0xab9a9c[_0xee9db3(0x4b0)](_0x3c1e7d,![],_0xee9db3(0x279))):_0xab9a9c[_0xee9db3(0x920)](_0xab9a9c[_0xee9db3(0x70d)],{'justResetting':!![]},_0x3c1e7d);}catch(_0xdcf1ac){errorlog(_0xdcf1ac);}else confirmAlt(_0xee9db3(0xafe))[_0xee9db3(0x998)](function(_0x561066){var _0x58d44e=_0xee9db3;if(_0x561066)try{_0xab9a9c[_0x58d44e(0x936)]({'hangup':!![]},_0x3c1e7d),_0xab9a9c[_0x58d44e(0x4b0)](_0x3c1e7d,![],_0x58d44e(0x645));}catch(_0x2b4868){errorlog(_0x2b4868);}});});}catch(_0x7e621f){errorlog(_0x7e621f);}},_0xab9a9c['flushPendingApprovals']=function(){return;},_0xab9a9c[_0xf92ab0(0x44e)]=function(){return;},_0xab9a9c[_0xf92ab0(0x44a)]=new AudioContext(),_0xab9a9c[_0xf92ab0(0x6b2)]=![],_0xab9a9c[_0xf92ab0(0x283)]=![],_0xab9a9c[_0xf92ab0(0x9e6)]=![],_0xab9a9c[_0xf92ab0(0x75b)]=null,_0xab9a9c['autoGainControl']=null,_0xab9a9c[_0xf92ab0(0xc8e)]=null,_0xab9a9c[_0xf92ab0(0xc11)]=null,_0xab9a9c[_0xf92ab0(0x4e2)]=![],_0xab9a9c[_0xf92ab0(0x9a9)]=![],_0xab9a9c[_0xf92ab0(0xbce)]=![],_0xab9a9c['broadcastIFrame']=![],_0xab9a9c[_0xf92ab0(0x2ce)]=![],_0xab9a9c['screenshareDenoise']=null,_0xab9a9c[_0xf92ab0(0x4d1)]=null,_0xab9a9c[_0xf92ab0(0x7f7)]=null,_0xab9a9c['screenshareStereo']=![],_0xab9a9c[_0xf92ab0(0x2e5)]=![],_0xab9a9c[_0xf92ab0(0x91d)]=0x0,_0xab9a9c[_0xf92ab0(0xab4)]=0x0,_0xab9a9c[_0xf92ab0(0x4f4)]='#000',_0xab9a9c[_0xf92ab0(0x457)]=0x0,_0xab9a9c[_0xf92ab0(0x8a9)]=![],_0xab9a9c[_0xf92ab0(0xa8f)]=![],_0xab9a9c[_0xf92ab0(0x480)]=null,_0xab9a9c[_0xf92ab0(0x42f)]=![],_0xab9a9c[_0xf92ab0(0x4b2)]=![],_0xab9a9c[_0xf92ab0(0x657)]=![];typeof _0xab9a9c[_0xf92ab0(0xa13)]===_0xf92ab0(0x2cf)&&(_0xab9a9c[_0xf92ab0(0xa13)]=![]);if(!_0xab9a9c['includeRTT']&&typeof window!==_0xf92ab0(0x2cf))try{var _0x15a791=new URLSearchParams(window[_0xf92ab0(0xbf8)]['search']||'');_0x15a791[_0xf92ab0(0x2f5)](_0xf92ab0(0xb75))&&(_0xab9a9c[_0xf92ab0(0xa13)]=!![]);}catch(_0xef5292){}_0xab9a9c[_0xf92ab0(0x30c)]=[],_0xab9a9c[_0xf92ab0(0x577)]=null,_0xab9a9c[_0xf92ab0(0xb2c)]=![],_0xab9a9c['blurBackground']=![],_0xab9a9c['slotBroadcastThrottle']=null,_0xab9a9c['canvas']=null,_0xab9a9c[_0xf92ab0(0x805)]=null,_0xab9a9c[_0xf92ab0(0x70e)]=null,_0xab9a9c[_0xf92ab0(0xb1f)]=![],_0xab9a9c[_0xf92ab0(0x29f)]=![],_0xab9a9c['auth']=![],_0xab9a9c[_0xf92ab0(0xb6e)]=![],_0xab9a9c[_0xf92ab0(0xca4)]=![],_0xab9a9c[_0xf92ab0(0x1e8)]=![],_0xab9a9c['closedCaptions']=![],_0xab9a9c[_0xf92ab0(0x7d3)]=![],_0xab9a9c[_0xf92ab0(0x72e)]=![],_0xab9a9c[_0xf92ab0(0x927)]=![],_0xab9a9c[_0xf92ab0(0xa15)]='',_0xab9a9c[_0xf92ab0(0x7ee)]='',_0xab9a9c[_0xf92ab0(0x432)]='',_0xab9a9c['audioCodec']=![],_0xab9a9c[_0xf92ab0(0x44b)]=![],_0xab9a9c['preferVideoCodec']=![],_0xab9a9c['h264profile']=null,_0xab9a9c[_0xf92ab0(0xb7f)]=![],_0xab9a9c[_0xf92ab0(0x79c)]=![],_0xab9a9c['tipQRSize']=0x96,_0xab9a9c[_0xf92ab0(0x4a8)]=![],_0xab9a9c[_0xf92ab0(0xba2)]=null,_0xab9a9c[_0xf92ab0(0x899)]=![],_0xab9a9c[_0xf92ab0(0x9b2)]=0x1,_0xab9a9c['cover']=![],_0xab9a9c['chatbutton']=null,_0xab9a9c[_0xf92ab0(0x59d)]={},_0xab9a9c[_0xf92ab0(0x86f)]=![],_0xab9a9c[_0xf92ab0(0xc30)]=!![],_0xab9a9c[_0xf92ab0(0x2ca)]=[],_0xab9a9c[_0xf92ab0(0x532)]={},_0xab9a9c[_0xf92ab0(0xbc9)]=![],_0xab9a9c[_0xf92ab0(0xa08)]=![],_0xab9a9c[_0xf92ab0(0x6b1)]=null,_0xab9a9c[_0xf92ab0(0xb1a)]=null,_0xab9a9c[_0xf92ab0(0x212)]=![],_0xab9a9c[_0xf92ab0(0x38e)]=![],_0xab9a9c[_0xf92ab0(0x3d4)]=![],_0xab9a9c[_0xf92ab0(0x823)]=!![],_0xab9a9c[_0xf92ab0(0x64b)]=![],_0xab9a9c[_0xf92ab0(0x90e)]=![],_0xab9a9c[_0xf92ab0(0x4a3)]={},_0xab9a9c['currentAudioConstraints']={},_0xab9a9c['colorVideosBackground']=![],_0xab9a9c[_0xf92ab0(0x1bc)]=0x0,_0xab9a9c['zoomedBitrate']=0x25a,_0xab9a9c['structure']=![],_0xab9a9c[_0xf92ab0(0x614)]=![],_0xab9a9c['bitrateGroupFlag']=![],_0xab9a9c[_0xf92ab0(0xa73)]=![],_0xab9a9c[_0xf92ab0(0x773)]=![],_0xab9a9c[_0xf92ab0(0x20b)]=null,_0xab9a9c[_0xf92ab0(0xb2f)]=![],_0xab9a9c[_0xf92ab0(0xca9)]=![],_0xab9a9c['decrypted']=![],_0xab9a9c[_0xf92ab0(0xadf)]=null,_0xab9a9c[_0xf92ab0(0x827)]=![],_0xab9a9c[_0xf92ab0(0x973)]=![],_0xab9a9c[_0xf92ab0(0x637)]=![],_0xab9a9c[_0xf92ab0(0x314)]=![],_0xab9a9c[_0xf92ab0(0x8d6)]=![],_0xab9a9c['disableMouseEvents']=![],_0xab9a9c[_0xf92ab0(0xc10)]=![],_0xab9a9c['directorViewBitrate']=0x23,_0xab9a9c[_0xf92ab0(0x1ba)]=![],_0xab9a9c['directorSpeakerMuted']=null,_0xab9a9c[_0xf92ab0(0x1ee)]=null,_0xab9a9c['directorList']=[],_0xab9a9c[_0xf92ab0(0x959)]=![],_0xab9a9c[_0xf92ab0(0xc5d)]=![],_0xab9a9c[_0xf92ab0(0xb38)]=![],_0xab9a9c['directorStreamID']=![],_0xab9a9c[_0xf92ab0(0x40a)]=null,_0xab9a9c[_0xf92ab0(0x583)]=![],_0xab9a9c[_0xf92ab0(0x33b)]=!![],_0xab9a9c['darkmode']=null,_0xab9a9c[_0xf92ab0(0x67e)]=![],_0xab9a9c[_0xf92ab0(0x7e9)]=![],_0xab9a9c['effect']=![],_0xab9a9c[_0xf92ab0(0x550)]=![],_0xab9a9c[_0xf92ab0(0x4b8)]=![],_0xab9a9c[_0xf92ab0(0x500)]=![],_0xab9a9c['experimental']=![],_0xab9a9c['fakeFeeds']=![],_0xab9a9c[_0xf92ab0(0xb97)]=![],_0xab9a9c[_0xf92ab0(0x43c)]=![],_0xab9a9c[_0xf92ab0(0xbe3)]=![],_0xab9a9c[_0xf92ab0(0x2e1)]=![],_0xab9a9c[_0xf92ab0(0x713)]=![],_0xab9a9c[_0xf92ab0(0x298)]=![],_0xab9a9c['forceRetry']=0x384,_0xab9a9c[_0xf92ab0(0xbea)]=![],_0xab9a9c[_0xf92ab0(0x89f)]=new TextEncoder(_0xf92ab0(0xa46)),_0xab9a9c['exclude']=![],_0xab9a9c['excludeaudio']=![],_0xab9a9c[_0xf92ab0(0xbab)]=![],_0xab9a9c['focusStyle']=![],_0xab9a9c[_0xf92ab0(0x6d7)]=![],_0xab9a9c[_0xf92ab0(0x943)]=![],_0xab9a9c[_0xf92ab0(0x54b)]=![],_0xab9a9c[_0xf92ab0(0xa9f)]=[],_0xab9a9c[_0xf92ab0(0x6eb)]=[],_0xab9a9c[_0xf92ab0(0xa20)]=![],_0xab9a9c[_0xf92ab0(0x303)]=null,_0xab9a9c['firstPlayTriggered']=![],_0xab9a9c[_0xf92ab0(0xb13)]=![],_0xab9a9c[_0xf92ab0(0xa66)]=![],_0xab9a9c['frameRate']=![],_0xab9a9c[_0xf92ab0(0xb5c)]=![],_0xab9a9c[_0xf92ab0(0x7a6)]=![],_0xab9a9c[_0xf92ab0(0x382)]=null,_0xab9a9c[_0xf92ab0(0x707)]=![],_0xab9a9c[_0xf92ab0(0xcb7)]=![],_0xab9a9c[_0xf92ab0(0x96a)]=![],_0xab9a9c[_0xf92ab0(0x377)]=![],_0xab9a9c[_0xf92ab0(0xacf)]=![],_0xab9a9c[_0xf92ab0(0x2c7)]=null,_0xab9a9c[_0xf92ab0(0x34e)]=[],_0xab9a9c[_0xf92ab0(0x7d7)]=[],_0xab9a9c[_0xf92ab0(0xbb5)]=![],_0xab9a9c['groupAudio']=![],_0xab9a9c[_0xf92ab0(0xa8c)]=null,_0xab9a9c[_0xf92ab0(0x669)]=![],_0xab9a9c[_0xf92ab0(0xa5d)]=![],_0xab9a9c[_0xf92ab0(0xbd5)]=![],_0xab9a9c[_0xf92ab0(0x43f)]=![],_0xab9a9c[_0xf92ab0(0x7dc)]=![],_0xab9a9c[_0xf92ab0(0x7ca)]=![],_0xab9a9c[_0xf92ab0(0x288)]=![],_0xab9a9c[_0xf92ab0(0xa74)]=![],_0xab9a9c[_0xf92ab0(0x972)]=[{'urls':[_0xf92ab0(0x1f9),_0xf92ab0(0x696)]}],_0xab9a9c['introButton']=![],_0xab9a9c[_0xf92ab0(0xa6b)]=[],_0xab9a9c[_0xf92ab0(0xcd2)]={},_0xab9a9c['noiframe']=![],_0xab9a9c[_0xf92ab0(0x494)]=![],_0xab9a9c[_0xf92ab0(0xc5c)]=![],_0xab9a9c[_0xf92ab0(0x691)]=0x1,_0xab9a9c[_0xf92ab0(0xb28)]=0x0,_0xab9a9c['quality_ss']=![],_0xab9a9c[_0xf92ab0(0x7f1)]=![],_0xab9a9c[_0xf92ab0(0x2e3)]=![],_0xab9a9c['infocus']=![],_0xab9a9c['infocus2']=![],_0xab9a9c[_0xf92ab0(0x5ef)]=![],_0xab9a9c[_0xf92ab0(0x257)]=![],_0xab9a9c[_0xf92ab0(0xa82)]={},_0xab9a9c['joiningRoom']=![],_0xab9a9c[_0xf92ab0(0x5e0)]=![],_0xab9a9c['keyframeRate']=![],_0xab9a9c[_0xf92ab0(0x7db)]={},_0xab9a9c['lowerVolume']=[],_0xab9a9c[_0xf92ab0(0xbeb)]=![],_0xab9a9c[_0xf92ab0(0x240)]=![],_0xab9a9c[_0xf92ab0(0x55e)]=!![],_0xab9a9c[_0xf92ab0(0x1ac)]=0x1,_0xab9a9c['mids']={},_0xab9a9c[_0xf92ab0(0x1c7)]=![],_0xab9a9c[_0xf92ab0(0xc36)]=[],_0xab9a9c[_0xf92ab0(0x2c5)]=![],_0xab9a9c['maxviewers']=![],_0xab9a9c[_0xf92ab0(0x777)]=![],_0xab9a9c[_0xf92ab0(0x53e)]=![],_0xab9a9c[_0xf92ab0(0xb6c)]=![],_0xab9a9c[_0xf92ab0(0x581)]=![],_0xab9a9c[_0xf92ab0(0x23a)]=![],_0xab9a9c[_0xf92ab0(0x9ea)]=![],_0xab9a9c[_0xf92ab0(0x1c9)]=![],_0xab9a9c[_0xf92ab0(0x9f9)]=![],_0xab9a9c[_0xf92ab0(0x504)]=![],_0xab9a9c[_0xf92ab0(0xacb)]=![],_0xab9a9c['leftMiniPreview']=![],_0xab9a9c[_0xf92ab0(0xa90)]=![],_0xab9a9c[_0xf92ab0(0x2fa)]=![],_0xab9a9c['minptime']=![],_0xab9a9c[_0xf92ab0(0xa1c)]=![],_0xab9a9c[_0xf92ab0(0x336)]=![],_0xab9a9c[_0xf92ab0(0x95b)]=![],_0xab9a9c[_0xf92ab0(0x969)]=![],_0xab9a9c[_0xf92ab0(0x543)]=![],_0xab9a9c[_0xf92ab0(0x512)]=0x15e,_0xab9a9c[_0xf92ab0(0x8b3)]=0x23,_0xab9a9c['labelsize']=![],_0xab9a9c[_0xf92ab0(0x3c9)]=![],_0xab9a9c[_0xf92ab0(0xbc3)]=![],_0xab9a9c[_0xf92ab0(0x379)]=0x2710,_0xab9a9c['layout']=![],_0xab9a9c[_0xf92ab0(0x1a5)]=null,_0xab9a9c[_0xf92ab0(0x2c1)]=![],_0xab9a9c[_0xf92ab0(0x7bb)]=![],_0xab9a9c['layouts']=![],_0xab9a9c[_0xf92ab0(0x1d5)]=![],_0xab9a9c[_0xf92ab0(0xc19)]=_0xab9a9c[_0xf92ab0(0x54d)](0x5),_0xab9a9c['meterStyle']=![],_0xab9a9c['meshcastAudioBitrate']=![],_0xab9a9c[_0xf92ab0(0x4b6)]=![],_0xab9a9c['motionRecord']=![],_0xab9a9c[_0xf92ab0(0x764)]=null,_0xab9a9c[_0xf92ab0(0x856)]=![],_0xab9a9c[_0xf92ab0(0x3a7)]=![],_0xab9a9c[_0xf92ab0(0x830)]=![],_0xab9a9c[_0xf92ab0(0x744)]=null,_0xab9a9c['manualSink']=![],_0xab9a9c[_0xf92ab0(0xa49)]=![],_0xab9a9c[_0xf92ab0(0x80b)]=![],_0xab9a9c['midiOut']=![],_0xab9a9c[_0xf92ab0(0xcd0)]=![],_0xab9a9c[_0xf92ab0(0x758)]=![],_0xab9a9c[_0xf92ab0(0xa7a)]=![],_0xab9a9c[_0xf92ab0(0x529)]=![],_0xab9a9c[_0xf92ab0(0x56a)]=![],_0xab9a9c['midiOffset']=0x17,_0xab9a9c[_0xf92ab0(0x325)]=![],_0xab9a9c[_0xf92ab0(0x726)]=![],_0xab9a9c[_0xf92ab0(0xce5)]=![],_0xab9a9c[_0xf92ab0(0x5c8)]=![],_0xab9a9c[_0xf92ab0(0x5a7)]=![],_0xab9a9c[_0xf92ab0(0xa3c)]=![],_0xab9a9c[_0xf92ab0(0x420)]=![],_0xab9a9c[_0xf92ab0(0x540)]=![],_0xab9a9c[_0xf92ab0(0x2f0)]=[],_0xab9a9c[_0xf92ab0(0x33f)]=![],_0xab9a9c[_0xf92ab0(0xb87)]=![],_0xab9a9c[_0xf92ab0(0x821)]=![],_0xab9a9c[_0xf92ab0(0x78c)]=![],_0xab9a9c['meshcast2FallbackAttempted']=![],_0xab9a9c['meshcast2Anonymous']=![],_0xab9a9c[_0xf92ab0(0xa58)]=![],_0xab9a9c[_0xf92ab0(0xa0a)]=null,_0xab9a9c[_0xf92ab0(0x69a)]=![],_0xab9a9c[_0xf92ab0(0x49f)]=![],_0xab9a9c[_0xf92ab0(0x987)]=![],_0xab9a9c[_0xf92ab0(0x926)]=![],_0xab9a9c['whipoutScreenSettingsUserSet']=![],_0xab9a9c[_0xf92ab0(0x623)]=![],_0xab9a9c['noMeshcast']=![],_0xab9a9c[_0xf92ab0(0x5e4)]=![],_0xab9a9c[_0xf92ab0(0x8b2)]=![],_0xab9a9c['muted_activeSpeaker']=![],_0xab9a9c[_0xf92ab0(0x56b)]=![],_0xab9a9c[_0xf92ab0(0xafa)]=![],_0xab9a9c[_0xf92ab0(0x767)]={},_0xab9a9c[_0xf92ab0(0x2f3)]=![],_0xab9a9c[_0xf92ab0(0x9bc)]=![],_0xab9a9c[_0xf92ab0(0x98f)]=![],_0xab9a9c[_0xf92ab0(0xb96)]=![],_0xab9a9c['maxAvailableSlots']=0x14,_0xab9a9c[_0xf92ab0(0xbc7)]=![],_0xab9a9c[_0xf92ab0(0x886)]=![],_0xab9a9c[_0xf92ab0(0x3b7)]=![],_0xab9a9c[_0xf92ab0(0xaf8)]=null,_0xab9a9c['nocursor']=![],_0xab9a9c[_0xf92ab0(0xcd5)]=![],_0xab9a9c[_0xf92ab0(0x647)]=![],_0xab9a9c['obsfix']=![],_0xab9a9c[_0xf92ab0(0x571)]=![],_0xab9a9c[_0xf92ab0(0x3b8)]=![],_0xab9a9c[_0xf92ab0(0x46c)]=![],_0xab9a9c[_0xf92ab0(0x294)]=![],_0xab9a9c[_0xf92ab0(0x66b)]=![],_0xab9a9c['remoteHash']=![],_0xab9a9c[_0xf92ab0(0x681)]=![],_0xab9a9c['obsState']={},_0xab9a9c[_0xf92ab0(0x444)][_0xf92ab0(0x569)]=null,_0xab9a9c[_0xf92ab0(0x444)]['streaming']=null,_0xab9a9c[_0xf92ab0(0x444)]['recording']=null,_0xab9a9c[_0xf92ab0(0x444)][_0xf92ab0(0xbcf)]=null,_0xab9a9c[_0xf92ab0(0x444)][_0xf92ab0(0x4ea)]=null,_0xab9a9c[_0xf92ab0(0xadd)]=![],_0xab9a9c[_0xf92ab0(0x28f)]=_0xf92ab0(0xaec),_0xab9a9c['outboundVideoBitrate_userSet']=![],_0xab9a9c[_0xf92ab0(0x8a7)]=![],_0xab9a9c[_0xf92ab0(0xcc6)]=![],_0xab9a9c[_0xf92ab0(0x3c0)]=![],_0xab9a9c[_0xf92ab0(0x9ae)]=![],_0xab9a9c[_0xf92ab0(0x7df)]=![],_0xab9a9c['panning']=![],_0xab9a9c[_0xf92ab0(0x913)]=![],_0xab9a9c['password']=![],_0xab9a9c[_0xf92ab0(0x4e9)]=null,_0xab9a9c[_0xf92ab0(0xc9e)]=![],_0xab9a9c[_0xf92ab0(0x7e5)]=![],_0xab9a9c[_0xf92ab0(0x87c)]=![],_0xab9a9c[_0xf92ab0(0x490)]=![],_0xab9a9c[_0xf92ab0(0x4b4)]=![],_0xab9a9c[_0xf92ab0(0xbdf)]=null,_0xab9a9c[_0xf92ab0(0xca3)]=![],_0xab9a9c['overlayControls']=![],_0xab9a9c[_0xf92ab0(0x332)]=0x5dc,_0xab9a9c['preset']=![],_0xab9a9c['pcs']={},_0xab9a9c[_0xf92ab0(0x999)]=![],_0xab9a9c['pip3']=![],_0xab9a9c[_0xf92ab0(0x714)]=![],_0xab9a9c['autoPiPPromptVideo']=![],_0xab9a9c[_0xf92ab0(0x34a)]=![],_0xab9a9c[_0xf92ab0(0x858)]=![],_0xab9a9c[_0xf92ab0(0x98e)]=![],_0xab9a9c[_0xf92ab0(0x52b)]=![],_0xab9a9c[_0xf92ab0(0x4dd)]=![],_0xab9a9c[_0xf92ab0(0xb06)]=![],_0xab9a9c[_0xf92ab0(0x94c)]=![],_0xab9a9c[_0xf92ab0(0xcc0)]=![],_0xab9a9c[_0xf92ab0(0x6d8)]=![],_0xab9a9c[_0xf92ab0(0xac2)]=![],_0xab9a9c[_0xf92ab0(0xc1c)]=0x1e,_0xab9a9c['posterImage']=![],_0xab9a9c[_0xf92ab0(0x3c8)]=![],_0xab9a9c[_0xf92ab0(0x3cc)]='https://temp.vdo.ninja/',_0xab9a9c[_0xf92ab0(0x486)]=![],_0xab9a9c['proxy']=![],_0xab9a9c[_0xf92ab0(0x66a)]=null,_0xab9a9c[_0xf92ab0(0x682)]=null,_0xab9a9c[_0xf92ab0(0xa12)]=![],_0xab9a9c['pseudoguest']=![],_0xab9a9c[_0xf92ab0(0x61b)]=!![],_0xab9a9c[_0xf92ab0(0xbde)]=![],_0xab9a9c[_0xf92ab0(0x9fc)]=![],_0xab9a9c['queueType']=![],_0xab9a9c[_0xf92ab0(0xb3c)]=[],_0xab9a9c[_0xf92ab0(0x1dd)]=[],_0xab9a9c[_0xf92ab0(0xc23)]=![],_0xab9a9c[_0xf92ab0(0x218)]=![],_0xab9a9c['relaywss']=![],_0xab9a9c[_0xf92ab0(0x9c1)]=![],_0xab9a9c[_0xf92ab0(0x740)]=![],_0xab9a9c[_0xf92ab0(0x22b)]=![],_0xab9a9c[_0xf92ab0(0xb4c)]=![],_0xab9a9c['record']=!![],_0xab9a9c[_0xf92ab0(0x93b)]=0x1770,_0xab9a9c[_0xf92ab0(0x1d9)]=![],_0xab9a9c[_0xf92ab0(0x2b5)]=0x1770,_0xab9a9c[_0xf92ab0(0x57b)]=![],_0xab9a9c[_0xf92ab0(0x831)]=0x1388,_0xab9a9c['recordingVideoCodec']=![],_0xab9a9c[_0xf92ab0(0x6b8)]=![],_0xab9a9c['roomenc']=![],_0xab9a9c[_0xf92ab0(0x70d)]=![],_0xab9a9c[_0xf92ab0(0x9c3)]=![],_0xab9a9c[_0xf92ab0(0x249)]=![],_0xab9a9c[_0xf92ab0(0x436)]=![],_0xab9a9c[_0xf92ab0(0xa0f)]=null,_0xab9a9c['showRoomTime']=![],_0xab9a9c['rotate']=![],_0xab9a9c[_0xf92ab0(0x1e9)]=!![],_0xab9a9c['requireencryption']=![],_0xab9a9c[_0xf92ab0(0x34f)]=![],_0xab9a9c[_0xf92ab0(0x45c)]=![],_0xab9a9c[_0xf92ab0(0x6ca)]={},_0xab9a9c['rpcs']={},_0xab9a9c[_0xf92ab0(0xa84)]=![],_0xab9a9c['sampleRate']=![],_0xab9a9c[_0xf92ab0(0x63f)]=![],_0xab9a9c[_0xf92ab0(0x5e9)]=![],_0xab9a9c[_0xf92ab0(0x44c)]=null,_0xab9a9c[_0xf92ab0(0x4c3)]=![],_0xab9a9c[_0xf92ab0(0x6f6)]=![],_0xab9a9c[_0xf92ab0(0xa53)]=![],_0xab9a9c[_0xf92ab0(0xbd1)]=![],_0xab9a9c[_0xf92ab0(0x96d)]=![],_0xab9a9c[_0xf92ab0(0x558)]=![],_0xab9a9c[_0xf92ab0(0x8fa)]=[],_0xab9a9c[_0xf92ab0(0x92f)]={},_0xab9a9c[_0xf92ab0(0x470)]=![],_0xab9a9c[_0xf92ab0(0x4f7)]=![],_0xab9a9c[_0xf92ab0(0xb21)]=![],_0xab9a9c[_0xf92ab0(0x335)]=![],_0xab9a9c[_0xf92ab0(0x94f)]='*',_0xab9a9c['scene']=![],_0xab9a9c[_0xf92ab0(0x585)]=![],_0xab9a9c[_0xf92ab0(0x79e)]={},_0xab9a9c['silence']=![],_0xab9a9c[_0xf92ab0(0x5ce)]=0x1f4,_0xab9a9c['slotsList']=![],_0xab9a9c['syncState']=![],_0xab9a9c[_0xf92ab0(0x4c7)]=null,_0xab9a9c['sdpSemantics']='unified-plan',_0xab9a9c['screenshare']=![],_0xab9a9c[_0xf92ab0(0x914)]=![],_0xab9a9c['screenShareElement']=![],_0xab9a9c[_0xf92ab0(0xcce)]=![],_0xab9a9c[_0xf92ab0(0xb54)]=![],_0xab9a9c[_0xf92ab0(0x761)]=![],_0xab9a9c[_0xf92ab0(0xc15)]=![],_0xab9a9c[_0xf92ab0(0x701)]=![],_0xab9a9c[_0xf92ab0(0x572)]=![],_0xab9a9c[_0xf92ab0(0xc3b)]=![],_0xab9a9c[_0xf92ab0(0x5f4)]=![],_0xab9a9c[_0xf92ab0(0x1d3)]=![],_0xab9a9c[_0xf92ab0(0x430)]=![],_0xab9a9c[_0xf92ab0(0x915)]=![],_0xab9a9c[_0xf92ab0(0x41d)]=![],_0xab9a9c['sensorData']=![],_0xab9a9c[_0xf92ab0(0x285)]=['pos',_0xf92ab0(0x290),_0xf92ab0(0x8aa),'mag',_0xf92ab0(0x904),'acc'],_0xab9a9c[_0xf92ab0(0x9c5)]=0x0,_0xab9a9c[_0xf92ab0(0x55d)]=![],_0xab9a9c['surfaceSwitching']=![],_0xab9a9c[_0xf92ab0(0x6a0)]=![],_0xab9a9c[_0xf92ab0(0x225)]=![],_0xab9a9c[_0xf92ab0(0x5f1)]=![],_0xab9a9c[_0xf92ab0(0x74a)]=![],_0xab9a9c[_0xf92ab0(0xab7)]=![],_0xab9a9c[_0xf92ab0(0x2c0)]=![],_0xab9a9c[_0xf92ab0(0x67c)]=![],_0xab9a9c[_0xf92ab0(0xcb1)]=![],_0xab9a9c['devicePixelRatio']=![],_0xab9a9c[_0xf92ab0(0x6e5)]=![],_0xab9a9c['screenshareVideoOnly']=![],_0xab9a9c[_0xf92ab0(0x591)]=null,_0xab9a9c[_0xf92ab0(0x4e1)]=![],_0xab9a9c[_0xf92ab0(0x365)]=[],_0xab9a9c[_0xf92ab0(0xb1d)]=0xbb8,_0xab9a9c['redirectHangup']=![],_0xab9a9c['screenShareElementHidden']=![],_0xab9a9c[_0xf92ab0(0x9fa)]=![],_0xab9a9c['scalabilityMode']=![],_0xab9a9c['showSettings']=!![],_0xab9a9c[_0xf92ab0(0x990)]=![],_0xab9a9c['sink']=![],_0xab9a9c[_0xf92ab0(0x472)]=![],_0xab9a9c[_0xf92ab0(0x8f0)]=![],_0xab9a9c[_0xf92ab0(0x28b)]=null,_0xab9a9c[_0xf92ab0(0x1ad)]=![],_0xab9a9c[_0xf92ab0(0x287)]={},_0xab9a9c[_0xf92ab0(0x77e)]=![],_0xab9a9c['maxScene']=0x8,_0xab9a9c[_0xf92ab0(0x271)]=![],_0xab9a9c[_0xf92ab0(0x32d)]=![],_0xab9a9c[_0xf92ab0(0x695)]=![],_0xab9a9c[_0xf92ab0(0x6a6)]=null,_0xab9a9c[_0xf92ab0(0x41a)]=0xbb8,_0xab9a9c['store']=![],_0xab9a9c[_0xf92ab0(0x839)]=![],_0xab9a9c['streamID']=null,_0xab9a9c['streamSrc']=null,_0xab9a9c[_0xf92ab0(0x7fd)]=null,_0xab9a9c[_0xf92ab0(0x2d4)]=null,_0xab9a9c['style']=![],_0xab9a9c[_0xf92ab0(0x95a)]=![],_0xab9a9c[_0xf92ab0(0xae6)]=![],_0xab9a9c[_0xf92ab0(0x9f6)]=null,_0xab9a9c[_0xf92ab0(0xc16)]=![],_0xab9a9c[_0xf92ab0(0x92b)]=![],_0xab9a9c[_0xf92ab0(0x683)]=0x1f4,_0xab9a9c[_0xf92ab0(0x5ff)]=![],_0xab9a9c[_0xf92ab0(0x615)]=null,_0xab9a9c[_0xf92ab0(0x73e)]=['./media/bg_sample.webp',_0xf92ab0(0x655)],_0xab9a9c['defaultForegroundImages']=[_0xf92ab0(0x7d6)],_0xab9a9c['selectedImage_contents']=![],_0xab9a9c[_0xf92ab0(0xa14)]=![],_0xab9a9c[_0xf92ab0(0x7bf)]=![],_0xab9a9c[_0xf92ab0(0x239)]=![],_0xab9a9c[_0xf92ab0(0x630)]=![],_0xab9a9c['effectsImage']=![],_0xab9a9c['tz']=![],_0xab9a9c[_0xf92ab0(0x94b)]=![],_0xab9a9c[_0xf92ab0(0x231)]=![],_0xab9a9c[_0xf92ab0(0x6a1)]=![],_0xab9a9c[_0xf92ab0(0xba9)]=![],_0xab9a9c['transferred']=![],_0xab9a9c['twilio']=![],_0xab9a9c[_0xf92ab0(0x2bb)]=![],_0xab9a9c['videoElement']=![],_0xab9a9c['videoMuted']=![],_0xab9a9c[_0xf92ab0(0x2d3)]=![],_0xab9a9c[_0xf92ab0(0x8b4)]=![],_0xab9a9c['remoteVideoMuted']=![],_0xab9a9c[_0xf92ab0(0xc68)]=![],_0xab9a9c['view']=![],_0xab9a9c[_0xf92ab0(0x4f1)]=![],_0xab9a9c[_0xf92ab0(0x57d)]=![],_0xab9a9c[_0xf92ab0(0x6b6)]=![],_0xab9a9c[_0xf92ab0(0x28e)]=![],_0xab9a9c['warnUserTriggered']=![],_0xab9a9c[_0xf92ab0(0x94e)]=![],_0xab9a9c[_0xf92ab0(0x82b)]=![],_0xab9a9c[_0xf92ab0(0xcd6)]=![],_0xab9a9c[_0xf92ab0(0x232)]=![],_0xab9a9c[_0xf92ab0(0x33d)]=![],_0xab9a9c[_0xf92ab0(0x608)]={},_0xab9a9c[_0xf92ab0(0x635)]={},_0xab9a9c[_0xf92ab0(0xc49)]=![],_0xab9a9c[_0xf92ab0(0x2b6)]=null,_0xab9a9c[_0xf92ab0(0x1fd)]=![],_0xab9a9c[_0xf92ab0(0xa09)]=![],_0xab9a9c['waitImage']=![],_0xab9a9c['waitImageTimeout']=0x1388,_0xab9a9c['waitImageTimeoutObject']=![],_0xab9a9c[_0xf92ab0(0x2f4)]={},_0xab9a9c[_0xf92ab0(0x620)]=![],_0xab9a9c['webPquality']=![],_0xab9a9c['ws']=null,_0xab9a9c[_0xf92ab0(0xad2)]=![],_0xab9a9c[_0xf92ab0(0x649)]=null,_0xab9a9c['website']=![],_0xab9a9c['welcomeMessage']=![],_0xab9a9c[_0xf92ab0(0x939)]=![],_0xab9a9c[_0xf92ab0(0x576)]=![],_0xab9a9c[_0xf92ab0(0x864)]=![],_0xab9a9c[_0xf92ab0(0xaf1)]=![],_0xab9a9c[_0xf92ab0(0x300)]=![],_0xab9a9c[_0xf92ab0(0x84b)]=![],_0xab9a9c[_0xf92ab0(0x56f)]=![],_0xab9a9c[_0xf92ab0(0xcde)]=![],_0xab9a9c[_0xf92ab0(0x786)]=![],_0xab9a9c[_0xf92ab0(0x5b2)]=![],_0xab9a9c[_0xf92ab0(0x56e)]=![],_0xab9a9c[_0xf92ab0(0x52b)]=![],_0xab9a9c[_0xf92ab0(0x380)]=![],_0xab9a9c[_0xf92ab0(0x824)]=![],_0xab9a9c['whipOutScreen']=![],_0xab9a9c[_0xf92ab0(0xc28)]=![],_0xab9a9c['whipoutScreenSettings']=![],_0xab9a9c['whipPublishPrimary']=!![],_0xab9a9c['whipPublishScreen']=!![],_0xab9a9c[_0xf92ab0(0x66e)]=_0xf92ab0(0x471),_0xab9a9c['whepInput']=![],_0xab9a9c[_0xf92ab0(0x709)]=0x7d0,_0xab9a9c['whipWait']=0x7d0,_0xab9a9c[_0xf92ab0(0x919)]=![],_0xab9a9c[_0xf92ab0(0x79d)]=![],_0xab9a9c['whiteBalance']=![],_0xab9a9c['exposure']=![],_0xab9a9c[_0xf92ab0(0x4d7)]=![],_0xab9a9c['sharpness']=![],_0xab9a9c[_0xf92ab0(0xac1)]=![],_0xab9a9c[_0xf92ab0(0x9c4)]=![],_0xab9a9c[_0xf92ab0(0xb5c)]=![],_0xab9a9c[_0xf92ab0(0x2eb)]=!![],_0xab9a9c[_0xf92ab0(0x2b7)]=null,_0xab9a9c[_0xf92ab0(0x48f)]=![],_0xab9a9c['dbx']=![],_0xab9a9c[_0xf92ab0(0x942)]=null,_0xab9a9c['pauseInvisible']=![],_0xab9a9c[_0xf92ab0(0xa1a)]=![],_0xab9a9c[_0xf92ab0(0x769)]=![],_0xab9a9c[_0xf92ab0(0xbd0)]=![],_0xab9a9c[_0xf92ab0(0x8ea)]=![],_0xab9a9c['redAudio']=![],_0xab9a9c[_0xf92ab0(0xb88)]=![],_0xab9a9c['detune']=![],_0xab9a9c[_0xf92ab0(0x3cd)]=null,_0xab9a9c[_0xf92ab0(0xbfd)]='',_0xab9a9c[_0xf92ab0(0xaf2)]=null,_0xab9a9c[_0xf92ab0(0x536)]=![],_0xab9a9c['viewheight']=![],_0xab9a9c[_0xf92ab0(0x2a2)]=![],_0xab9a9c[_0xf92ab0(0xa02)]=![],_0xab9a9c[_0xf92ab0(0xb10)]=null,_0xab9a9c['UUID']=![],_0xab9a9c['localMuteElement']=getById('muteStateTemplate')[_0xf92ab0(0x5b5)](!![]),_0xab9a9c[_0xf92ab0(0x7c4)]=null,_0xab9a9c[_0xf92ab0(0x98d)]['id']='localMuteElement',_0xab9a9c[_0xf92ab0(0xb1c)]=getById(_0xf92ab0(0x8c6))[_0xf92ab0(0x5b5)](!![]),_0xab9a9c[_0xf92ab0(0xb1c)]['id']='localVoiceMeter',_0xab9a9c['voiceMeter'][_0xf92ab0(0x1b0)]['opacity']=0x0,_0xab9a9c[_0xf92ab0(0xb1c)][_0xf92ab0(0xa18)][_0xf92ab0(0x9b1)]=0x0,_0xab9a9c[_0xf92ab0(0x8cf)]=![],_0xab9a9c[_0xf92ab0(0x42e)]=![],_0xab9a9c[_0xf92ab0(0x4b3)]=0x19,_0xab9a9c[_0xf92ab0(0x892)]=![],_0xab9a9c[_0xf92ab0(0xb64)]=![],_0xab9a9c[_0xf92ab0(0x36b)]=![],_0xab9a9c[_0xf92ab0(0x6b4)]=!![],_0xab9a9c[_0xf92ab0(0xaa2)]=![],_0xab9a9c[_0xf92ab0(0x847)]=!![],_0xab9a9c[_0xf92ab0(0x2e9)]=!![],_0xab9a9c[_0xf92ab0(0xc00)]=![],_0xab9a9c[_0xf92ab0(0x9d5)]=![],_0xab9a9c[_0xf92ab0(0x2a3)]=![],_0xab9a9c[_0xf92ab0(0x242)]=_0xf92ab0(0x941),_0xab9a9c['GDRIVE_API_KEY']='AIzaSyAcboxS2N-39sfn1xn9jNCebvKkuHAdlNk',_0xab9a9c[_0xf92ab0(0x2bf)]=_0xf92ab0(0x730),_0xab9a9c[_0xf92ab0(0x851)]=_0xf92ab0(0xc3d);if(location['hostname']==_0xf92ab0(0x962))_0xab9a9c[_0xf92ab0(0x7a9)]=_0xf92ab0(0x962);else{if(location[_0xf92ab0(0x9f3)]==_0xf92ab0(0x820))_0xab9a9c[_0xf92ab0(0x7a9)]=_0xf92ab0(0x962);else{if([_0xf92ab0(0x962),_0xf92ab0(0x4c0),_0xf92ab0(0x387),'socialstream.ninja']['includes'](location[_0xf92ab0(0x9f3)][_0xf92ab0(0x55a)]('.')[_0xf92ab0(0x912)](-0x2)['join']('.')))_0xab9a9c['salt']=location['hostname']['split']('.')[_0xf92ab0(0x912)](-0x2)[_0xf92ab0(0x1e2)]('.');else try{var _0x41e9fa=/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;if(_0x41e9fa[_0xf92ab0(0x6f2)](window[_0xf92ab0(0xbf8)]['hostname']))_0xab9a9c['salt']=_0xf92ab0(0x962);else window['location'][_0xf92ab0(0x9f3)]=='localhost'?_0xab9a9c[_0xf92ab0(0x7a9)]=_0xf92ab0(0x962):_0xab9a9c['salt']=location['hostname'];}catch(_0x1e0bc7){_0xab9a9c[_0xf92ab0(0x7a9)]=location['hostname'],errorlog(_0x1e0bc7);}}}_0xab9a9c[_0xf92ab0(0x6b3)]=(function(){var _0x4ab6ac=location['hostname'];if(_0x4ab6ac==='vdo.ninja')return!![];if(_0x4ab6ac==='dev.versus.cam')return!![];return![];}());_0xab9a9c[_0xf92ab0(0x6b3)]&&(_0xab9a9c[_0xf92ab0(0xa06)]={'startTime':Date['now'](),'connectionSuccesses':0x0,'connectionFailures':0x0,'iceRestarts':0x0,'packetLossVideoSamples':[],'packetLossAudioSamples':[],'rttSamples':[],'jitterSamples':[],'bitrateSamples':[],'turnServersUsed':[],'meshcastServersUsed':[],'candidateTypesLocal':[],'candidateTypesRemote':[],'lastVideoCodec':null,'lastAudioCodec':null,'lastResolution':null,'transportType':null,'sent':![],'wssSuccess':![]});_0xab9a9c[_0xf92ab0(0xab5)]=function(_0x4e4976,_0x156462=_0xab9a9c[_0xf92ab0(0x54c)]+_0xab9a9c[_0xf92ab0(0x7a9)]){var _0x3786e3=_0xf92ab0,_0x504b26=crypto['getRandomValues'](new Uint8Array(0x10));return crypto[_0x3786e3(0x4a1)][_0x3786e3(0xc98)]({'name':_0x3786e3(0x836)},convertStringToArrayBufferView(_0x156462))[_0x3786e3(0x998)](function(_0x219d4f){var _0x4de158=_0x3786e3;return window['crypto']['subtle'][_0x4de158(0x2da)]('raw',_0x219d4f,{'name':_0x4de158(0x5ed)},![],[_0x4de158(0x6af),_0x4de158(0xb17)])[_0x4de158(0x998)](function(_0x21ccc4){var _0x343136=_0x4de158;return crypto['subtle'][_0x343136(0x6af)]({'name':_0x343136(0x5ed),'iv':_0x504b26},_0x21ccc4,convertStringToArrayBufferView(_0x4e4976))['then'](function(_0x4ba00e){return encrypted_data=new Uint8Array(_0x4ba00e),encrypted_data=toHexString(encrypted_data),_0x504b26=toHexString(_0x504b26),[encrypted_data,_0x504b26];},function(_0x39f170){var _0x2ad2cd=_0x343136;return errorlog(_0x39f170[_0x2ad2cd(0x3a3)]),![];});},function(_0x37a374){return errorlog(_0x37a374),![];});})['catch'](errorlog);},_0xab9a9c[_0xf92ab0(0x347)]=function(_0x289a5b,_0x251e5e,_0x33331a=_0xab9a9c[_0xf92ab0(0x54c)]+_0xab9a9c[_0xf92ab0(0x7a9)]){var _0x39a376=_0xf92ab0;return _0x289a5b=toByteArray(_0x289a5b),_0x251e5e=toByteArray(_0x251e5e),crypto[_0x39a376(0x4a1)][_0x39a376(0xc98)]({'name':_0x39a376(0x836)},convertStringToArrayBufferView(_0x33331a))['then'](function(_0x478bf6){var _0x567f24=_0x39a376;return window['crypto']['subtle']['importKey'](_0x567f24(0x9db),_0x478bf6,{'name':'AES-CBC'},![],[_0x567f24(0x6af),_0x567f24(0xb17)])[_0x567f24(0x998)](function(_0x34b201){var _0x101011=_0x567f24;return crypto[_0x101011(0x4a1)][_0x101011(0xb17)]({'name':_0x101011(0x5ed),'iv':_0x251e5e},_0x34b201,_0x289a5b)['then'](function(_0x20aaa5){var _0x297667=_0x101011,_0x29912b=new Uint8Array(_0x20aaa5),_0x5f4b46='';for(var _0x451949=0x0;_0x451949<_0x29912b[_0x297667(0xc04)];_0x451949++){_0x5f4b46+=String[_0x297667(0xa77)](_0x29912b[_0x451949]);}return _0x5f4b46;},function(_0xfdc388){return errorlog(_0x251e5e),errorlog(_0x289a5b),errorlog(_0xfdc388),![];});});})[_0x39a376(0xacc)](errorlog);},_0xab9a9c['decodeRemote']=async function(_0x1d6e45){var _0x209111=_0xf92ab0;if(typeof _0x1d6e45[_0x209111(0x1d9)]!==_0x209111(0x537))return _0x1d6e45;try{_0x1d6e45[_0x209111(0x1d9)]['length']==0x2&&(!_0xab9a9c[_0x209111(0x42a)]&&(_0xab9a9c[_0x209111(0x42a)]=await generateHash(_0xab9a9c[_0x209111(0x1d9)]+_0xab9a9c['salt'],0xc)),_0x1d6e45['remote']=await _0xab9a9c['decryptMessage'](_0x1d6e45['remote'][0x0],_0x1d6e45[_0x209111(0x1d9)][0x1],_0xab9a9c['remoteHash']),_0x1d6e45[_0x209111(0x1d9)]?log(_0x209111(0xb4a)):warnlog(_0x209111(0x1f5)),log(_0x1d6e45));}catch(_0x3f7f30){errorlog(_0x3f7f30);}return _0x1d6e45;},_0xab9a9c[_0xf92ab0(0xc21)]=async function(_0xa00536){var _0x513549=_0xf92ab0;try{if(_0xa00536[_0x513549(0x1d9)]&&typeof _0xa00536[_0x513549(0x1d9)]===_0x513549(0x9cb)){var _0x39847e=await generateHash(_0xa00536[_0x513549(0x1d9)]+_0xab9a9c[_0x513549(0x7a9)],0xc);_0xa00536[_0x513549(0x1d9)]=await _0xab9a9c[_0x513549(0xab5)](_0xa00536['remote'],_0x39847e);}}catch(_0xfc3d85){errorlog(_0xfc3d85);}return _0xa00536;},_0xab9a9c[_0xf92ab0(0xa56)]=function(_0x13559b){var _0x19d7b1=_0xf92ab0;try{try{_0x13559b=decodeURIComponent(_0x13559b[_0x19d7b1(0x35b)](/ /g,'+'));}catch(_0x42b301){}_0x13559b=CryptoJS[_0x19d7b1(0x58b)][_0x19d7b1(0xb17)](_0x13559b,_0x19d7b1(0xc7b)),_0x13559b=_0x13559b[_0x19d7b1(0xc7a)](CryptoJS[_0x19d7b1(0x89f)]['Utf8']);if(_0x13559b){if(_0x13559b[_0x19d7b1(0x602)](_0x19d7b1(0x491)))_0x13559b=_0x13559b[_0x19d7b1(0x35b)](_0x19d7b1(0x491),'');else{if(_0x13559b[_0x19d7b1(0x602)](_0x19d7b1(0x21f)))_0x13559b=_0x13559b[_0x19d7b1(0x35b)](_0x19d7b1(0x21f),'');else{if(_0x13559b['startsWith']('/'))_0x13559b=_0x13559b[_0x19d7b1(0x35b)]('/','');else{if(_0x13559b['startsWith'](_0x19d7b1(0x5e5)))_0x13559b=_0x13559b[_0x19d7b1(0x35b)](_0x19d7b1(0x5e5),'');else{if(_0x13559b[_0x19d7b1(0x602)]('vdo.ninja/'))_0x13559b=_0x13559b[_0x19d7b1(0x35b)](_0x19d7b1(0x9af),'');else _0x13559b[_0x19d7b1(0x602)](_0x19d7b1(0x6c1))&&(_0x13559b=_0x13559b['replace'](_0x19d7b1(0x6c1),''));}}}}_0x13559b=_0x13559b[_0x19d7b1(0x55a)]('?')[_0x19d7b1(0x954)](0x1)[_0x19d7b1(0x1e2)]('?'),_0x13559b&&(_0x13559b='?'+_0x13559b[_0x19d7b1(0x35b)](/\?/g,'&'),_0xab9a9c[_0x19d7b1(0xbf7)]=_0x13559b);}}catch(_0x2c5703){warnlog(_0x2c5703);}},_0xab9a9c['requestKeyframe']=function(_0x27f634,_0x3e406f=![]){var _0x1f562e=_0xf92ab0,_0x216635={};_0x216635['keyframe']=!![],_0x216635['scene']=_0x3e406f,_0xab9a9c[_0x1f562e(0x936)](_0x216635,_0x27f634);},_0xab9a9c[_0xf92ab0(0x1f2)]=function(_0x11ff8b,_0xe7592e,_0xfd12ab=null){var _0x2a607c=_0xf92ab0;if(!_0xab9a9c['rpcs'][_0xe7592e])return![];var _0x53f9d2={};if(_0xfd12ab!==null)_0xab9a9c[_0x2a607c(0x404)][_0xe7592e][_0x2a607c(0xcd7)]=_0xfd12ab||![];else{if(_0xab9a9c[_0x2a607c(0x404)][_0xe7592e][_0x2a607c(0xcd7)]){warnlog(_0x2a607c(0x3fe));return;}}_0x53f9d2[_0x2a607c(0x6bb)]=_0x11ff8b,log(_0x53f9d2),_0xab9a9c['sendRequest'](_0x53f9d2,_0xe7592e);},_0xab9a9c[_0xf92ab0(0x5b3)]=function(_0x260afe,_0x5ee139,_0x1c453e=![],_0x402de8=null){var _0x35f0fa=_0xf92ab0;log(_0x35f0fa(0x725)+_0x1c453e);if(!_0xab9a9c[_0x35f0fa(0x404)][_0x5ee139]||!_0xab9a9c[_0x35f0fa(0x404)][_0x5ee139]['getStats'])return![];if(_0x402de8!==null)_0xab9a9c[_0x35f0fa(0x404)][_0x5ee139][_0x35f0fa(0x458)]=_0x402de8||![];else{if(_0xab9a9c[_0x35f0fa(0x404)][_0x5ee139][_0x35f0fa(0x458)]){warnlog(_0x35f0fa(0xa9c));return;}}if(_0x260afe===![]){}else _0xab9a9c['rpcs'][_0x5ee139]['targetBandwidth']=_0x260afe;var _0x265aad=-0x1;_0xab9a9c[_0x35f0fa(0x404)][_0x5ee139][_0x35f0fa(0x9b6)]!==![]?_0x260afe=parseInt(_0xab9a9c['rpcs'][_0x5ee139][_0x35f0fa(0x9b6)]):_0x260afe=parseInt(_0xab9a9c['rpcs'][_0x5ee139][_0x35f0fa(0x3b9)]);if(_0xab9a9c['obsState']['visibility']===![]){if(_0xab9a9c[_0x35f0fa(0x46c)]!==![]){if(window[_0x35f0fa(0x704)])return![];}}else{if(_0xab9a9c[_0x35f0fa(0x4b6)]&&_0x260afe===0x0)return![];}_0x260afe===0x0&&_0xab9a9c[_0x35f0fa(0x404)][_0x5ee139]['remoteMuteState']&&(_0x260afe=0x1);if(_0xab9a9c[_0x35f0fa(0x404)][_0x5ee139][_0x35f0fa(0x3ce)]===_0x260afe)return![];log(_0x35f0fa(0xb76)+_0x260afe);var _0x54194c={};_0x54194c['bitrate']=_0x260afe;if(_0x1c453e===null){}else{if(_0x1c453e)_0x260afe===0x0?(warnlog('OPTIMIZED\x20AUDIO\x20ENABLED;\x20zero\x20bitrate'),_0x54194c[_0x35f0fa(0x6bb)]=0x0):_0x265aad<0x10&&_0x265aad>=0x0?_0x54194c[_0x35f0fa(0x6bb)]=_0x265aad:_0x54194c['audioBitrate']=0x10;else _0x402de8===null&&(_0x54194c[_0x35f0fa(0x6bb)]=_0x265aad);}return _0xab9a9c[_0x35f0fa(0x936)](_0x54194c,_0x5ee139)?(_0xab9a9c['rpcs'][_0x5ee139][_0x35f0fa(0x3ce)]=_0x260afe,!![]):(setTimeout(function _0xb6ebf7(){var _0x15e73d=_0x35f0fa;_0xab9a9c[_0x15e73d(0x5b3)](![],_0x5ee139);},0x1388),warnlog('couldn\x27t\x20set\x20rate\x20limit'),![]);},_0xab9a9c['sendGenericData']=function(_0x3d9ea3,_0x1e09b7=![],_0x802c34=![],_0x288864=![]){var _0x1521ad=_0xf92ab0,_0x3e196b=![],_0x5e9bc5={};_0x5e9bc5[_0x1521ad(0xc03)]=_0x3d9ea3;try{if(!_0x1e09b7&&!_0x802c34){if(_0x288864==_0x1521ad(0x404))_0xab9a9c[_0x1521ad(0x936)](_0x5e9bc5);else _0x288864==_0x1521ad(0x84f)?_0xab9a9c[_0x1521ad(0x1eb)](_0x5e9bc5):_0xab9a9c[_0x1521ad(0xae3)](_0x5e9bc5);_0x3e196b=!![];}else{if(_0x1e09b7){_0x1e09b7=_0x1e09b7+'';if(_0x288864==_0x1521ad(0x404))_0xab9a9c['sendRequest'](_0x5e9bc5,_0x1e09b7);else _0x288864==_0x1521ad(0x84f)?_0xab9a9c[_0x1521ad(0x1eb)](_0x5e9bc5,_0x1e09b7):_0xab9a9c[_0x1521ad(0xae3)](_0x5e9bc5,_0x1e09b7);_0x3e196b=!![];}else{if(_0x802c34){_0x802c34=_0x802c34+'';for(var _0xd4862b in _0xab9a9c['rpcs']){if(_0xab9a9c[_0x1521ad(0x404)][_0xd4862b][_0x1521ad(0xa96)]===_0x802c34){if(_0x288864==_0x1521ad(0x404))_0xab9a9c['sendRequest'](_0x5e9bc5,_0xd4862b);else _0x288864=='pcs'?_0xab9a9c[_0x1521ad(0x1eb)](_0x5e9bc5,_0xd4862b):_0xab9a9c[_0x1521ad(0xae3)](_0x5e9bc5,_0xd4862b);_0x3e196b=!![];}}}}}return _0x3e196b;}catch(_0x4b5f06){return![];}},_0xab9a9c['gotGenericData']=function(_0x398414,_0x20c75c){var _0x3229d9=_0xf92ab0,_0x1e24de={};_0x1e24de[_0x3229d9(0x421)]={},_0x1e24de['dataReceived']=_0x398414;_0x20c75c!==null&&(_0x1e24de[_0x3229d9(0xab9)]=_0x20c75c);if(isIFrame)parent[_0x3229d9(0x3c6)](_0x1e24de,_0xab9a9c[_0x3229d9(0x94f)]);else _0x398414[_0x3229d9(0x3c3)]&&!isIFrame&&getChatMessage(_0x398414['overlayNinja']['chatmessage'],_0x398414['overlayNinja']['chatname'],![],![]);},_0xab9a9c[_0xf92ab0(0x6ce)]=function(){var _0x4f36ec=_0xf92ab0;if(_0xab9a9c[_0x4f36ec(0x50b)]===null)return;for(var _0x5ae9b4 in _0xab9a9c[_0x4f36ec(0x404)]){try{var _0x5a5ee2=getReceivers2(_0x5ae9b4);for(var _0x5cd9c1=0x0;_0x5cd9c1<_0x5a5ee2[_0x4f36ec(0xade)];_0x5cd9c1++){_0x5a5ee2[_0x5cd9c1][_0x4f36ec(0x50d)][_0x4f36ec(0xcd1)]==_0x4f36ec(0x544)&&(ChromiumVersion&&ChromiumVersion>=0x85&&(_0x5a5ee2[_0x5cd9c1][_0x4f36ec(0x50d)][_0x4f36ec(0x8e4)]=!![]),_0x5a5ee2[_0x5cd9c1]['track'][_0x4f36ec(0x8e4)]=!_0xab9a9c[_0x4f36ec(0x50b)]);}}catch(_0x5ff313){}}_0xab9a9c['directorSpeakerMuted']&&(getById(_0x4f36ec(0x931))[_0x4f36ec(0x8b2)]=!![]);},_0xab9a9c[_0xf92ab0(0xcb9)]=function(){var _0x25b3b2=_0xf92ab0;if(_0xab9a9c[_0x25b3b2(0x1ee)]===null)return;_0xab9a9c[_0x25b3b2(0x1ee)]?(getById(_0x25b3b2(0xb32))[_0x25b3b2(0xc93)][_0x25b3b2(0xada)](_0x25b3b2(0x63d)),!_0xab9a9c['cleanOutput']&&warnUser(getTranslation(_0x25b3b2(0x8e6)),![],![])):(getById(_0x25b3b2(0xb32))[_0x25b3b2(0xc93)][_0x25b3b2(0x50f)](_0x25b3b2(0x63d)),!_0xab9a9c['cleanOutput']&&closeModal());for(var _0x34fcfd in _0xab9a9c['rpcs']){try{var _0x460530=getReceivers2(_0x34fcfd);for(var _0x3f7c6e=0x0;_0x3f7c6e<_0x460530[_0x25b3b2(0xade)];_0x3f7c6e++){_0x460530[_0x3f7c6e][_0x25b3b2(0x50d)][_0x25b3b2(0xcd1)]==_0x25b3b2(0xa55)&&(_0x460530[_0x3f7c6e][_0x25b3b2(0x50d)][_0x25b3b2(0x8e4)]=!![],_0x460530[_0x3f7c6e][_0x25b3b2(0x50d)][_0x25b3b2(0x8e4)]=!_0xab9a9c['directorDisplayMuted']);}}catch(_0x246ef9){errorlog(_0x246ef9);}}_0xab9a9c[_0x25b3b2(0x1ee)]&&(getById(_0x25b3b2(0x931))[_0x25b3b2(0x8b2)]=!![]);},_0xab9a9c[_0xf92ab0(0x3b0)]=async function(){var _0x1c5491=_0xf92ab0;await _0xab9a9c[_0x1c5491(0x3ea)]();if(_0xab9a9c['shadowBanned']){log(_0x1c5491(0x341));return;}else{if(_0xab9a9c[_0x1c5491(0x50a)]!==![])_0xab9a9c[_0x1c5491(0x50a)]=_0x1c5491(0xc6a),log('seeding\x20blocked');else{if(_0xab9a9c['doNotSeed'])log(_0x1c5491(0x6ee));else{var _0xfbf021={};_0xfbf021[_0x1c5491(0x7aa)]=_0x1c5491(0x2f9),_0xab9a9c['authMode']&&_0xab9a9c['realStreamID']?(_0xfbf021[_0x1c5491(0xa96)]=_0xab9a9c['realStreamID'],log(_0x1c5491(0xbf4)+_0xab9a9c[_0x1c5491(0x310)])):_0xfbf021[_0x1c5491(0xa96)]=_0xab9a9c[_0x1c5491(0xa96)],_0xab9a9c['sendMsg'](_0xfbf021),log(_0x1c5491(0x985)),pokeAPI('seeding',!![]),pokeIframeAPI(_0x1c5491(0x42c),!![]),pokeIframeAPI(_0x1c5491(0x41d),!![]);}}}},_0xab9a9c[_0xf92ab0(0xb80)]=function(){var _0x5ab2e6=_0xf92ab0;getById(_0x5ab2e6(0x3f0))[_0x5ab2e6(0xca0)]=!![],getById(_0x5ab2e6(0x3f0))[_0x5ab2e6(0xbb2)]=_0x5ab2e6(0x7c9),getById(_0x5ab2e6(0xc5b))[_0x5ab2e6(0xc93)][_0x5ab2e6(0xada)](_0x5ab2e6(0x63d)),_0xab9a9c[_0x5ab2e6(0x959)]&&(_0xab9a9c['directorHash']?_0xab9a9c[_0x5ab2e6(0xb38)]&&(_0xab9a9c[_0x5ab2e6(0xb38)]in _0xab9a9c[_0x5ab2e6(0x404)]&&(_0xab9a9c[_0x5ab2e6(0x404)][_0xab9a9c[_0x5ab2e6(0xb38)]][_0x5ab2e6(0x9f4)]===![]&&_0xab9a9c[_0x5ab2e6(0xab5)](_0xab9a9c[_0x5ab2e6(0xc5d)],_0xab9a9c['directorHash'])['then'](function(_0x37b214){var _0xd0c239=_0x5ab2e6,_0x94a897={};_0x94a897[_0xd0c239(0xab9)]=_0xab9a9c[_0xd0c239(0xb38)],_0x94a897[_0xd0c239(0xb80)]=_0x37b214[0x0],_0x94a897['vector']=_0x37b214[0x1],_0xab9a9c[_0xd0c239(0x404)][_0xab9a9c[_0xd0c239(0xb38)]][_0xd0c239(0x9f4)]===![]&&(_0xab9a9c['sendRequest'](_0x94a897,_0x94a897['UUID'])&&(_0xab9a9c[_0xd0c239(0x404)][_0xab9a9c[_0xd0c239(0xb38)]][_0xd0c239(0x9f4)]=!![]));})[_0x5ab2e6(0xacc)](errorlog))):generateHash(_0xab9a9c[_0x5ab2e6(0x959)]+_0xab9a9c['salt']+_0x5ab2e6(0x9d8),0xc)[_0x5ab2e6(0x998)](function(_0x15c3c9){var _0x2ba19b=_0x5ab2e6;_0xab9a9c[_0x2ba19b(0xc5d)]=_0x15c3c9;_0xab9a9c[_0x2ba19b(0xb38)]&&(_0xab9a9c['rpcs'][_0xab9a9c[_0x2ba19b(0xb38)]][_0x2ba19b(0x9f4)]===![]&&_0xab9a9c['encryptMessage'](_0xab9a9c['directorHash'],_0xab9a9c[_0x2ba19b(0xc5d)])[_0x2ba19b(0x998)](function(_0x4feb65){var _0x4b08f7=_0x2ba19b,_0x1c1931={};_0x1c1931[_0x4b08f7(0xab9)]=_0xab9a9c[_0x4b08f7(0xb38)],_0x1c1931[_0x4b08f7(0xb80)]=_0x4feb65[0x0],_0x1c1931[_0x4b08f7(0xa85)]=_0x4feb65[0x1],_0xab9a9c[_0x4b08f7(0x404)][_0xab9a9c[_0x4b08f7(0xb38)]][_0x4b08f7(0x9f4)]===![]&&(_0xab9a9c['sendRequest'](_0x1c1931,_0x1c1931[_0x4b08f7(0xab9)])&&(_0xab9a9c['rpcs'][_0xab9a9c[_0x4b08f7(0xb38)]][_0x4b08f7(0x9f4)]=!![]));})[_0x2ba19b(0xacc)](errorlog));return;})[_0x5ab2e6(0xacc)](errorlog));},_0xab9a9c['pixelFix']=function(_0x1dd30f,_0x17a07c){return _0x1dd30f;},_0xab9a9c[_0xf92ab0(0x9fe)]=function(_0x13f9f3=![]){var _0x406f4a=_0xf92ab0;log(_0x406f4a(0xc02));if(_0x13f9f3){if(!_0xab9a9c['pcs'][_0x13f9f3])return![];if(_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0xa05)]!==![]||_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0x39a)]!==![]||_0xab9a9c['pcs'][_0x13f9f3][_0x406f4a(0x447)]!==![])return log(_0x406f4a(0x708)+_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0x39a)]+_0x406f4a(0xb05)+_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0x447)]),_0xab9a9c[_0x406f4a(0x97b)](_0x13f9f3,_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0x39a)],_0xab9a9c['pcs'][_0x13f9f3][_0x406f4a(0x447)],_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0x1b6)],_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0xb0c)]),!![];else{if(_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0xa53)]!==![])return log(_0x406f4a(0x5d3)),_0xab9a9c[_0x406f4a(0x24f)](_0x13f9f3,_0xab9a9c[_0x406f4a(0x84f)][_0x13f9f3][_0x406f4a(0xa53)],!![]),!![];}}else for(var _0x4ab559 in _0xab9a9c[_0x406f4a(0x84f)]){setTimeout(function(_0x31d6ba){var _0x2032cb=_0x406f4a;if(_0xab9a9c[_0x2032cb(0x84f)][_0x31d6ba][_0x2032cb(0xa05)]!==![]||_0xab9a9c['pcs'][_0x31d6ba]['scaleWidth']!==![]||_0xab9a9c[_0x2032cb(0x84f)][_0x31d6ba][_0x2032cb(0x447)]!==![])log(_0x2032cb(0x708)+_0xab9a9c[_0x2032cb(0x84f)][_0x31d6ba]['scaleWidth']+_0x2032cb(0xb05)+_0xab9a9c[_0x2032cb(0x84f)][_0x31d6ba][_0x2032cb(0x447)]),_0xab9a9c['setResolution'](_0x31d6ba,_0xab9a9c['pcs'][_0x31d6ba][_0x2032cb(0x39a)],_0xab9a9c[_0x2032cb(0x84f)][_0x31d6ba][_0x2032cb(0x447)],_0xab9a9c[_0x2032cb(0x84f)][_0x31d6ba]['scaleSnap'],_0xab9a9c[_0x2032cb(0x84f)][_0x31d6ba][_0x2032cb(0xb0c)]);else _0xab9a9c['pcs'][_0x31d6ba][_0x2032cb(0xa53)]!==![]&&(log(_0x2032cb(0x5d3)),_0xab9a9c[_0x2032cb(0x24f)](_0x31d6ba,_0xab9a9c[_0x2032cb(0x84f)][_0x31d6ba][_0x2032cb(0xa53)],!![]));},0x0,_0x4ab559);}return![];},_0xab9a9c[_0xf92ab0(0x2f6)]=function(_0x3f49dc=_0xab9a9c[_0xf92ab0(0xadd)]){var _0x581348=_0xf92ab0;warnlog(_0x581348(0x7ea));if(_0xab9a9c['whipOut'][_0x581348(0xa53)]!==_0x3f49dc){if(_0x3f49dc==null){try{var _0x5c9122=_0xab9a9c[_0x581348(0x52b)][_0x581348(0x3f7)]()[_0x581348(0x54f)](function(_0x5d0379){var _0x19d913=_0x581348;return _0x5d0379[_0x19d913(0x50d)]&&_0x5d0379[_0x19d913(0x50d)][_0x19d913(0xcd1)]==_0x19d913(0xa55);});}catch(_0x13d3ed){errorlog(_0x13d3ed);}if(!_0x5c9122){warnlog(_0x581348(0x234));return;}var _0x385b79=_0x5c9122[_0x581348(0x1a4)]();(!_0x385b79[_0x581348(0x342)]||_0x385b79[_0x581348(0x342)][_0x581348(0xade)]==0x0)&&(_0x385b79['encodings']=[{}]),_0x581348(0x267)in _0x385b79['encodings'][0x0]?(_0x3f49dc=0x64/_0x385b79['encodings'][0x0][_0x581348(0x267)],_0x3f49dc=_0x3f49dc*0.95):_0x3f49dc=0x5f;}else _0xab9a9c['whipOut'][_0x581348(0xa53)]=_0x3f49dc;try{if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad))log('iOS\x20devices\x20do\x20not\x20support\x20dynamic\x20bitrates\x20correctly;\x20skipping');else{if(_0x581348(0x534)in window&&'setParameters'in window[_0x581348(0x534)]['prototype']){try{var _0x5c9122=_0xab9a9c[_0x581348(0x52b)][_0x581348(0x3f7)]()['find'](function(_0x5ab864){var _0x20cd7c=_0x581348;return _0x5ab864[_0x20cd7c(0x50d)]&&_0x5ab864[_0x20cd7c(0x50d)][_0x20cd7c(0xcd1)]==_0x20cd7c(0xa55);});}catch(_0x6fc692){errorlog(_0x6fc692);}if(!_0x5c9122){warnlog(_0x581348(0x234));return;}var _0x4e4e37={};if(_0x3f49dc<=0x0||_0x3f49dc==0x64){var _0x3cd8c7=getChromiumVersion();_0x3cd8c7>0x50?_0x4e4e37[_0x581348(0x267)]=null:_0x4e4e37[_0x581348(0x267)]=0x1;}else _0x4e4e37['scaleResolutionDownBy']=0x64/_0x3f49dc;setEncodings(_0x5c9122,_0x4e4e37,function(_0x462788){var _0x3e7c3d=_0x581348;log(_0x3e7c3d(0x7b4)),pokeIframeAPI(_0x3e7c3d(0xb36),_0x462788,_0x3e7c3d(0xb87)),pokeIframeAPI(_0x3e7c3d(0x3bf),_0x462788,_0x3e7c3d(0xb87)),_0xab9a9c[_0x3e7c3d(0x52b)][_0x3e7c3d(0x287)][_0x3e7c3d(0x367)]=parseInt(_0x462788)+'%';},_0x3f49dc);return;}}}catch(_0x52b9be){errorlog(_0x52b9be);}}},_0xab9a9c[_0xf92ab0(0x24f)]=function(_0x24727a,_0x37c017,_0x11fa30=![]){var _0x2f312f=_0xf92ab0;warnlog(_0x2f312f(0x5c1)+_0x37c017);try{_0xab9a9c[_0x2f312f(0x84f)][_0x24727a]['stats'][_0x2f312f(0x367)]=_0x37c017;}catch(_0x2117d4){errorlog(_0x2117d4);}if(!_0x11fa30&&_0xab9a9c[_0x2f312f(0x84f)][_0x24727a][_0x2f312f(0xa53)]===_0x37c017)return;if(_0x37c017==null){try{var _0x1c5ae2=getSenders2(_0x24727a)[_0x2f312f(0x54f)](function(_0x590853){var _0x16ef67=_0x2f312f;return _0x590853[_0x16ef67(0x50d)]&&_0x590853[_0x16ef67(0x50d)][_0x16ef67(0xcd1)]=='video';});}catch(_0x37a193){errorlog(_0x37a193);}if(!_0x1c5ae2){warnlog(_0x2f312f(0x234));return;}var _0x1bfafa=_0x1c5ae2[_0x2f312f(0x1a4)]();(!_0x1bfafa['encodings']||_0x1bfafa['encodings']['length']==0x0)&&(_0x1bfafa[_0x2f312f(0x342)]=[{}]),_0x2f312f(0x267)in _0x1bfafa[_0x2f312f(0x342)][0x0]?(_0x37c017=0x64/_0x1bfafa[_0x2f312f(0x342)][0x0][_0x2f312f(0x267)],_0x37c017=_0x37c017*0.95):_0x37c017=0x5f;}else _0x37c017=Math[_0x2f312f(0xc48)](_0x37c017),_0xab9a9c[_0x2f312f(0x84f)][_0x24727a]['scale']=_0x37c017;try{if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad))log(_0x2f312f(0x700));else{if('RTCRtpSender'in window&&_0x2f312f(0x2e7)in window[_0x2f312f(0x534)][_0x2f312f(0x479)]){try{var _0x1c5ae2=getSenders2(_0x24727a)[_0x2f312f(0x54f)](function(_0x586d56){var _0x26a016=_0x2f312f;return _0x586d56[_0x26a016(0x50d)]&&_0x586d56[_0x26a016(0x50d)][_0x26a016(0xcd1)]==_0x26a016(0xa55);});}catch(_0xede6f4){errorlog(_0xede6f4);}if(!_0x1c5ae2){warnlog(_0x2f312f(0x234));return;}_0x37c017=_0xab9a9c[_0x2f312f(0xb77)](_0x24727a,![],_0x37c017);var _0x48f046={};if(_0x37c017<=0x0||_0x37c017==0x64){var _0x97a2aa=getChromiumVersion();_0x97a2aa>0x50?_0x48f046[_0x2f312f(0x267)]=null:_0x48f046[_0x2f312f(0x267)]=0x1;}else _0x48f046[_0x2f312f(0x267)]=0x64/_0x37c017;setEncodings(_0x1c5ae2,_0x48f046,function(_0x11fb72){var _0x20b2f2=_0x2f312f;log(_0x20b2f2(0x5d7)+_0x11fb72[0x0]),pokeIframeAPI(_0x20b2f2(0xb36),_0x11fb72[0x0],_0x11fb72[0x1]),pokeIframeAPI(_0x20b2f2(0x3bf),_0x11fb72[0x0],_0x11fb72[0x1]),_0xab9a9c[_0x20b2f2(0x84f)][_0x11fb72[0x1]][_0x20b2f2(0x287)]['scaleFactor']=parseInt(_0x11fb72[0x0])+'%';},[_0x37c017,_0x24727a]);return;}}}catch(_0x2de837){errorlog(_0x2de837);}},_0xab9a9c[_0xf92ab0(0x8eb)]=function(_0x230e4d,_0x5f0ed7,_0x11e365,_0x5124de=![],_0x8cc7d6=![],_0x5c2efc=null){var _0x529877=_0xf92ab0;if(!(_0x230e4d in _0xab9a9c[_0x529877(0x404)]))return;_0x5c2efc===null&&(_0x5c2efc=_0xab9a9c[_0x529877(0xb0c)]||![]);var _0x969423=![];!(_0xab9a9c[_0x529877(0x404)][_0x230e4d][_0x529877(0x39a)]==Math[_0x529877(0x502)](_0x5f0ed7)||_0xab9a9c[_0x529877(0x404)][_0x230e4d][_0x529877(0x39a)]===Math['ceil'](_0x5f0ed7))&&(_0x5f0ed7=Math['round'](_0x5f0ed7),_0xab9a9c['rpcs'][_0x230e4d][_0x529877(0x39a)]=_0x5f0ed7,_0x969423=!![]);!(_0xab9a9c[_0x529877(0x404)][_0x230e4d]['scaleHeight']==Math[_0x529877(0x502)](_0x11e365)||_0xab9a9c[_0x529877(0x404)][_0x230e4d][_0x529877(0x447)]===Math['ceil'](_0x11e365))&&(_0x11e365=Math[_0x529877(0xc45)](_0x11e365),_0xab9a9c[_0x529877(0x404)][_0x230e4d][_0x529877(0x447)]=_0x11e365,_0x969423=!![]);_0xab9a9c[_0x529877(0x404)][_0x230e4d]['scaleSnap']!=_0x5124de&&(_0xab9a9c[_0x529877(0x404)][_0x230e4d]['scaleSnap']=_0x5124de,_0x969423=!![]);_0x5f0ed7=Math[_0x529877(0xc45)](_0x5f0ed7),_0x11e365=Math[_0x529877(0xc45)](_0x11e365);if(_0x969423){var _0x30c2fe={};_0x30c2fe['UUID']=_0x230e4d,_0x30c2fe['requestResolution']={'w':_0x5f0ed7,'h':_0x11e365,'s':_0x5124de,'c':_0x5c2efc},_0x8cc7d6&&(_0x30c2fe['requestAs']=_0x8cc7d6),log(_0x5f0ed7+'\x20'+_0x11e365),_0xab9a9c['sendRequest'](_0x30c2fe,_0x230e4d);}_0x5124de?_0xab9a9c[_0x529877(0x404)][_0x230e4d][_0x529877(0x287)][_0x529877(0x7e8)]='~\x20'+parseInt(_0x5f0ed7)+'\x20x\x20'+parseInt(_0x11e365):_0xab9a9c[_0x529877(0x404)][_0x230e4d][_0x529877(0x287)][_0x529877(0x7e8)]=parseInt(_0x5f0ed7)+_0x529877(0xb05)+parseInt(_0x11e365);},_0xab9a9c[_0xf92ab0(0xb77)]=function(_0x4eada7,_0x1c6395=![],_0x5c1ddf=![]){var _0x5d2907=_0xf92ab0;if(_0x5c1ddf){}else _0xab9a9c['pcs'][_0x4eada7]['scale']?_0x5c1ddf=_0xab9a9c['pcs'][_0x4eada7][_0x5d2907(0xa53)]:_0x5c1ddf=0x64;_0xab9a9c[_0x5d2907(0x84f)][_0x4eada7][_0x5d2907(0xa05)]&&_0x5c1ddf>_0xab9a9c[_0x5d2907(0x84f)][_0x4eada7][_0x5d2907(0xa05)]&&(_0x5c1ddf=_0xab9a9c[_0x5d2907(0x84f)][_0x4eada7][_0x5d2907(0xa05)]);if(_0x1c6395)_0x5c1ddf=_0x3ff164(_0x4eada7,_0x5c1ddf,_0x1c6395);else _0xab9a9c[_0x5d2907(0x84f)][_0x4eada7]['scaleDueToBitrate']&&_0xab9a9c[_0x5d2907(0x84f)][_0x4eada7][_0x5d2907(0x8d1)]<_0x5c1ddf&&(_0x5c1ddf=_0xab9a9c[_0x5d2907(0x84f)][_0x4eada7]['scaleDueToBitrate']);if(_0xab9a9c[_0x5d2907(0xc15)]&&_0xab9a9c[_0x5d2907(0x84f)][_0x4eada7]['scaleSnap']){if(_0x5c1ddf>0x55)_0x5c1ddf=0x64;else _0x5c1ddf>0x2a&&_0x5c1ddf<0x32&&(_0x5c1ddf=0x32);}return _0x5c1ddf=_0xab9a9c[_0x5d2907(0xae8)](_0x5c1ddf,_0x4eada7),_0x5c1ddf;},_0xab9a9c[_0xf92ab0(0x97b)]=function(_0x185192=![],_0x51dfc2=null,_0x4c5c44=null,_0xa6cb8=![],_0x335f27=![]){var _0x153cf7=_0xf92ab0;log(_0x153cf7(0x974)+_0x51dfc2+'x'+_0x4c5c44);if(_0x185192&&!(_0x185192 in _0xab9a9c[_0x153cf7(0x84f)]))return;else{if(!_0x185192){for(var _0x1f5f37 in _0xab9a9c[_0x153cf7(0x84f)]){_0xab9a9c[_0x153cf7(0x97b)](_0x1f5f37,_0xab9a9c[_0x153cf7(0x84f)][_0x1f5f37]['scaleWidth'],_0xab9a9c[_0x153cf7(0x84f)][_0x1f5f37][_0x153cf7(0x447)],_0xab9a9c[_0x153cf7(0x84f)][_0x1f5f37][_0x153cf7(0x1b6)],_0xab9a9c['pcs'][_0x1f5f37][_0x153cf7(0xb0c)]);}return;}}_0x335f27=_0x335f27||![],snape=_0xa6cb8||![];if(_0x51dfc2===null&&_0x4c5c44===null){if(!_0xab9a9c[_0x153cf7(0x84f)][_0x185192][_0x153cf7(0x39a)]&&!_0xab9a9c['pcs'][_0x185192][_0x153cf7(0x447)])return;else _0x51dfc2=_0xab9a9c[_0x153cf7(0x84f)][_0x185192]['scaleWidth']||0x64,_0x4c5c44=_0xab9a9c[_0x153cf7(0x84f)][_0x185192]['scaleHeight']||0x64;}else _0xab9a9c[_0x153cf7(0x84f)][_0x185192]['scaleWidth']=_0x51dfc2,_0xab9a9c[_0x153cf7(0x84f)][_0x185192][_0x153cf7(0x447)]=_0x4c5c44,_0xab9a9c['pcs'][_0x185192][_0x153cf7(0x1b6)]=_0xa6cb8,_0xab9a9c[_0x153cf7(0x84f)][_0x185192]['cover']=_0x335f27;if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad))return;if(_0x153cf7(0x534)in window&&_0x153cf7(0x2e7)in window[_0x153cf7(0x534)][_0x153cf7(0x479)]){var _0x4ca268=getSenders2(_0x185192)[_0x153cf7(0x54f)](function(_0x5af870){var _0x11aeab=_0x153cf7;return _0x5af870['track']&&_0x5af870['track'][_0x11aeab(0xcd1)]==_0x11aeab(0xa55);});if(!_0x4ca268){log(_0x153cf7(0x46a));return;}var _0x840143={};if(_0x153cf7(0x2a8)in _0xab9a9c[_0x153cf7(0x84f)][_0x185192]){var _0x2ab288=_0xab9a9c['screenStream'][_0x153cf7(0x5c7)]();if(_0x2ab288[_0x153cf7(0xade)])var _0x2a0a7a=_0x2ab288[0x0][_0x153cf7(0x498)](),_0x27d4d8=_0x2a0a7a[_0x153cf7(0x43f)],_0x355248=_0x2a0a7a['width'];else return;}else{if(_0xab9a9c['videoElement']&&_0xab9a9c[_0x153cf7(0x7eb)][_0x153cf7(0x5f6)]){var _0x2ab288=_0xab9a9c[_0x153cf7(0x7eb)][_0x153cf7(0x5f6)][_0x153cf7(0x5c7)]();if(_0x2ab288[_0x153cf7(0xade)])var _0x2a0a7a=_0x2ab288[0x0]['getSettings'](),_0x27d4d8=_0x2a0a7a['height'],_0x355248=_0x2a0a7a[_0x153cf7(0x28e)];else return;}else return;}var _0x35a8a1=0x64*_0x51dfc2/_0x355248,_0x2717e5=0x64*_0x4c5c44/_0x27d4d8;warnlog(_0x35a8a1+_0x153cf7(0xb05)+_0x2717e5);var _0x1a84b1=0x64;if(_0x51dfc2===null)_0x1a84b1=_0x2717e5;else{if(_0x4c5c44===null)_0x1a84b1=_0x35a8a1;else _0x335f27?_0x35a8a1>_0x2717e5?_0x1a84b1=_0x35a8a1:_0x1a84b1=_0x2717e5:_0x35a8a1<_0x2717e5?_0x1a84b1=_0x35a8a1:_0x1a84b1=_0x2717e5;}_0x1a84b1>0x64&&(_0x1a84b1=0x64);log(_0x153cf7(0x708)+_0x1a84b1),_0xab9a9c[_0x153cf7(0x84f)][_0x185192]['scaleResolution']=_0x1a84b1;var _0x212a01=_0xab9a9c[_0x153cf7(0xb77)](_0x185192);if(_0x212a01<=0x0||_0x212a01==0x64){var _0xb00bcb=getChromiumVersion();_0xb00bcb>0x50?_0x840143[_0x153cf7(0x267)]=null:_0x840143[_0x153cf7(0x267)]=0x1;}else _0x840143['scaleResolutionDownBy']=0x64/_0x212a01;setEncodings(_0x4ca268,_0x840143,function(_0x239eb8){var _0x276ab0=_0x153cf7;log('scale\x20set!'),pokeIframeAPI('setVideoScale',_0x239eb8[0x0],_0x239eb8[0x1]),pokeIframeAPI(_0x276ab0(0x3bf),_0x239eb8[0x0],_0x239eb8[0x1]),_0xab9a9c[_0x276ab0(0x84f)][_0x239eb8[0x1]]['stats'][_0x276ab0(0x367)]=parseInt(_0x239eb8[0x0])+'%';},[_0x212a01,_0x185192]);return;}},_0xab9a9c['forcePLI']=function(_0x3fce42=null,_0x16b851=null){var _0x59603b=_0xf92ab0;_0x16b851&&_0x16b851[_0x59603b(0xbd8)]();_0xab9a9c[_0x59603b(0xbc9)]&&(_0xab9a9c['chunkedRecorder'][_0x59603b(0x1ff)]=!![],log('FORCING\x20A\x20CHUNKED\x20KEY\x20FRAME:\x20'+_0x3fce42));if(iOS||iPad)return log('iOS\x20devices\x20do\x20not\x20support\x20dynamic\x20bitrates\x20correctly;\x20skipping'),![];else{if(_0x59603b(0x534)in window&&'setParameters'in window['RTCRtpSender'][_0x59603b(0x479)]){log(_0x59603b(0x21c)+_0x3fce42);if(_0x3fce42==null){for(_0x3fce42 in _0xab9a9c[_0x59603b(0x84f)]){_0xab9a9c[_0x59603b(0xa03)](_0x3fce42);}return![];}if(!(_0x3fce42 in _0xab9a9c[_0x59603b(0x84f)]))return![];_0xab9a9c[_0x59603b(0x84f)][_0x3fce42][_0x59603b(0x85e)]&&(_0xab9a9c[_0x59603b(0x84f)][_0x3fce42][_0x59603b(0x4bc)]&&(clearTimeout(_0xab9a9c[_0x59603b(0x84f)][_0x3fce42][_0x59603b(0x4bc)]),_0xab9a9c[_0x59603b(0x84f)][_0x3fce42][_0x59603b(0x4bc)]=null),_0xab9a9c[_0x59603b(0x84f)][_0x3fce42][_0x59603b(0x4bc)]=setTimeout(function(_0x7bc27){var _0x23dcfd=_0x59603b;!_0xab9a9c[_0x23dcfd(0x84f)][_0x7bc27]?clearInterval(this):_0xab9a9c['forcePLI'](_0x7bc27);},parseInt(_0xab9a9c[_0x59603b(0x84f)][_0x3fce42][_0x59603b(0x85e)]),_0x3fce42));try{var _0x352de4=getSenders2(_0x3fce42)[_0x59603b(0x54f)](function(_0x4d47ef){var _0x346506=_0x59603b;return _0x4d47ef[_0x346506(0x50d)]&&_0x4d47ef[_0x346506(0x50d)]['kind']==_0x346506(0xa55);});if(!_0x352de4)return warnlog(_0x59603b(0x46a)),![];var _0xfd2590={};return _0xfd2590[_0x59603b(0x267)]=0xa,setEncodings(_0x352de4,_0xfd2590,function(_0xc161b8){var _0x10fe60=_0x59603b;log(_0x10fe60(0x47d)+_0xc161b8[0x0]);var _0x2d12f7=_0xab9a9c[_0x10fe60(0xb77)](_0xc161b8[0x0]),_0x203359={};if(_0x2d12f7<=0x0||_0x2d12f7==0x64){var _0x2a9e1c=getChromiumVersion();_0x2a9e1c>0x50?_0x203359['scaleResolutionDownBy']=null:_0x203359[_0x10fe60(0x267)]=0x1;}else _0x203359[_0x10fe60(0x267)]=0x64/_0x2d12f7;setEncodings(_0xc161b8[0x1],_0x203359,function(){var _0x264887=_0x10fe60;log(_0x264887(0x703));});},[_0x3fce42,_0x352de4]),!![];}catch(_0x17e235){errorlog(_0x17e235);}}}return![];},_0xab9a9c['enhanceAudioEncoder']=function(_0x40d580){var _0x57186d=_0xf92ab0;log(_0x57186d(0x981));var _0x493d54=getSenders2(_0x40d580)[_0x57186d(0x54f)](function(_0x44db8b){var _0x4008bd=_0x57186d;return _0x44db8b[_0x4008bd(0x50d)]&&_0x44db8b['track'][_0x4008bd(0xcd1)]==_0x4008bd(0x544);});if(!_0x493d54)return log('no\x20audio\x20track\x20to\x20poke'),![];var _0x2e31f3={};try{_0x2e31f3[_0x57186d(0xb67)]=_0x57186d(0x412),_0x2e31f3[_0x57186d(0x98b)]=_0x57186d(0x412),_0x2e31f3[_0x57186d(0x6bd)]=!![],setEncodings(_0x493d54,_0x2e31f3,function(_0x49f27b){var _0x3bdd3b=_0x57186d;log(_0x3bdd3b(0x5dc)),pokeIframeAPI('prioritize-audio',!![],_0x49f27b);},_0x40d580);}catch(_0xe02034){errorlog(_0xe02034);}},_0xab9a9c['degradationPreference']=function(_0x2e81d9,_0x1f5260='maintain-framerate'){var _0x33f6eb=_0xf92ab0,_0x3fa213=getSenders2(_0x2e81d9)[_0x33f6eb(0x54f)](function(_0x5e52e7){var _0x404433=_0x33f6eb;return _0x5e52e7[_0x404433(0x50d)]&&_0x5e52e7[_0x404433(0x50d)][_0x404433(0xcd1)]==_0x404433(0xa55);});if(!_0x3fa213)return log(_0x33f6eb(0x4a0)),![];var _0xf57e3c={};try{_0x1f5260===!![]?(_0xf57e3c[_0x33f6eb(0x48d)]='maintain-framerate',log(_0x33f6eb(0xb53))):(_0xf57e3c['degradationPreference']=_0x1f5260,log(_0x33f6eb(0x8a6)+_0x1f5260)),setEncodings(_0x3fa213,_0xf57e3c,(function(){var _0xdb1794=_0x33f6eb;log(_0xdb1794(0x210));}()));}catch(_0xc0eb51){errorlog(_0xc0eb51);}},_0xab9a9c[_0xf92ab0(0xb6d)]=function(_0xbb38a,_0x5e10fe,_0x5c60fc=![]){var _0x4a1200=_0xf92ab0;log(_0x4a1200(0xb5a)+_0xbb38a+_0x4a1200(0x52d)+_0x5c60fc);if(_0xab9a9c[_0x4a1200(0x53e)]===![])return;_0x5e10fe[_0x4a1200(0x53e)]=parseInt(_0xab9a9c['maxBandwidth']/0x64*_0xbb38a),_0x5c60fc?_0xab9a9c[_0x4a1200(0xaf9)](null):_0xab9a9c['limitBitrate'](_0x5e10fe[_0x4a1200(0xab9)],null);},_0xab9a9c[_0xf92ab0(0xc9b)]=function(_0x1baa97,_0x2cc49f=0x7d00,_0x26d10e=0x3e8){var _0x5636b0=_0xf92ab0;log(_0x5636b0(0x2b8));var _0x45cd47=getSenders2(_0x1baa97)[_0x5636b0(0x54f)](function(_0x1b5a0f){var _0x3982c3=_0x5636b0;return _0x1b5a0f[_0x3982c3(0x50d)]&&_0x1b5a0f[_0x3982c3(0x50d)][_0x3982c3(0xcd1)]==_0x3982c3(0x544);});if(!_0x45cd47)return log(_0x5636b0(0x90b)),![];var _0x304b33={};_0x304b33[_0x5636b0(0xb59)]=_0x2cc49f,setEncodings(_0x45cd47,_0x304b33,function(_0x611ec1){var _0x4f41d0=_0x5636b0;pokeIframeAPI(_0x4f41d0(0xa62),_0x611ec1[0x0],_0x611ec1[0x1]),pokeIframeAPI(_0x4f41d0(0x611),_0x611ec1[0x0],_0x611ec1[0x1]),_0x611ec1[0x2]>0x0&&setTimeout(function(){var _0x7d9129=_0x4f41d0;try{if(_0x611ec1[0x1]in _0xab9a9c[_0x7d9129(0x84f)])var _0x602174=getSenders2(_0x611ec1[0x1])['find'](function(_0x2a5e00){var _0xff5a4=_0x7d9129;return _0x2a5e00[_0xff5a4(0x50d)]&&_0x2a5e00['track'][_0xff5a4(0xcd1)]==_0xff5a4(0x544);});else return![];if(!_0x602174)return log(_0x7d9129(0x90b)),![];var _0x16deb1={};_0x16deb1[_0x7d9129(0xb59)]=null,setEncodings(_0x602174,_0x16deb1,function(){log('done\x20clearing\x20audio');});}catch(_0x24de5e){errorlog(_0x24de5e);}},_0x611ec1[0x2],_0x611ec1[0x1]);},[_0x2cc49f,_0x1baa97,_0x26d10e]);},_0xab9a9c[_0xf92ab0(0x920)]=function(_0xb0e5bc,_0x1f62d4,_0x414217){var _0x1125d5=_0xf92ab0;pokeIframeAPI('transfer',_0xb0e5bc,_0x414217);if(_0xab9a9c[_0x1125d5(0x54c)])return generateHash(_0xb0e5bc+_0xab9a9c[_0x1125d5(0x54c)]+_0xab9a9c[_0x1125d5(0x7a9)],0x10)[_0x1125d5(0x998)](function(_0x5787be){var _0x42f64f=_0x1125d5,_0x31cb3c={};_0x1f62d4[_0x42f64f(0x87a)]&&(_0x1f62d4[_0x42f64f(0xbb7)]=_0x5787be);if(_0xab9a9c[_0x42f64f(0x827)]&&_0xab9a9c[_0x42f64f(0xb38)])_0x31cb3c[_0x42f64f(0x74e)]=_0x414217,_0x31cb3c[_0x42f64f(0x70d)]=_0x5787be,_0x31cb3c[_0x42f64f(0xbdc)]=_0x1f62d4,_0xab9a9c[_0x42f64f(0x936)](_0x31cb3c,_0xab9a9c[_0x42f64f(0xb38)]),log(_0x31cb3c);else{if(_0x1f62d4[_0x42f64f(0x87a)])_0x31cb3c[_0x42f64f(0x7aa)]=_0x42f64f(0x74e),_0x31cb3c[_0x42f64f(0xbdc)]=_0x1f62d4,log(_0x31cb3c),_0xab9a9c['sendRequest'](_0x31cb3c,_0x414217,function(){var _0x254ce1=_0x42f64f,_0x3d54fe={};_0x3d54fe['request']=_0x254ce1(0x74e),_0x3d54fe[_0x254ce1(0x70d)]=_0x5787be,_0x3d54fe[_0x254ce1(0x357)]=_0x414217,_0xab9a9c[_0x254ce1(0x3ba)](_0x3d54fe);}),log(_0x31cb3c);else{if(_0x42f64f(0x4e2)in _0x1f62d4)_0x31cb3c[_0x42f64f(0x7aa)]='migrate',_0x31cb3c[_0x42f64f(0xbdc)]=_0x1f62d4,delete _0x31cb3c['transferSettings']['roomid'],delete _0x31cb3c[_0x42f64f(0xbdc)][_0x42f64f(0xbb7)],log(_0x31cb3c),_0xab9a9c['sendRequest'](_0x31cb3c,_0x414217,function(){var _0x130437=_0x42f64f,_0x32fb84={};_0x32fb84[_0x130437(0x7aa)]=_0x130437(0x74e),_0x32fb84[_0x130437(0x70d)]=_0x5787be,_0x32fb84[_0x130437(0x357)]=_0x414217,_0xab9a9c[_0x130437(0x3ba)](_0x32fb84);}),log(_0x31cb3c);else Object[_0x42f64f(0x7db)](_0x1f62d4)[_0x42f64f(0xade)]?(_0x31cb3c[_0x42f64f(0x7aa)]=_0x42f64f(0x74e),_0x31cb3c[_0x42f64f(0xbdc)]=_0x1f62d4,delete _0x31cb3c[_0x42f64f(0xbdc)][_0x42f64f(0x70d)],delete _0x31cb3c['transferSettings']['roomenc'],log(_0x31cb3c),_0xab9a9c[_0x42f64f(0x936)](_0x31cb3c,_0x414217,function(){var _0xcfd11e=_0x42f64f,_0x3d3e37={};_0x3d3e37[_0xcfd11e(0x7aa)]=_0xcfd11e(0x74e),_0x3d3e37['roomid']=_0x5787be,_0x3d3e37[_0xcfd11e(0x357)]=_0x414217,_0xab9a9c['sendMsg'](_0x3d3e37);}),log(_0x31cb3c)):(_0x31cb3c['request']=_0x42f64f(0x74e),_0x31cb3c[_0x42f64f(0x70d)]=_0x5787be,_0x31cb3c[_0x42f64f(0x357)]=_0x414217,_0xab9a9c[_0x42f64f(0x3ba)](_0x31cb3c));}}})[_0x1125d5(0xacc)](errorlog);else{_0x1f62d4[_0x1125d5(0x87a)]&&(_0x1f62d4[_0x1125d5(0xbb7)]=_0xb0e5bc);var _0x43edfb={};if(_0xab9a9c[_0x1125d5(0x827)]&&_0xab9a9c[_0x1125d5(0xb38)])_0x43edfb[_0x1125d5(0x74e)]=_0x414217,_0x43edfb['roomid']=_0xb0e5bc,_0x43edfb[_0x1125d5(0xbdc)]=_0x1f62d4,_0xab9a9c['sendRequest'](_0x43edfb,_0xab9a9c[_0x1125d5(0xb38)]),log(_0x43edfb);else{if(_0x1f62d4[_0x1125d5(0x87a)])_0x43edfb[_0x1125d5(0x7aa)]='migrate',_0x43edfb['transferSettings']=_0x1f62d4,_0xab9a9c[_0x1125d5(0x936)](_0x43edfb,_0x414217,function(){var _0x37073f=_0x1125d5,_0x23f8b0={};_0x23f8b0[_0x37073f(0x7aa)]=_0x37073f(0x74e),_0x23f8b0[_0x37073f(0x70d)]=_0xb0e5bc,_0x23f8b0[_0x37073f(0x357)]=_0x414217,_0xab9a9c[_0x37073f(0x3ba)](_0x23f8b0);});else{if(_0x1125d5(0x4e2)in _0x1f62d4)_0x43edfb[_0x1125d5(0x7aa)]=_0x1125d5(0x74e),_0x43edfb[_0x1125d5(0xbdc)]=_0x1f62d4,delete _0x43edfb[_0x1125d5(0xbdc)][_0x1125d5(0x70d)],delete _0x43edfb['transferSettings'][_0x1125d5(0xbb7)],_0xab9a9c['sendRequest'](_0x43edfb,_0x414217,function(){var _0x50a3ce=_0x1125d5,_0x287c39={};_0x287c39[_0x50a3ce(0x7aa)]=_0x50a3ce(0x74e),_0x287c39[_0x50a3ce(0x70d)]=_0xb0e5bc,_0x287c39[_0x50a3ce(0x357)]=_0x414217,_0xab9a9c[_0x50a3ce(0x3ba)](_0x287c39);});else Object[_0x1125d5(0x7db)](_0x1f62d4)[_0x1125d5(0xade)]?(_0x43edfb[_0x1125d5(0x7aa)]='migrate',_0x43edfb[_0x1125d5(0xbdc)]=_0x1f62d4,delete _0x43edfb[_0x1125d5(0xbdc)]['roomid'],delete _0x43edfb[_0x1125d5(0xbdc)][_0x1125d5(0xbb7)],log(_0x43edfb),_0xab9a9c[_0x1125d5(0x936)](_0x43edfb,_0x414217,function(){var _0x218e72=_0x1125d5,_0x4f3a41={};_0x4f3a41[_0x218e72(0x7aa)]=_0x218e72(0x74e),_0x4f3a41[_0x218e72(0x70d)]=_0xb0e5bc,_0x4f3a41[_0x218e72(0x357)]=_0x414217,_0xab9a9c['sendMsg'](_0x4f3a41);}),log(_0x43edfb)):(_0x43edfb['request']='migrate',_0x43edfb[_0x1125d5(0x70d)]=_0xb0e5bc,_0x43edfb['target']=_0x414217,_0xab9a9c[_0x1125d5(0x3ba)](_0x43edfb));}}}},_0xab9a9c[_0xf92ab0(0x368)]=async function(_0x4b224b,_0x15b627){var _0x388f22=_0xf92ab0;_0x15b627=parseInt(_0x15b627);try{var _0x4d9459=getSenders2(_0x4b224b)['find'](function(_0x30ab04){var _0x1162ff=_0x2b9f;return _0x30ab04[_0x1162ff(0x50d)]&&_0x30ab04[_0x1162ff(0x50d)][_0x1162ff(0xcd1)]==_0x1162ff(0x544);});if(!_0x4d9459){log(_0x388f22(0x7ab));return;}var _0xf8fdd={};if(_0x15b627<0x0){_0xf8fdd[_0x388f22(0xc34)]=!![];if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad)){_0x15b627=0x20;if(_0xab9a9c['pcs'][_0x4b224b][_0x388f22(0xa62)]!==![])_0x15b627=_0xab9a9c[_0x388f22(0x84f)][_0x4b224b][_0x388f22(0xa62)];else _0xab9a9c[_0x388f22(0xbee)]&&(_0x15b627=_0xab9a9c['audiobitrate']);_0xf8fdd[_0x388f22(0xb59)]=_0x15b627*0x400;}else _0xab9a9c[_0x388f22(0x84f)][_0x4b224b][_0x388f22(0xa62)]!==![]?(_0x15b627=_0xab9a9c['pcs'][_0x4b224b]['setAudioBitrate'],_0xf8fdd[_0x388f22(0xb59)]=_0x15b627*0x400):_0xf8fdd[_0x388f22(0xb59)]=null;}else _0x15b627===0x0?_0xf8fdd['active']=![]:(_0xf8fdd[_0x388f22(0xc34)]=!![],_0xf8fdd['maxBitrate']=_0x15b627*0x400);_0xab9a9c[_0x388f22(0x84f)][_0x4b224b][_0x388f22(0x2e4)]&&(_0xf8fdd[_0x388f22(0xc34)]=![]),setEncodings(_0x4d9459,_0xf8fdd,function(_0x27b969){var _0x268453=_0x388f22;pokeIframeAPI(_0x268453(0xa62),_0x27b969[0x0],_0x27b969[0x1]),pokeIframeAPI(_0x268453(0x611),_0x27b969[0x0],_0x27b969[0x1]),log('audio\x20bandwidth\x20set\x20f!');},[_0x15b627,_0x4b224b]);}catch(_0x200c12){errorlog(_0x200c12),log(_0x4b224b),log(_0xab9a9c[_0x388f22(0x84f)][_0x4b224b]);}},_0xab9a9c[_0xf92ab0(0x4da)]=function(_0x4d9686){var _0x4605f1=_0xf92ab0;if(_0xab9a9c['iframeSrc']&&_0xab9a9c['pcs'][_0x4d9686][_0x4605f1(0x9fd)]===!![])_0xab9a9c[_0x4605f1(0x523)](_0x4d9686,0x0),_0xab9a9c['pcs'][_0x4d9686]['optimizedBitrate']===0x0&&(_0xab9a9c['pcs'][_0x4d9686][_0x4605f1(0x444)][_0x4605f1(0x569)]===![]?_0xab9a9c[_0x4605f1(0x368)](_0x4d9686,0x0):_0xab9a9c[_0x4605f1(0x368)](_0x4d9686,-0x1));else{if(_0xab9a9c[_0x4605f1(0x84f)][_0x4d9686]&&_0xab9a9c[_0x4605f1(0x84f)][_0x4d9686][_0x4605f1(0xbfc)]!==![]){if(_0xab9a9c['pcs'][_0x4d9686]['obsState']['visibility']===![]){var _0x390d70=_0xab9a9c[_0x4605f1(0x84f)][_0x4d9686][_0x4605f1(0xbfc)];_0xab9a9c[_0x4605f1(0x84f)][_0x4d9686]['savedBitrate']&&_0xab9a9c[_0x4605f1(0x84f)][_0x4d9686][_0x4605f1(0x402)]>0x0&&(_0xab9a9c[_0x4605f1(0x84f)][_0x4d9686][_0x4605f1(0x402)]<_0xab9a9c[_0x4605f1(0x84f)][_0x4d9686][_0x4605f1(0xbfc)]&&(_0x390d70=_0xab9a9c[_0x4605f1(0x84f)][_0x4d9686]['savedBitrate'])),_0xab9a9c[_0x4605f1(0x523)](_0x4d9686,_0x390d70),_0xab9a9c['pcs'][_0x4d9686][_0x4605f1(0xbfc)]===0x0&&_0xab9a9c[_0x4605f1(0x368)](_0x4d9686,0x0);}else _0xab9a9c[_0x4605f1(0x84f)][_0x4d9686][_0x4605f1(0xbfc)]===0x0&&(_0xab9a9c['limitAudioBitrate'](_0x4d9686,-0x1),_0xab9a9c[_0x4605f1(0x559)](),_0xab9a9c[_0x4605f1(0x504)]&&_0xab9a9c[_0x4605f1(0x523)](_0x4d9686,null));}else _0xab9a9c[_0x4605f1(0x559)](),_0xab9a9c['maxvideobitrate']&&_0xab9a9c['limitBitrate'](_0x4d9686,null);}},_0xab9a9c['limitTotalBitrateGuests']=function(_0x2d5638=0x0,_0x31e5c9=![]){var _0x4fad0c=_0xf92ab0;if(!_0xab9a9c['limitTotalBitrate'])return _0x2d5638;if(!_0xab9a9c[_0x4fad0c(0x70d)]||_0xab9a9c[_0x4fad0c(0x268)]!==![])return log(_0x4fad0c(0x9e4)),_0xab9a9c['limitTotalBitrateAll'](_0x2d5638,_0x31e5c9),_0x2d5638;if((iOS||iPad)&&SafariVersion&&SafariVersion<=0xd)return _0x2d5638;var _0x16fd69=_0x2d5638;if(_0x31e5c9===![])_0x16fd69=0x0;else _0x16fd69<0x0&&(_0x16fd69=_0xab9a9c['pcs'][_0x31e5c9][_0x4fad0c(0x29e)]||Math[_0x4fad0c(0xb03)](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x53e)]||0x0))||0x9c4);var _0x33df56=0x0;for(var _0x21ced1 in _0xab9a9c[_0x4fad0c(0x84f)]){if(_0x31e5c9===_0x21ced1)continue;if(!_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x1ab)])continue;try{var _0x466c23=getSenders2(_0x21ced1)['find'](function(_0x916891){var _0x3e5411=_0x4fad0c;return _0x916891['track']&&_0x916891[_0x3e5411(0x50d)][_0x3e5411(0xcd1)]=='video';});if(!_0x466c23)continue;var _0x16cd40=_0x466c23['getParameters']();if(!_0x16cd40['encodings']||_0x16cd40[_0x4fad0c(0x342)][_0x4fad0c(0xade)]==0x0){_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x29e)]<0x0?_0x16fd69+=Math[_0x4fad0c(0xb03)](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x53e)]||0x0))||0x9c4:_0x16fd69+=_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x29e)]||Math[_0x4fad0c(0xb03)](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1]['maxBandwidth']||0x0))||0x9c4;warnlog(_0x16fd69),_0x33df56+=0x1;continue;}if(_0x16cd40['encodings'][0x0][_0x4fad0c(0xc34)]==![])continue;if(_0x16cd40[_0x4fad0c(0x342)][0x0][_0x4fad0c(0xb59)])'preLimitedBitrate'in _0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1]?_0x16fd69+=parseInt(_0xab9a9c['pcs'][_0x21ced1][_0x4fad0c(0x77f)]):_0x16fd69+=parseInt(_0x16cd40['encodings'][0x0][_0x4fad0c(0xb59)])/0x400;else _0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x29e)]<0x0?_0x16fd69+=Math[_0x4fad0c(0xb03)](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x53e)]||0x0))||0x9c4:(_0x16fd69+=_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x29e)]||Math[_0x4fad0c(0xb03)](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c['pcs'][_0x21ced1]['maxBandwidth']||0x0))||0x9c4,warnlog(_0x16fd69));_0x33df56+=0x1;}catch(_0x42aa8c){errorlog(_0x42aa8c);}}if(!_0x16fd69)return _0x16fd69;warnlog(_0x4fad0c(0x5c5)+_0x16fd69);var _0x3bdae7=parseFloat(_0x16fd69/_0xab9a9c[_0x4fad0c(0xbc3)]);_0x3bdae7<0x1&&(_0x3bdae7=0x1);for(var _0x21ced1 in _0xab9a9c[_0x4fad0c(0x84f)]){if(_0x31e5c9===_0x21ced1)continue;if(!_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x1ab)])continue;try{var _0x466c23=getSenders2(_0x21ced1)[_0x4fad0c(0x54f)](function(_0x32cdb1){var _0x2e6a5b=_0x4fad0c;return _0x32cdb1[_0x2e6a5b(0x50d)]&&_0x32cdb1['track'][_0x2e6a5b(0xcd1)]=='video';});if(!_0x466c23)continue;var _0x16cd40=_0x466c23['getParameters']();if(!_0x16cd40[_0x4fad0c(0x342)]||_0x16cd40['encodings'][_0x4fad0c(0xade)]==0x0){if(_0xab9a9c['pcs'][_0x21ced1]['setBitrate']<0x0)var _0x11d32a=Math['min'](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1]['maxBandwidth']||0x0))||0x9c4;else var _0x11d32a=_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x29e)]||Math['min'](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x53e)]||0x0))||0x9c4;var _0x23f27d=parseInt(_0x11d32a/_0x3bdae7);_0xab9a9c[_0x4fad0c(0x523)](_0x21ced1,_0x23f27d,!![]);continue;}if(_0x16cd40[_0x4fad0c(0x342)][0x0][_0x4fad0c(0xc34)]==![])continue;if(_0x16cd40[_0x4fad0c(0x342)][0x0][_0x4fad0c(0xb59)]){if('preLimitedBitrate'in _0xab9a9c['pcs'][_0x21ced1])var _0x11d32a=parseInt(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x77f)]);else var _0x11d32a=parseInt(parseInt(_0x16cd40[_0x4fad0c(0x342)][0x0][_0x4fad0c(0xb59)])/0x400);var _0x23f27d=parseInt(_0x11d32a/_0x3bdae7);_0xab9a9c['limitBitrate'](_0x21ced1,_0x23f27d,!![]);}else{if(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1]['setBitrate']<0x0)var _0x11d32a=Math['min'](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1]['maxBandwidth']||0x0))||0x9c4;else var _0x11d32a=_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1][_0x4fad0c(0x29e)]||Math['min'](_0xab9a9c[_0x4fad0c(0x8a7)]||0x0||(_0xab9a9c[_0x4fad0c(0x84f)][_0x21ced1]['maxBandwidth']||0x0))||0x9c4;var _0x23f27d=parseInt(_0x11d32a/_0x3bdae7);_0xab9a9c[_0x4fad0c(0x523)](_0x21ced1,_0x23f27d,!![]);}}catch(_0x264bb0){errorlog(_0x264bb0);}}return parseInt(_0x2d5638/_0x3bdae7);},_0xab9a9c[_0xf92ab0(0x561)]=function(_0xc0ba03=0x0,_0x3bf37a=![]){var _0x194c8e=_0xf92ab0;if(!_0xab9a9c[_0x194c8e(0xbc3)])return _0xc0ba03;if((iOS||iPad)&&SafariVersion&&SafariVersion<=0xd)return _0xc0ba03;var _0x3d3a4b=_0xc0ba03;if(_0x3bf37a===![])_0x3d3a4b=0x0;else _0x3d3a4b<0x0&&(_0x3d3a4b=_0xab9a9c[_0x194c8e(0x84f)][_0x3bf37a][_0x194c8e(0x29e)]||Math[_0x194c8e(0xb03)](_0xab9a9c['outboundVideoBitrate']||0x0||(_0xab9a9c['pcs'][_0x11df7b][_0x194c8e(0x53e)]||0x0))||0x9c4);var _0x3e06c2=0x0;for(var _0x11df7b in _0xab9a9c['pcs']){if(_0x3bf37a===_0x11df7b)continue;try{var _0x53d30f=getSenders2(_0x11df7b)[_0x194c8e(0x54f)](function(_0x251441){var _0x39c403=_0x194c8e;return _0x251441[_0x39c403(0x50d)]&&_0x251441[_0x39c403(0x50d)][_0x39c403(0xcd1)]=='video';});if(!_0x53d30f)continue;var _0x50156f=_0x53d30f[_0x194c8e(0x1a4)]();if(!_0x50156f[_0x194c8e(0x342)]||_0x50156f[_0x194c8e(0x342)][_0x194c8e(0xade)]==0x0){_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x29e)]<0x0?_0x3d3a4b+=Math['min'](_0xab9a9c[_0x194c8e(0x8a7)]||0x0||(_0xab9a9c['pcs'][_0x11df7b]['maxBandwidth']||0x0))||0x9c4:_0x3d3a4b+=_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x29e)]||Math[_0x194c8e(0xb03)](_0xab9a9c[_0x194c8e(0x8a7)]||0x0||(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x53e)]||0x0))||0x9c4;warnlog(_0x3d3a4b),_0x3e06c2+=0x1;continue;}if(_0x50156f[_0x194c8e(0x342)][0x0]['active']==![])continue;if(_0x50156f[_0x194c8e(0x342)][0x0]['maxBitrate'])_0x194c8e(0x77f)in _0xab9a9c[_0x194c8e(0x84f)][_0x11df7b]?_0x3d3a4b+=parseInt(_0xab9a9c['pcs'][_0x11df7b][_0x194c8e(0x77f)]):_0x3d3a4b+=parseInt(_0x50156f[_0x194c8e(0x342)][0x0][_0x194c8e(0xb59)])/0x400;else _0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x29e)]<0x0?_0x3d3a4b+=Math[_0x194c8e(0xb03)](_0xab9a9c[_0x194c8e(0x8a7)]||0x0||(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x53e)]||0x0))||0x9c4:(_0x3d3a4b+=_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x29e)]||Math[_0x194c8e(0xb03)](_0xab9a9c[_0x194c8e(0x8a7)]||0x0||(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b]['maxBandwidth']||0x0))||0x9c4,warnlog(_0x3d3a4b));_0x3e06c2+=0x1;}catch(_0x493d8f){errorlog(_0x493d8f);}}if(!_0x3d3a4b)return _0x3d3a4b;warnlog(_0x194c8e(0x5c5)+_0x3d3a4b);var _0x55e905=parseFloat(_0x3d3a4b/_0xab9a9c[_0x194c8e(0xbc3)]);_0x55e905<0x1&&(_0x55e905=0x1);for(var _0x11df7b in _0xab9a9c[_0x194c8e(0x84f)]){if(_0x3bf37a===_0x11df7b)continue;try{var _0x53d30f=getSenders2(_0x11df7b)[_0x194c8e(0x54f)](function(_0x4d408c){var _0x4f1097=_0x194c8e;return _0x4d408c[_0x4f1097(0x50d)]&&_0x4d408c[_0x4f1097(0x50d)][_0x4f1097(0xcd1)]==_0x4f1097(0xa55);});if(!_0x53d30f)continue;var _0x50156f=_0x53d30f[_0x194c8e(0x1a4)]();if(!_0x50156f[_0x194c8e(0x342)]||_0x50156f[_0x194c8e(0x342)][_0x194c8e(0xade)]==0x0){if(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x29e)]<0x0)var _0x4c3891=Math[_0x194c8e(0xb03)](_0xab9a9c[_0x194c8e(0x8a7)]||0x0||(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b]['maxBandwidth']||0x0))||0x9c4;else var _0x4c3891=_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b]['setBitrate']||Math[_0x194c8e(0xb03)](_0xab9a9c[_0x194c8e(0x8a7)]||0x0||(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x53e)]||0x0))||0x9c4;var _0x5583d9=parseInt(_0x4c3891/_0x55e905);_0xab9a9c[_0x194c8e(0x523)](_0x11df7b,_0x5583d9,!![]);continue;}if(_0x50156f['encodings'][0x0][_0x194c8e(0xc34)]==![])continue;if(_0x50156f[_0x194c8e(0x342)][0x0][_0x194c8e(0xb59)]){if('preLimitedBitrate'in _0xab9a9c[_0x194c8e(0x84f)][_0x11df7b])var _0x4c3891=parseInt(_0xab9a9c['pcs'][_0x11df7b][_0x194c8e(0x77f)]);else var _0x4c3891=parseInt(parseInt(_0x50156f[_0x194c8e(0x342)][0x0][_0x194c8e(0xb59)])/0x400);var _0x5583d9=parseInt(_0x4c3891/_0x55e905);_0xab9a9c[_0x194c8e(0x523)](_0x11df7b,_0x5583d9,!![]);}else{if(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x29e)]<0x0)var _0x4c3891=Math[_0x194c8e(0xb03)](_0xab9a9c[_0x194c8e(0x8a7)]||0x0||(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x53e)]||0x0))||0x9c4;else var _0x4c3891=_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x29e)]||Math[_0x194c8e(0xb03)](_0xab9a9c[_0x194c8e(0x8a7)]||0x0||(_0xab9a9c[_0x194c8e(0x84f)][_0x11df7b][_0x194c8e(0x53e)]||0x0))||0x9c4;var _0x5583d9=parseInt(_0x4c3891/_0x55e905);_0xab9a9c[_0x194c8e(0x523)](_0x11df7b,_0x5583d9,!![]);}}catch(_0x506596){errorlog(_0x506596);}}return parseInt(_0xc0ba03/_0x55e905);},_0xab9a9c[_0xf92ab0(0x47f)]=function(_0x3c1de5,_0x283926=![]){var _0x4db052=_0xf92ab0,_0x43bc11={};_0x43bc11['directorSettings']={},_0x43bc11[_0x4db052(0xc90)]['addCoDirector']=[_0x3c1de5],_0xab9a9c[_0x4db052(0xae3)](_0x43bc11,_0x283926),pokeIframeAPI(_0x4db052(0x75c),_0x3c1de5);},_0xab9a9c[_0xf92ab0(0xaf9)]=function(_0x16ca6c=null){var _0x5e507f=_0xf92ab0;if(!_0xab9a9c['whipOut'])return;_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0xaf0)]&&(clearInterval(_0xab9a9c[_0x5e507f(0x52b)]['bitrateTimeout']),_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0xaf0)]=null);if(_0x16ca6c===null){if(_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x402)]===![])return;_0x16ca6c=_0xab9a9c['whipOut'][_0x5e507f(0x402)];}_0x16ca6c=parseInt(_0x16ca6c);if(_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x29e)]&&_0x16ca6c>_0xab9a9c[_0x5e507f(0x52b)]['setBitrate'])_0x16ca6c=_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x29e)];else _0xab9a9c[_0x5e507f(0x52b)]['setBitrate']===![]&&(_0x16ca6c<0x0&&(_0xab9a9c[_0x5e507f(0x8a7)]?_0x16ca6c=_0xab9a9c[_0x5e507f(0x8a7)]:_0x16ca6c=0x9c4));_0xab9a9c['maxvideobitrate']&&(_0x16ca6c>_0xab9a9c[_0x5e507f(0x504)]&&(_0x16ca6c=_0xab9a9c[_0x5e507f(0x504)]));_0xab9a9c[_0x5e507f(0x52b)]['savedBitrate']=_0x16ca6c;_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0xbfc)]!==![]&&(_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x444)][_0x5e507f(0x569)]===![]&&(_0x16ca6c>_0xab9a9c['whipOut'][_0x5e507f(0xbfc)]&&(_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x402)]=_0x16ca6c,_0x16ca6c=parseInt(_0xab9a9c['whipOut']['optimizedBitrate'])||0x0)));if(_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x53e)]!==null){if(_0xab9a9c['whipOut']['maxBandwidth']<_0x16ca6c)_0x16ca6c=_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x53e)],_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x287)][_0x5e507f(0x6c6)]=_0x16ca6c,warnlog(_0x5e507f(0x86b)+_0x16ca6c+_0x5e507f(0x5cb));else _0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x287)]&&(_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x287)]['max_bandwidth_capped_kbps']=![]);}else _0x5e507f(0x6c6)in _0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x287)]&&(_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x287)][_0x5e507f(0x6c6)]=![]);if(_0x16ca6c===0x0){var _0x517b4e=Date[_0x5e507f(0x30e)]()-_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x334)];_0x517b4e<_0xab9a9c['rampUpTime']&&(_0x16ca6c=_0xab9a9c[_0x5e507f(0x332)],log(_0x5e507f(0x81a)+(Date[_0x5e507f(0x30e)]()-_0xab9a9c[_0x5e507f(0x52b)]['startTime'])),_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0xaf0)]=setTimeout(function(){var _0x556880=_0x5e507f;try{warnlog(_0x556880(0x1f6)+(Date[_0x556880(0x30e)]()-_0xab9a9c[_0x556880(0x52b)][_0x556880(0x334)])),_0xab9a9c[_0x556880(0xaf9)](null);}catch(_0x2f13d2){}},_0xab9a9c[_0x5e507f(0x2b5)]-_0x517b4e+0x5));}try{if((iOS||iPad)&&SafariVersion&&SafariVersion<=0xd){log('iOS\x20devices\x20do\x20not\x20support\x20dynamic\x20bitrates\x20correctly;\x20skipping');var _0x189fc6=_0xab9a9c['whipOut'][_0x5e507f(0x3f7)]()[_0x5e507f(0x54f)](function(_0xcca8f7){var _0x2cb565=_0x5e507f;return _0xcca8f7[_0x2cb565(0x50d)]&&_0xcca8f7[_0x2cb565(0x50d)][_0x2cb565(0xcd1)]==_0x2cb565(0xa55);});if(!_0x189fc6){warnlog('can\x27t\x20change\x20bitrate;\x20no\x20video\x20sender\x20found');return;}var _0x97b507={};if(_0x16ca6c<0x0)_0x97b507[_0x5e507f(0xc34)]=!![],_0x16ca6c=0x9c4,_0xab9a9c[_0x5e507f(0x42f)]&&(_0x16ca6c=_0xab9a9c['bitrate']),_0xab9a9c['maxvideobitrate']&&(_0x16ca6c>_0xab9a9c[_0x5e507f(0x504)]&&(_0x16ca6c=_0xab9a9c[_0x5e507f(0x504)])),_0x97b507[_0x5e507f(0xb59)]=_0x16ca6c*0x400;else _0x16ca6c===0x0?_0x97b507[_0x5e507f(0xc34)]=![]:(_0x97b507['active']=!![],_0x97b507[_0x5e507f(0xb59)]=_0x16ca6c*0x400);setEncodings(_0x189fc6,_0x97b507,function(_0x47c7d8){var _0x5314d0=_0x5e507f;pokeIframeAPI(_0x5314d0(0x88c),_0x47c7d8),log(_0x5314d0(0xa19)+_0x47c7d8);},_0x16ca6c);return;}else{if(_0x5e507f(0x534)in window&&_0x5e507f(0x2e7)in window['RTCRtpSender'][_0x5e507f(0x479)]){var _0x189fc6=_0xab9a9c[_0x5e507f(0x52b)]['getSenders']()[_0x5e507f(0x54f)](function(_0x213578){var _0x461654=_0x5e507f;return _0x213578[_0x461654(0x50d)]&&_0x213578[_0x461654(0x50d)][_0x461654(0xcd1)]==_0x461654(0xa55);});if(!_0x189fc6){log(_0x5e507f(0x46a));return;}var _0x97b507={};if(_0x16ca6c<0x0)_0x97b507[_0x5e507f(0xc34)]==![]&&(_0x97b507['active']=!![]),_0x97b507[_0x5e507f(0xb59)]=null;else _0x16ca6c===0x0?(_0x97b507[_0x5e507f(0xc34)]=![],Firefox&&(_0x97b507['maxBitrate']=0x1)):(_0x97b507[_0x5e507f(0xc34)]=!![],_0x97b507[_0x5e507f(0xb59)]=_0x16ca6c*0x400);iPad||iOS||Firefox?_0xab9a9c[_0x5e507f(0x52b)][_0x5e507f(0x7ef)]?(clearInterval(_0xab9a9c[_0x5e507f(0x52b)]['bitrateTimeoutFirefox']),_0xab9a9c['whipOut'][_0x5e507f(0x7ef)]=setTimeout(function(){var _0x332295=_0x5e507f;log(_0x332295(0x684)+_0x16ca6c),_0xab9a9c[_0x332295(0x52b)][_0x332295(0x7ef)]=![],_0xab9a9c[_0x332295(0xaf9)](null);},0x1f4)):(_0xab9a9c[_0x5e507f(0x52b)]['bitrateTimeoutFirefox']=setTimeout(function(){_0xab9a9c['whipOut']['bitrateTimeoutFirefox']=![];},0x1f4),setEncodings(_0x189fc6,_0x97b507,function(_0x147ca5){var _0x5f3d15=_0x5e507f;log(_0x5f3d15(0xad3)+_0x147ca5),pokeIframeAPI(_0x5f3d15(0x88c),_0x147ca5);},_0x16ca6c)):setEncodings(_0x189fc6,_0x97b507,function(_0x2b9d50){var _0x2fe7bf=_0x5e507f;log(_0x2fe7bf(0x62b)+_0x2b9d50),pokeIframeAPI(_0x2fe7bf(0x88c),_0x2b9d50);},_0x16ca6c);return;}else warnlog(_0x5e507f(0x1ec));}}catch(_0x49d27f){errorlog(_0x49d27f);}},_0xab9a9c[_0xf92ab0(0x757)]=function(_0x1a00b2,_0x40b1b4){var _0x54c6ca=_0xf92ab0;_0x40b1b4===![]?(_0xab9a9c[_0x54c6ca(0x84f)][_0x1a00b2]['setBitrate']=![],_0xab9a9c[_0x54c6ca(0x523)](_0x1a00b2,-0x1)):(_0x40b1b4=parseInt(_0x40b1b4)||-0x1,_0x40b1b4>=0x0&&(_0xab9a9c['pcs'][_0x1a00b2][_0x54c6ca(0x29e)]=_0x40b1b4,_0xab9a9c[_0x54c6ca(0x523)](_0x1a00b2,_0x40b1b4)));},_0xab9a9c[_0xf92ab0(0x989)]=function(_0xe1bc81,_0x1b203e){var _0x38417b=_0xf92ab0;_0x1b203e===![]?(_0xab9a9c['pcs'][_0xe1bc81][_0x38417b(0xa62)]=![],_0xab9a9c[_0x38417b(0x368)](_0xe1bc81,-0x1)):(_0x1b203e=parseInt(_0x1b203e)||-0x1,_0x1b203e>=0x0&&(_0xab9a9c[_0x38417b(0x84f)][_0xe1bc81][_0x38417b(0xa62)]=_0x1b203e,_0xab9a9c['limitAudioBitrate'](_0xe1bc81,_0x1b203e)));},_0xab9a9c[_0xf92ab0(0x523)]=function(_0x2000fd,_0x20412d=null,_0x1cabd8=![]){var _0x12ea2f=_0xf92ab0;log(_0x12ea2f(0x694)+_0x20412d);if(!(_0x2000fd in _0xab9a9c[_0x12ea2f(0x84f)]))return;_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0xaf0)]&&(clearInterval(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd]['bitrateTimeout']),_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd]['bitrateTimeout']=null);var _0x45737b=!![];if(_0x20412d===null){if(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x402)]===![]){if(_0xab9a9c['pcs'][_0x2000fd][_0x12ea2f(0x53e)]===null)return;else _0x20412d=_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x53e)],_0x45737b=![];}else _0x20412d=_0xab9a9c['pcs'][_0x2000fd]['savedBitrate'];}_0x20412d=parseInt(_0x20412d);if(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x29e)]&&_0x20412d>_0xab9a9c['pcs'][_0x2000fd][_0x12ea2f(0x29e)])_0x20412d=_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x29e)];else _0x20412d<0x0&&(_0x20412d=_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd]['setBitrate']||_0xab9a9c['outboundVideoBitrate']||0x9c4);let _0x4af739=_0xab9a9c[_0x12ea2f(0x504)];_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x1ab)]==!![]&&(_0x4af739!==![]?_0xab9a9c[_0x12ea2f(0x9c3)]!==![]&&(_0xab9a9c[_0x12ea2f(0x9c3)]<_0x4af739&&(_0x4af739=_0xab9a9c['roombitrate'])):_0x4af739=_0xab9a9c[_0x12ea2f(0x9c3)]);_0x4af739&&(_0x20412d>_0x4af739&&(_0x20412d=_0x4af739));_0x45737b&&!_0x1cabd8&&(log('save\x20bandwidth:\x20'+_0x20412d),_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x402)]=_0x20412d);_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0xbfc)]!==![]&&(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x444)][_0x12ea2f(0x569)]===![]&&(_0x20412d>_0xab9a9c['pcs'][_0x2000fd][_0x12ea2f(0xbfc)]&&(_0x45737b&&(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x402)]=_0x20412d),_0x20412d=parseInt(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0xbfc)])||0x0)));if(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x53e)]!==null){if(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x53e)]<_0x20412d)_0x20412d=_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x53e)],_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x287)][_0x12ea2f(0x6c6)]=_0x20412d,warnlog('Max\x20bandwidth\x20being\x20capped:\x20'+_0x20412d+'-kbps');else _0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x53e)]===_0x20412d&&!_0x45737b?(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x287)][_0x12ea2f(0x6c6)]=_0x20412d,warnlog(_0x12ea2f(0xa2b)+_0x20412d+'-kbps')):(warnlog(_0x12ea2f(0x659)+_0x20412d+'-kbps'),_0xab9a9c['pcs'][_0x2000fd][_0x12ea2f(0x287)]['max_bandwidth_capped_kbps']=![]);}else _0x12ea2f(0x6c6)in _0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd]['stats']&&(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x287)]['max_bandwidth_capped_kbps']=![]);_0x1cabd8===![]&&(_0xab9a9c[_0x12ea2f(0xbc3)]&&(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x77f)]=_0x20412d,_0x20412d=_0xab9a9c[_0x12ea2f(0x559)](_0x20412d,_0x2000fd)));if(_0x20412d===0x0){var _0x4d6127=Date[_0x12ea2f(0x30e)]()-_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x334)];_0x4d6127<_0xab9a9c[_0x12ea2f(0x2b5)]&&(_0x20412d=_0xab9a9c[_0x12ea2f(0x332)],log(_0x12ea2f(0x81a)+(Date[_0x12ea2f(0x30e)]()-_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x334)])),_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0xaf0)]=setTimeout(function(_0x368ccd){var _0x401374=_0x12ea2f;try{warnlog(_0x401374(0x1f6)+(Date[_0x401374(0x30e)]()-_0xab9a9c[_0x401374(0x84f)][_0x368ccd][_0x401374(0x334)])),_0xab9a9c[_0x401374(0x523)](_0x368ccd,null);}catch(_0x56fe8b){}},_0xab9a9c[_0x12ea2f(0x2b5)]-_0x4d6127+0x5,_0x2000fd));}try{if((iOS||iPad)&&SafariVersion&&SafariVersion<=0xd){log(_0x12ea2f(0x1a3));if(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x1ab)]==!![]&&_0xab9a9c['pcs'][_0x2000fd][_0x12ea2f(0xcb7)]==![])return;var _0x3edbca=getSenders2(_0x2000fd)['find'](function(_0x512f63){var _0x4a33a3=_0x12ea2f;return _0x512f63['track']&&_0x512f63['track']['kind']==_0x4a33a3(0xa55);});if(!_0x3edbca){log(_0x12ea2f(0x46a));return;}var _0x4743a6={};_0x20412d===0x0?_0x4743a6['active']=![]:(_0x4743a6[_0x12ea2f(0xc34)]=!![],_0x4743a6[_0x12ea2f(0xb59)]=_0x20412d*0x400);setEncodings(_0x3edbca,_0x4743a6,function(_0x5c7a89){var _0x33676f=_0x12ea2f;pokeIframeAPI('setVideoBitrate',_0x5c7a89[0x0],_0x5c7a89[0x1]),pokeIframeAPI(_0x33676f(0xb62),_0x5c7a89[0x0],_0x5c7a89[0x1]),log('bandwidth\x20set\x20a!\x20'+_0x5c7a89[0x0]);},[_0x20412d,_0x2000fd]);return;}else{if(_0x12ea2f(0x534)in window&&_0x12ea2f(0x2e7)in window[_0x12ea2f(0x534)][_0x12ea2f(0x479)]){var _0x3edbca=getSenders2(_0x2000fd)[_0x12ea2f(0x54f)](function(_0x3d83fb){var _0x5b46dd=_0x12ea2f;return _0x3d83fb[_0x5b46dd(0x50d)]&&_0x3d83fb['track'][_0x5b46dd(0xcd1)]=='video';});if(!_0x3edbca){log(_0x12ea2f(0x46a));return;}var _0x4743a6={};_0x20412d===0x0?(_0x4743a6[_0x12ea2f(0xc34)]=![],Firefox&&(_0x4743a6[_0x12ea2f(0xb59)]=0x1,_0x4743a6['scaleResolutionDownBy']=0x3e8)):(_0x4743a6[_0x12ea2f(0xc34)]=!![],_0x4743a6[_0x12ea2f(0xb59)]=_0x20412d*0x400);if(_0x20412d!==0x0){var _0x442674=_0xab9a9c['calculateScale'](_0x2000fd,_0x20412d);if(_0x442674<=0x0||_0x442674==0x64){var _0x203785=getChromiumVersion();_0x203785>0x50?_0x4743a6[_0x12ea2f(0x267)]=null:_0x4743a6['scaleResolutionDownBy']=0x1;}else _0x4743a6[_0x12ea2f(0x267)]=0x64/_0x442674;iPad||iOS||Firefox?_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd]['bitrateTimeoutFirefox']?(clearInterval(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x7ef)]),_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x7ef)]=setTimeout(function(_0x57b8be,_0x234a08){var _0xd69d43=_0x12ea2f;log(_0xd69d43(0x684)+_0x20412d),_0xab9a9c['pcs'][_0x57b8be][_0xd69d43(0x7ef)]=![],_0xab9a9c[_0xd69d43(0x523)](_0x57b8be,null,_0x234a08);},0x1f4,_0x2000fd,_0x1cabd8)):(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x7ef)]=setTimeout(function(_0x1539ae){var _0x150ab6=_0x12ea2f;_0xab9a9c[_0x150ab6(0x84f)][_0x1539ae][_0x150ab6(0x7ef)]=![];},0x1f4,_0x2000fd),setEncodings(_0x3edbca,_0x4743a6,function(_0x23775){var _0xdc4c91=_0x12ea2f;log(_0xdc4c91(0x96f)+_0x23775[0x0]),_0xab9a9c[_0xdc4c91(0x84f)][_0x23775[0x1]][_0xdc4c91(0x287)]['scaleFactor']=parseInt(_0x23775[0x2])+'%',pokeIframeAPI(_0xdc4c91(0x528),_0x23775[0x0],_0x23775[0x1]),pokeIframeAPI(_0xdc4c91(0xb36),_0x23775[0x2],_0x23775[0x1]),pokeIframeAPI(_0xdc4c91(0xb62),_0x23775[0x0],_0x23775[0x1]),pokeIframeAPI(_0xdc4c91(0x3bf),_0x23775[0x2],_0x23775[0x1]);},[_0x20412d,_0x2000fd,_0x442674])):(warnlog(_0x4743a6),setEncodings(_0x3edbca,_0x4743a6,function(_0x545dbd){var _0x1d0509=_0x12ea2f;log(_0x1d0509(0x89c)+_0x545dbd[0x0]),_0xab9a9c[_0x1d0509(0x84f)][_0x545dbd[0x1]][_0x1d0509(0x287)]['scaleFactor']=parseInt(_0x545dbd[0x2])+'%',pokeIframeAPI(_0x1d0509(0x528),_0x545dbd[0x0],_0x545dbd[0x1]),pokeIframeAPI(_0x1d0509(0xb36),_0x545dbd[0x2],_0x545dbd[0x1]),pokeIframeAPI(_0x1d0509(0xb62),_0x545dbd[0x0],_0x545dbd[0x1]),pokeIframeAPI(_0x1d0509(0x3bf),_0x545dbd[0x2],_0x545dbd[0x1]);},[_0x20412d,_0x2000fd,_0x442674]));}else iPad||iOS||Firefox?_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x7ef)]?(clearInterval(_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x7ef)]),_0xab9a9c[_0x12ea2f(0x84f)][_0x2000fd][_0x12ea2f(0x7ef)]=setTimeout(function(_0x5d33da,_0x6b710){var _0x6a531d=_0x12ea2f;log(_0x6a531d(0x684)+_0x20412d),_0xab9a9c[_0x6a531d(0x84f)][_0x5d33da]['bitrateTimeoutFirefox']=![],_0xab9a9c[_0x6a531d(0x523)](_0x5d33da,null,_0x6b710);},0x1f4,_0x2000fd,_0x1cabd8)):(_0xab9a9c['pcs'][_0x2000fd][_0x12ea2f(0x7ef)]=setTimeout(function(_0x593247){var _0x1c3490=_0x12ea2f;_0xab9a9c[_0x1c3490(0x84f)][_0x593247][_0x1c3490(0x7ef)]=![];},0x1f4,_0x2000fd),setEncodings(_0x3edbca,_0x4743a6,function(_0x17ef9f){var _0x402252=_0x12ea2f;log(_0x402252(0x7cf)+_0x17ef9f[0x0]),pokeIframeAPI(_0x402252(0x528),_0x17ef9f[0x0],_0x17ef9f[0x1]),pokeIframeAPI(_0x402252(0xb62),_0x17ef9f[0x0],_0x17ef9f[0x1]);},[_0x20412d,_0x2000fd])):setEncodings(_0x3edbca,_0x4743a6,function(_0x597380){var _0x1d0d28=_0x12ea2f;log('bandwidth\x20set\x20e!\x20'+_0x597380[0x0]),pokeIframeAPI(_0x1d0d28(0x528),_0x597380[0x0],_0x597380[0x1]),pokeIframeAPI(_0x1d0d28(0xb62),_0x597380[0x0],_0x597380[0x1]);},[_0x20412d,_0x2000fd]);}else warnlog(_0x12ea2f(0x1ec));}}catch(_0x582fa2){errorlog(_0x582fa2);}};function _0x3ff164(_0x4bd8ea,_0x259d86,_0x496b83){var _0x173d0e=_0xf92ab0;if(_0xab9a9c['noScaling'])return _0x259d86;warnlog(_0x173d0e(0x2e6)+_0x259d86+_0x173d0e(0x2aa)+_0x496b83);if(_0x496b83<0x0)_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64;else{if(_0x496b83>=0x259)_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea]['scaleDueToBitrate']=0x64;else{if('realUUID'in _0xab9a9c['pcs'][_0x4bd8ea])_0xab9a9c['pcs'][_0x4bd8ea]['scaleDueToBitrate']=0x64;else{if(_0xab9a9c['screenShareState'])_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64;else{var _0x290141=getNativeOutputResolution();if(_0x290141)try{_0x290141=_0x290141[_0x173d0e(0x28e)]*_0x290141['height'],_0x290141=Math['pow'](_0x290141,0.5);}catch(_0x20e0fe){_0x290141=![];}warnlog(_0x173d0e(0xa16)+_0x290141);if(_0x496b83>=0x15e){if(_0x290141&&_0x290141<=0x1e0)_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64;else{if(_0xab9a9c[_0x173d0e(0x9ea)]){if(_0x290141&&_0x290141>=0x5a0)_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea]['scaleDueToBitrate']=0x64/0x3;else _0xab9a9c[_0x173d0e(0x494)]?_0x290141&&_0x290141>=0x3c0?_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea]['scaleDueToBitrate']=0x64/0x2:_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64:_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x2;}else{if(_0x290141&&_0x290141>=0x5a0)_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea]['scaleDueToBitrate']=0x64/2.5;else _0x290141&&_0x290141>=0x3c0?_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x2:_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64;}}}else{if(_0x496b83>=0xc9){if(_0x290141&&_0x290141<0x1e0)_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64;else{if(_0xab9a9c[_0x173d0e(0x9ea)]){if(_0x290141&&_0x290141>=0x5a0)_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x4;else _0xab9a9c['flagship']?_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x2:_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/2.5;}else _0x290141&&_0x290141>=0x5a0?_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x3:_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x2;}}else{if(_0x290141&&_0x290141<=0xf0)_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64;else{if(_0x496b83>=0x51){if(_0xab9a9c[_0x173d0e(0x9ea)]){if(_0x290141&&_0x290141>=0x5a0)_0xab9a9c['pcs'][_0x4bd8ea]['scaleDueToBitrate']=0x64/0x6;else _0xab9a9c['flagship']?_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x3:_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x4;}else _0x290141&&_0x290141>=0x5a0?_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x4:_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x3;}else{if(_0xab9a9c['mobile']){if(_0x290141&&_0x290141>=0x3c0)_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x6;else _0xab9a9c['flagship']?_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x4:_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x5;}else _0x290141&&_0x290141>=0x5a0?_0xab9a9c['pcs'][_0x4bd8ea][_0x173d0e(0x8d1)]=0x64/0x5:_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea]['scaleDueToBitrate']=0x64/0x4;}}}}}}}}return _0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]<_0x259d86&&(_0x259d86=_0xab9a9c[_0x173d0e(0x84f)][_0x4bd8ea][_0x173d0e(0x8d1)]),_0x259d86;}function _0x2fb2bd(_0xe6d52f,_0x32ec41=0x2710){var _0x4ea622=_0xf92ab0;_0x32ec41=parseInt(_0x32ec41);if(_0xab9a9c[_0x4ea622(0xbee)])_0x32ec41+=_0xab9a9c['audiobitrate'];else{if(_0xab9a9c[_0x4ea622(0x827)]&&_0xab9a9c[_0x4ea622(0x839)]==0x5)_0x32ec41+=0x20;else _0xab9a9c[_0x4ea622(0x839)]&&_0xab9a9c[_0x4ea622(0x839)]!=0x3?_0xab9a9c[_0x4ea622(0xa25)]&&_0xab9a9c[_0x4ea622(0xa25)]==_0x4ea622(0xa0c)?_0x32ec41+=_0xab9a9c['audiobitratePRO']*0x2:_0x32ec41+=_0xab9a9c[_0x4ea622(0x4f6)]:_0x32ec41+=0x20;}return log('actual\x20bitrate:'+_0x32ec41),_0x32ec41<0x1&&(_0x32ec41=0x1),_0xe6d52f=CodecsHandler[_0x4ea622(0x27c)](_0xe6d52f,{'min':parseInt(_0x32ec41/0xa)||0x1,'max':_0x32ec41||0x1},_0xab9a9c['codec']),_0xe6d52f;}_0xab9a9c[_0xf92ab0(0x4d9)]=function(_0x22eeb6,_0x3bb497){var _0x227fb6=_0xf92ab0;log(_0x22eeb6),!_0xab9a9c[_0x227fb6(0x767)][_0x227fb6(0x4cc)]&&warnlog('Generate\x20Some\x20Crypto\x20keys\x20first'),window[_0x227fb6(0x2a7)]['subtle'][_0x227fb6(0xb07)]({'name':_0x227fb6(0x45b)},_0xab9a9c[_0x227fb6(0x767)][_0x227fb6(0x4cc)],_0xab9a9c[_0x227fb6(0x89f)]['encode'](_0x22eeb6))['then'](function(_0x3a6c47){var _0x3ae86f=_0x227fb6;_0x3a6c47=new Uint8Array(_0x3a6c47),_0x3a6c47=_0x3a6c47[_0x3ae86f(0x24b)]((_0xb05e5c,_0xcc2ad0)=>_0xb05e5c+_0xcc2ad0['toString'](0x10)['padStart'](0x2,'0'),''),_0x3bb497(_0x22eeb6,_0x3a6c47),log(JSON[_0x3ae86f(0x6d1)](_0x3a6c47));})[_0x227fb6(0xacc)](errorlog);},_0xab9a9c['verifyData']=function(_0x231044,_0x5328d2){var _0x39c6b4=_0xf92ab0;_0x231044['signature']=new Uint8Array(_0x231044[_0x39c6b4(0x7c2)][_0x39c6b4(0x629)](/.{1,2}/g)['map'](_0x106959=>parseInt(_0x106959,0x10)));if(_0xab9a9c[_0x39c6b4(0x7db)][_0x5328d2][_0x39c6b4(0x2ac)])return window[_0x39c6b4(0x2a7)][_0x39c6b4(0x4a1)][_0x39c6b4(0x735)]({'name':_0x39c6b4(0x45b)},_0xab9a9c[_0x39c6b4(0x7db)][_0x5328d2][_0x39c6b4(0x2ac)],_0x231044[_0x39c6b4(0x7c2)],_0xab9a9c['enc'][_0x39c6b4(0x736)](_0x231044['data']))['then'](function(_0x44bd39){return _0x44bd39;})['catch'](function(_0x3083ef){return errorlog(_0x3083ef),![];});},_0xab9a9c['desaltStreamID']=function(_0x70657e){var _0x2c0011=_0xf92ab0;if(_0xab9a9c['password'])return _0xab9a9c['hash']!==![]?(_0x70657e=_0x70657e[_0x2c0011(0x912)](0x0,-0x1*_0xab9a9c[_0x2c0011(0xbd5)][_0x2c0011(0xade)]),pokeIframeAPI(_0x2c0011(0xa2c),_0x70657e),_0x70657e):generateHash(_0xab9a9c['password']+_0xab9a9c['salt'],0x6)[_0x2c0011(0x998)](function(_0x5607c0){var _0xf5536d=_0x2c0011;return _0xab9a9c[_0xf5536d(0xbd5)]=_0x5607c0,_0x70657e=_0x70657e['slice'](0x0,-0x1*_0xab9a9c['hash'][_0xf5536d(0xade)]),pokeIframeAPI('stream-id-detected',_0x70657e),_0x70657e;})['catch'](errorlog);return pokeIframeAPI(_0x2c0011(0xa2c),_0x70657e),_0x70657e;},_0xab9a9c[_0xf92ab0(0xb5e)]=function(){var _0x2dff48=_0xf92ab0;if(_0xab9a9c[_0x2dff48(0x98e)])return;clearTimeout(_0xab9a9c[_0x2dff48(0x66a)]);if(!_0xab9a9c['ws']||_0xab9a9c['ws'][_0x2dff48(0x70b)]!==0x1)return;_0xab9a9c[_0x2dff48(0x66a)]=setTimeout(function(){var _0x3bc745=_0x2dff48;log(_0x3bc745(0x390));var _0x142e81={};_0x142e81['request']=_0x3bc745(0xb5e),_0xab9a9c[_0x3bc745(0x3ba)](_0x142e81);},0xbb8);},_0xab9a9c[_0xf92ab0(0x932)]=async function(_0x43fb36){var _0x21dd71=_0xf92ab0;await _0xab9a9c[_0x21dd71(0x3ea)]();if(_0xab9a9c[_0x21dd71(0x3db)]){log(_0x21dd71(0x489));return;}if(_0x43fb36[_0x21dd71(0xade)]>0x0){if(_0x43fb36===_0xab9a9c[_0x21dd71(0xa96)]){warnlog('Can\x27t\x20play\x20your\x20own\x20stream\x20ID');return;}var _0x1bc78c={};_0x1bc78c[_0x21dd71(0x7aa)]=_0x21dd71(0x67d),_0x1bc78c['streamID']=_0x43fb36,_0xab9a9c['sendMsg'](_0x1bc78c),_0xab9a9c[_0x21dd71(0x2f4)][_0x43fb36]=!![],pokeIframeAPI(_0x21dd71(0xa3a),_0x43fb36);}else log(_0x21dd71(0x680));},_0xab9a9c[_0xf92ab0(0x2c6)]=async function _0x212d64(_0x307d26){var _0x22fcad=_0xf92ab0;_0xab9a9c[_0x22fcad(0x50a)]===![]&&(_0xab9a9c['joiningRoom']=!![]);if(_0xab9a9c[_0x22fcad(0x27a)]&&window[_0x22fcad(0x588)]){const _0x133caa=await window['vdoAuth'][_0x22fcad(0x2c6)](_0x307d26);if(!_0x133caa){_0xab9a9c['joiningRoom']=![];return;}}await _0xab9a9c[_0x22fcad(0x3ea)]();var _0x23b13f={};_0x23b13f[_0x22fcad(0x7aa)]=_0x22fcad(0x8ba);_0xab9a9c[_0x22fcad(0x827)]&&!_0xab9a9c[_0x22fcad(0x973)]&&(_0x23b13f[_0x22fcad(0x1aa)]=!![]);_0xab9a9c[_0x22fcad(0x98e)]&&_0xab9a9c[_0x22fcad(0x268)]===![]&&(_0x23b13f[_0x22fcad(0xa96)]=_0xab9a9c[_0x22fcad(0xa96)]);var _0x283c9c='';_0xab9a9c[_0x22fcad(0xac0)]&&(_0x283c9c=_0xab9a9c['token']);if(_0xab9a9c[_0x22fcad(0x54c)]){if(_0xab9a9c['hash']){if(!_0xab9a9c[_0x22fcad(0x827)]&&!_0xab9a9c[_0x22fcad(0x268)])try{var _0x810d8c='vdo_block_'+sanitizeRoomName(_0x307d26)+'_'+_0xab9a9c[_0x22fcad(0xbd5)];if(getStorage(_0x810d8c))return _0xab9a9c[_0x22fcad(0x3db)]=!![],log(_0x22fcad(0x9d6)),_0xab9a9c[_0x22fcad(0x50a)]=![],Promise[_0x22fcad(0x30f)]([]);}catch(_0x20f44c){}return generateHash(_0x307d26+_0xab9a9c[_0x22fcad(0x54c)]+_0xab9a9c[_0x22fcad(0x7a9)]+_0x283c9c,0x10)['then'](function(_0x24fa6d){var _0x4f6c29=_0x22fcad;return _0xab9a9c['customWSS']&&(_0xab9a9c[_0x4f6c29(0xbb7)]=_0x24fa6d),_0x23b13f['roomid']=_0x24fa6d,_0xab9a9c[_0x4f6c29(0x3ba)](_0x23b13f),_0xab9a9c[_0x4f6c29(0xc2a)]=_0x1c8003(),log('deferring\x20with\x20a\x20promise;\x20hashed\x20room'),pokeIframeAPI('joining-room',_0x307d26),_0xab9a9c[_0x4f6c29(0xc2a)];})[_0x22fcad(0xacc)](errorlog);}else return generateHash(_0xab9a9c[_0x22fcad(0x54c)]+_0xab9a9c[_0x22fcad(0x7a9)],0x6)[_0x22fcad(0x998)](function(_0x4e2c80){var _0x9444e1=_0x22fcad;return _0xab9a9c['hash']=_0x4e2c80,log(_0x9444e1(0x65d)+_0x4e2c80),log(_0x9444e1(0x403)),_0xab9a9c[_0x9444e1(0x2c6)](_0x307d26);})['catch'](errorlog);}else{if(!_0xab9a9c['director']&&!_0xab9a9c[_0x22fcad(0x268)])try{var _0x810d8c=_0x22fcad(0x8f6)+sanitizeRoomName(_0x307d26)+'_';if(getStorage(_0x810d8c))return _0xab9a9c['shadowBanned']=!![],log(_0x22fcad(0x9d6)),_0xab9a9c[_0x22fcad(0x50a)]=![],Promise[_0x22fcad(0x30f)]([]);}catch(_0x36bf69){}return _0xab9a9c[_0x22fcad(0x98e)]&&(_0xab9a9c[_0x22fcad(0xbb7)]=_0x307d26),_0x23b13f[_0x22fcad(0x70d)]=_0x307d26,_0xab9a9c[_0x22fcad(0x3ba)](_0x23b13f),_0xab9a9c[_0x22fcad(0xc2a)]=_0x1c8003(),log('deferring\x20with\x20a\x20promise'),pokeIframeAPI(_0x22fcad(0x2d6),_0x307d26),_0xab9a9c[_0x22fcad(0xc2a)];}},_0xab9a9c[_0xf92ab0(0x3ba)]=function(_0x29a1a,_0x289611=![]){var _0x3f5e1a=_0xf92ab0;_0x289611&&(_0x29a1a['UUID']=_0x289611);if(_0xab9a9c[_0x3f5e1a(0x98e)]){_0xab9a9c['UUID']?_0x29a1a[_0x3f5e1a(0x208)]=_0xab9a9c[_0x3f5e1a(0xab9)]:(_0xab9a9c[_0x3f5e1a(0xab9)]=_0xab9a9c[_0x3f5e1a(0x54d)](0x14),_0x29a1a['from']=_0xab9a9c[_0x3f5e1a(0xab9)]);if(_0x29a1a[_0x3f5e1a(0xab9)]&&_0x29a1a[_0x3f5e1a(0x208)]===_0x29a1a['UUID'])return;!(_0x3f5e1a(0x70d)in _0x29a1a)&&(_0xab9a9c[_0x3f5e1a(0xbb7)]&&(_0x29a1a[_0x3f5e1a(0x70d)]=_0xab9a9c[_0x3f5e1a(0xbb7)]));}clearTimeout(_0xab9a9c[_0x3f5e1a(0x66a)]);try{if(_0xab9a9c[_0x3f5e1a(0x54c)]){if(_0x29a1a[_0x3f5e1a(0xa96)]){if(_0xab9a9c[_0x3f5e1a(0xbd5)]!==![]){if(!_0xab9a9c['ws']||typeof _0xab9a9c['ws']!==_0x3f5e1a(0x537)||_0xab9a9c['ws'][_0x3f5e1a(0x70b)]!==0x1)log(_0x29a1a,_0x3f5e1a(0x67a)),_0xab9a9c[_0x3f5e1a(0x2f0)][_0x3f5e1a(0x5e7)](_0x29a1a);else{_0x29a1a[_0x3f5e1a(0xa96)]=_0x29a1a['streamID'][_0x3f5e1a(0x7a4)](0x0,0x40)+_0xab9a9c[_0x3f5e1a(0xbd5)]['substring'](0x0,0x6);var _0x30614=JSON[_0x3f5e1a(0x6d1)](_0x29a1a);if((_0x29a1a['description']||_0x29a1a[_0x3f5e1a(0x460)])&&_0x30614[_0x3f5e1a(0xade)]<0x88b8){}else{if(_0x30614[_0x3f5e1a(0xade)]>0x2710){errorlog(_0x3f5e1a(0x9b9)),errorlog(_0x29a1a),errorlog(_0x30614[_0x3f5e1a(0xade)]);return;}}_0xab9a9c['ws']['send'](_0x30614);}}else return generateHash(_0xab9a9c[_0x3f5e1a(0x54c)]+_0xab9a9c['salt'],0x6)['then'](function(_0x229153){var _0x49ce12=_0x3f5e1a;_0xab9a9c[_0x49ce12(0xbd5)]=_0x229153;if(typeof _0xab9a9c['ws']!==_0x49ce12(0x537)||_0xab9a9c['ws']['readyState']!==0x1)log(_0x29a1a,_0x49ce12(0x67a)),_0xab9a9c[_0x49ce12(0x2f0)][_0x49ce12(0x5e7)](_0x29a1a);else{_0x29a1a[_0x49ce12(0xa96)]=_0x29a1a[_0x49ce12(0xa96)][_0x49ce12(0x7a4)](0x0,0x40)+_0xab9a9c[_0x49ce12(0xbd5)][_0x49ce12(0x7a4)](0x0,0x6);var _0x4335a4=JSON[_0x49ce12(0x6d1)](_0x29a1a);if((_0x29a1a['description']||_0x29a1a[_0x49ce12(0x460)])&&_0x4335a4[_0x49ce12(0xade)]<0x88b8){}else{if(_0x4335a4[_0x49ce12(0xade)]>0x2710){errorlog(_0x49ce12(0x9b9));return;}}_0xab9a9c['ws'][_0x49ce12(0x4c6)](_0x4335a4);}})[_0x3f5e1a(0xacc)](errorlog);}else{if(!_0xab9a9c['ws']||typeof _0xab9a9c['ws']!==_0x3f5e1a(0x537)||_0xab9a9c['ws']['readyState']!==0x1)log(_0x29a1a,_0x3f5e1a(0x67a)),_0xab9a9c['msg'][_0x3f5e1a(0x5e7)](_0x29a1a);else{var _0x30614=JSON[_0x3f5e1a(0x6d1)](_0x29a1a);if((_0x29a1a['description']||_0x29a1a[_0x3f5e1a(0x460)])&&_0x30614[_0x3f5e1a(0xade)]<0x88b8){}else{if(_0x30614[_0x3f5e1a(0xade)]>0x2710){errorlog(_0x3f5e1a(0x9b9));return;}}_0xab9a9c['ws'][_0x3f5e1a(0x4c6)](_0x30614);}}}else{if(typeof _0xab9a9c['ws']!==_0x3f5e1a(0x537)||_0xab9a9c['ws'][_0x3f5e1a(0x70b)]!==0x1)warnlog(_0x3f5e1a(0xb5d)),_0xab9a9c[_0x3f5e1a(0x2f0)][_0x3f5e1a(0x5e7)](_0x29a1a);else{var _0x30614=JSON[_0x3f5e1a(0x6d1)](_0x29a1a);if(_0x30614[_0x3f5e1a(0xade)]>0x61a8){errorlog(_0x3f5e1a(0x9b9));return;}_0xab9a9c['ws'][_0x3f5e1a(0x4c6)](_0x30614);}}}catch(_0x3d9554){errorlog(_0x3d9554);}},_0xab9a9c[_0xf92ab0(0xae3)]=function(_0x2d3062,_0x143f2e=![],_0x58239b=![]){var _0x4c91e4=_0xf92ab0;if(_0xab9a9c[_0x4c91e4(0xa5b)]){log('requesting\x20via\x20relaywss'),_0x2d3062[_0x4c91e4(0x1ac)]=++_0xab9a9c[_0x4c91e4(0x1ac)];if(!_0x143f2e){}else _0xab9a9c['rpcs'][_0x143f2e]&&_0x4c91e4(0x2a8)in _0xab9a9c[_0x4c91e4(0x404)][_0x143f2e]?_0xab9a9c['sendMsg']({..._0x2d3062,'altUUID':!![]},_0xab9a9c['rpcs'][_0x143f2e][_0x4c91e4(0x2a8)]):_0xab9a9c[_0x4c91e4(0x3ba)]({..._0x2d3062},_0x143f2e);}var _0x38c823=[],_0x868b24=JSON[_0x4c91e4(0x6d1)](_0x2d3062);for(var _0x583e85 in _0xab9a9c[_0x4c91e4(0x84f)]){if(_0x58239b&&_0x58239b===_0x583e85)continue;if(_0x143f2e&&_0x143f2e!==_0x583e85)continue;_0xab9a9c['relaywss']&&!_0x143f2e&&_0xab9a9c[_0x4c91e4(0x3ba)]({..._0x2d3062},_0x583e85);try{_0xab9a9c[_0x4c91e4(0x84f)][_0x583e85][_0x4c91e4(0x5cc)][_0x4c91e4(0x4c6)](_0x868b24),_0x38c823[_0x4c91e4(0x5e7)](_0x583e85);}catch(_0x1bf39f){_0xab9a9c[_0x4c91e4(0x84f)][_0x583e85]['startTime']+0x186a0_0x22d831&&(_0x22d831=_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x464)],_0x48e4b6=_0x1ed62b)),_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b][_0x1ba2eb(0x7ca)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b][_0x1ba2eb(0x7ca)]['sceneType2']&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b]['iframeEle'][_0x1ba2eb(0x464)]>_0x22d831&&(_0x22d831=_0xab9a9c['rpcs'][_0x1ed62b]['iframeEle'][_0x1ba2eb(0x464)],_0x48e4b6=_0x1ed62b)));}_0x48e4b6&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6]['mutedStateScene']=![],applyMuteState(_0x48e4b6),_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7eb)]&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x780)]&&clearInterval(_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7eb)]['controlTimer']),_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7eb)][_0x1ba2eb(0xb0f)]=![],_0xab9a9c[_0x1ba2eb(0x20b)]&&(_0xab9a9c['rpcs'][_0x48e4b6]['videoElement'][_0x1ba2eb(0x780)]=setTimeout(showControlBar[_0x1ba2eb(0x8a5)](null,_0xab9a9c['rpcs'][_0x48e4b6][_0x1ba2eb(0x7eb)]),0x3e8)),_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7eb)]['style'][_0x1ba2eb(0x375)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7eb)]['style'][_0x1ba2eb(0x375)]!==_0x1ba2eb(0x2ed)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]=_0x1ba2eb(0x2ed),_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x464)]=Date[_0x1ba2eb(0x30e)](),_0x2557f4=!![])),_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7ca)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)]['display']&&_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6]['iframeEle']['style'][_0x1ba2eb(0x375)]!=='block'&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6][_0x1ba2eb(0x7ca)]['style'][_0x1ba2eb(0x375)]=_0x1ba2eb(0x2ed),_0xab9a9c[_0x1ba2eb(0x404)][_0x48e4b6]['iframeEle'][_0x1ba2eb(0x464)]=Date[_0x1ba2eb(0x30e)](),_0x2557f4=!![]));}else{for(var _0x1ed62b in _0xab9a9c[_0x1ba2eb(0x404)]){_0x1ed62b!==_0x2f50ee&&(_0xab9a9c['rpcs'][_0x1ed62b][_0x1ba2eb(0xaa5)]=!![],applyMuteState(_0x1ed62b),_0xab9a9c['rpcs'][_0x1ed62b]['videoElement']&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)]['display']&&_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!=='none'&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b]['videoElement'][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]=_0x1ba2eb(0xcd8),_0x2557f4=!![])),_0xab9a9c['rpcs'][_0x1ed62b][_0x1ba2eb(0x7ca)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)]['display']&&_0xab9a9c['rpcs'][_0x1ed62b][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!==_0x1ba2eb(0xcd8)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b]['iframeEle'][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]=_0x1ba2eb(0xcd8),_0x2557f4=!![]));}_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['mutedStateScene']=![],applyMuteState(_0x2f50ee),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)]?(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['videoElement'][_0x1ba2eb(0x780)]&&clearInterval(_0xab9a9c['rpcs'][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x780)]),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)]['controls']=![],_0xab9a9c['showControls']&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x780)]=setTimeout(showControlBar[_0x1ba2eb(0x8a5)](null,_0xab9a9c['rpcs'][_0x2f50ee][_0x1ba2eb(0x7eb)]),0x3e8)),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['videoElement'][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!==_0x1ba2eb(0x2ed)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)]['display']=_0x1ba2eb(0x2ed),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['videoElement'][_0x1ba2eb(0x464)]=Date[_0x1ba2eb(0x30e)](),_0x2557f4=!![])):_0x15772b=![],_0xab9a9c['rpcs'][_0x2f50ee]['iframeEle']&&_0xab9a9c['rpcs'][_0x2f50ee][_0x1ba2eb(0x7ca)]['style'][_0x1ba2eb(0x375)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['iframeEle'][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!==_0x1ba2eb(0x2ed)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['iframeEle'][_0x1ba2eb(0x1b0)]['display']=_0x1ba2eb(0x2ed),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x464)]=Date['now'](),_0x2557f4=!![]);}}else{if(_0xab9a9c[_0x1ba2eb(0x77e)]==0x1){if(_0x1c553e[_0x1ba2eb(0xb58)]==0x0)_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)]&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)]['display']&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!==_0x1ba2eb(0xcd8)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]='none',_0x2557f4=!![])),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)]['display']&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!=='none'&&(_0xab9a9c['rpcs'][_0x2f50ee]['iframeEle'][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]=_0x1ba2eb(0xcd8),_0x2557f4=!![]);else{for(var _0x1ed62b in _0xab9a9c['rpcs']){_0x1ed62b!==_0x2f50ee&&(_0xab9a9c['rpcs'][_0x1ed62b][_0x1ba2eb(0x7eb)]&&(_0xab9a9c['rpcs'][_0x1ed62b][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)]['display']&&_0xab9a9c['rpcs'][_0x1ed62b]['videoElement'][_0x1ba2eb(0x1b0)]['display']!==_0x1ba2eb(0xcd8)&&(_0xab9a9c['rpcs'][_0x1ed62b]['videoElement']['style'][_0x1ba2eb(0x375)]=_0x1ba2eb(0xcd8),_0x2557f4=!![])),_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b][_0x1ba2eb(0x7ca)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x1ed62b][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)]['display']&&_0xab9a9c['rpcs'][_0x1ed62b]['iframeEle']['style'][_0x1ba2eb(0x375)]!==_0x1ba2eb(0xcd8)&&(_0xab9a9c['rpcs'][_0x1ed62b][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]=_0x1ba2eb(0xcd8),_0x2557f4=!![]));}_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['videoElement']?(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x780)]&&clearInterval(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x780)]),_0xab9a9c['rpcs'][_0x2f50ee]['videoElement'][_0x1ba2eb(0xb0f)]=![],_0xab9a9c[_0x1ba2eb(0x20b)]&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x780)]=setTimeout(showControlBar[_0x1ba2eb(0x8a5)](null,_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['videoElement']),0x3e8)),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['videoElement'][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!==_0x1ba2eb(0x2ed)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]=_0x1ba2eb(0x2ed),_0x2557f4=!![])):_0x15772b=![],_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)]&&_0xab9a9c['rpcs'][_0x2f50ee][_0x1ba2eb(0x7ca)]['style']['display']&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!==_0x1ba2eb(0x2ed)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['iframeEle'][_0x1ba2eb(0x1b0)]['display']=_0x1ba2eb(0x2ed),_0x2557f4=!![]);}}else _0x1c553e['value']==0x0?(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0xaa5)]=!![],applyMuteState(_0x2f50ee),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)]&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)]['style']['display']&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!==_0x1ba2eb(0xcd8)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]=_0x1ba2eb(0xcd8),_0x2557f4=!![])),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)]&&_0xab9a9c['rpcs'][_0x2f50ee][_0x1ba2eb(0x7ca)]['style'][_0x1ba2eb(0x375)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)]['style'][_0x1ba2eb(0x375)]!=='none'&&(_0xab9a9c['rpcs'][_0x2f50ee][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]='none',_0x2557f4=!![])):(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0xaa5)]=![],applyMuteState(_0x2f50ee),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)]?(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['videoElement'][_0x1ba2eb(0x780)]&&clearInterval(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['videoElement'][_0x1ba2eb(0x780)]),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0xb0f)]=![],_0xab9a9c['showControls']&&(_0xab9a9c['rpcs'][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x780)]=setTimeout(showControlBar[_0x1ba2eb(0x8a5)](null,_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)]),0x3e8)),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)][_0x1ba2eb(0x375)]!==_0x1ba2eb(0x2ed)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x1b0)]['display']=_0x1ba2eb(0x2ed),_0x2557f4=!![])):(warnlog(_0x1ba2eb(0x7d2)),_0x15772b=![]),_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['iframeEle']&&_0xab9a9c['rpcs'][_0x2f50ee]['iframeEle']['style'][_0x1ba2eb(0x375)]&&_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7ca)][_0x1ba2eb(0x1b0)]['display']!==_0x1ba2eb(0x2ed)&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee]['iframeEle']['style'][_0x1ba2eb(0x375)]=_0x1ba2eb(0x2ed),_0x2557f4=!![]));}}_0xab9a9c['sceneSync'](_0x2f50ee);}else _0x1c553e[_0x1ba2eb(0xa0d)]==_0x1ba2eb(0x57d)&&(log(parseInt(_0x1c553e[_0x1ba2eb(0xb58)])/0x64),_0xab9a9c['rpcs'][_0x2f50ee][_0x1ba2eb(0x7eb)]&&(_0xab9a9c[_0x1ba2eb(0x404)][_0x2f50ee][_0x1ba2eb(0x7eb)][_0x1ba2eb(0x57d)]=parseInt(_0x1c553e['value'])/0x64,log(_0x1ba2eb(0x487))));}}}}!_0x15772b&&!_0x11b030&&(warnlog(_0x1ba2eb(0x98c)),_0xab9a9c[_0x1ba2eb(0x6ca)][_0x1c553e[_0x1ba2eb(0x357)]]&&clearTimeout(_0xab9a9c[_0x1ba2eb(0x6ca)][_0x1c553e[_0x1ba2eb(0x357)]]),_0xab9a9c['retryScenes'][_0x1c553e[_0x1ba2eb(0x357)]]=setTimeout(function(_0x2f5f4a){var _0x14d4d8=_0x1ba2eb;log(_0x14d4d8(0x93e)),_0xab9a9c[_0x14d4d8(0x29d)](_0x1c553e,!![]);},0xbb8,_0x1c553e[_0x1ba2eb(0x357)])),_0x2557f4&&updateMixer();}}else{if(_0x1c553e[_0x1ba2eb(0xa0d)]==_0x1ba2eb(0x74e)){}else{if(_0x1c553e[_0x1ba2eb(0xa0d)]==_0x1ba2eb(0xaba)){}}}}else _0x1c553e[_0x1ba2eb(0xa0d)]===_0x1ba2eb(0x624)&&(warnlog(_0x1ba2eb(0x599)),log(_0x1c553e),_0xab9a9c[_0x1ba2eb(0x624)]=_0x1c553e[_0x1ba2eb(0xb58)],pokeIframeAPI(_0x1ba2eb(0xabb),_0xab9a9c[_0x1ba2eb(0x624)]),updateMixer());}},_0xab9a9c[_0xf92ab0(0xa79)]=function(){var _0x3b9b3c=_0xf92ab0;log(_0x3b9b3c(0xad9)),_0xab9a9c[_0x3b9b3c(0xb38)]in _0xab9a9c[_0x3b9b3c(0x84f)]&&(_0xab9a9c[_0x3b9b3c(0x84f)][_0xab9a9c[_0x3b9b3c(0xb38)]][_0x3b9b3c(0x287)]&&_0xab9a9c[_0x3b9b3c(0x84f)][_0xab9a9c['directorUUID']][_0x3b9b3c(0x287)][_0x3b9b3c(0xa82)]&&(_0xab9a9c[_0x3b9b3c(0x84f)][_0xab9a9c[_0x3b9b3c(0xb38)]][_0x3b9b3c(0x287)][_0x3b9b3c(0xa82)][_0x3b9b3c(0x827)]=!![])),_0xab9a9c[_0x3b9b3c(0xb38)]in _0xab9a9c[_0x3b9b3c(0x404)]&&(_0xab9a9c[_0x3b9b3c(0x404)][_0xab9a9c[_0x3b9b3c(0xb38)]]['stats']&&_0xab9a9c[_0x3b9b3c(0x404)][_0xab9a9c['directorUUID']]['stats'][_0x3b9b3c(0xa82)]&&(_0xab9a9c[_0x3b9b3c(0x404)][_0xab9a9c[_0x3b9b3c(0xb38)]][_0x3b9b3c(0x287)][_0x3b9b3c(0xa82)][_0x3b9b3c(0x827)]=!![]),_0xab9a9c[_0x3b9b3c(0x827)]&&(getById(_0x3b9b3c(0x66f)+_0xab9a9c['directorUUID'])[_0x3b9b3c(0xc93)][_0x3b9b3c(0xada)]('directorBox'),_0xab9a9c['rpcs'][_0xab9a9c[_0x3b9b3c(0xb38)]][_0x3b9b3c(0x5e0)]===![]&&miniTranslate(getById('label_'+_0xab9a9c['directorUUID']),_0x3b9b3c(0x573)))),_0xab9a9c[_0x3b9b3c(0xb80)](),updateUserList(),pokeIframeAPI(_0x3b9b3c(0x8b0),_0xab9a9c[_0x3b9b3c(0xb38)]);},_0xab9a9c[_0xf92ab0(0x3ea)]=async function _0x56f5d4(_0x39076c=![]){var _0xb468de=_0xf92ab0;if(_0xab9a9c[_0xb468de(0x6a1)]===!![]){log(_0xb468de(0x806));return;}if(_0xab9a9c['ws']!==null){log(_0xb468de(0x244));return;}_0xab9a9c[_0xb468de(0xad2)]==![]&&(_0xab9a9c[_0xb468de(0x80d)]!==![]?_0xab9a9c[_0xb468de(0xad2)]=_0xb468de(0xad0):_0xab9a9c[_0xb468de(0xad2)]=_0xb468de(0x216));if(!RTCPeerConnection){console[_0xb468de(0xca1)](getTranslation(_0xb468de(0x385)));!_0xab9a9c['cleanOutput']&&warnUser(getTranslation(_0xb468de(0x385)),![],![]);return;}_0xab9a9c['ws']===null&&(_0xab9a9c['ws']=![],await chooseBestTURN());if(_0xab9a9c['customWSS']===![]){_0xab9a9c[_0xb468de(0x649)]=_0xab9a9c[_0xb468de(0x54d)](0xc);for(var _0x181b8e in _0xab9a9c[_0xb468de(0x404)]){warnlog('Checking\x20to\x20see\x20if\x20reconnectino\x20to\x20ws\x20lost\x20any\x20peers'),_0xab9a9c[_0xb468de(0x404)][_0x181b8e]['connectionState']===_0xb468de(0xa40)&&(warnlog('cleaning\x20up\x20lost\x20connection'),_0xab9a9c[_0xb468de(0x35f)](_0x181b8e));}}_0xab9a9c[_0xb468de(0xc9e)]?(_0xab9a9c['ws']={},_0xab9a9c['ws']['close']=function(_0x489938){},_0xab9a9c['ws']['readyState']=0x1,_0xab9a9c['ws'][_0xb468de(0x4c6)]=function(_0x1252bb){var _0x5cbe97=_0xb468de;parent[_0x5cbe97(0x3c6)]({'bypass':_0x1252bb},_0xab9a9c['iframetarget']);},setTimeout(function(){var _0x313622=_0xb468de;_0xab9a9c['ws'][_0x313622(0x97d)]();},0xa)):_0xab9a9c['ws']=new WebSocket(_0xab9a9c[_0xb468de(0xad2)]),_0x39076c==![]&&(_0xab9a9c['showTime']===!![]&&(_0xab9a9c['showTime']=null,toggleClock()),_0xab9a9c[_0xb468de(0x38f)]=setTimeout(function(){var _0x318efc=_0xb468de;pokeIframeAPI('hssConnection',_0x318efc(0x38f)),pokeIframeAPI(_0x318efc(0xb84),_0x318efc(0x38f)),errorlog(_0x318efc(0x893)),!_0xab9a9c[_0x318efc(0xca4)]&&(!_0xab9a9c['studioSoftware']&&(_0xab9a9c['warnUserTriggered']=!![],warnUser(getTranslation('site-not-responsive'),0x7530,![])));},0x7530)),_0xab9a9c['ws'][_0xb468de(0x97d)]=function _0x537cf7(){var _0x520ab5=_0xb468de;_0xab9a9c[_0x520ab5(0x59c)]&&closeModal();_0xab9a9c[_0x520ab5(0x7df)]=!![],clearTimeout(_0xab9a9c[_0x520ab5(0x66a)]),clearTimeout(_0xab9a9c[_0x520ab5(0x38f)]),log(_0x520ab5(0x81f));_0xab9a9c[_0x520ab5(0x6b3)]&&_0xab9a9c[_0x520ab5(0xa06)]&&(_0xab9a9c[_0x520ab5(0xa06)]['wssSuccess']=!![]);checkConnection();if(_0xab9a9c[_0x520ab5(0x9cf)]){errorlog(_0x520ab5(0x317));for(_0x5bcf77 in _0xab9a9c[_0x520ab5(0x404)]){try{_0xab9a9c['rpcs'][_0x5bcf77]['streamID']?!_0xab9a9c['include'][_0x520ab5(0xa4f)](_0xab9a9c[_0x520ab5(0x404)][_0x5bcf77][_0x520ab5(0xa96)])&&_0xab9a9c[_0x520ab5(0x35f)](_0x5bcf77):_0xab9a9c[_0x520ab5(0x35f)](_0x5bcf77);}catch(_0x5af343){}}for(_0x5bcf77 in _0xab9a9c['pcs']){try{_0xab9a9c[_0x520ab5(0x219)](_0x5bcf77);}catch(_0x112b04){}}_0xab9a9c[_0x520ab5(0x9cf)]=![],_0xab9a9c['broadcastIFrame']=![],_0xab9a9c[_0x520ab5(0x2b7)]&&(!_0xab9a9c[_0x520ab5(0x2b7)]['closed']&&(_0xab9a9c[_0x520ab5(0x2b7)][_0x520ab5(0x3ef)](),_0xab9a9c[_0x520ab5(0x2b7)]=null));}if(_0xab9a9c[_0x520ab5(0x2f0)]&&_0xab9a9c[_0x520ab5(0x2f0)]['length']>0x0)try{var _0x41fdd6=_0xab9a9c[_0x520ab5(0x2f0)][_0x520ab5(0x912)](-0x1e);_0xab9a9c['msg']=[],_0x41fdd6[_0x520ab5(0x306)](function(_0x5d92dd){var _0xb06dab=_0x520ab5;log(_0xb06dab(0x8d0)),_0xab9a9c[_0xb06dab(0x3ba)](_0x5d92dd);});}catch(_0xeaa49e){errorlog(_0xeaa49e);}if(_0x39076c==!![]){pokeIframeAPI(_0x520ab5(0x24a),'reconnected'),pokeIframeAPI(_0x520ab5(0xb84),_0x520ab5(0xa35));_0xab9a9c[_0x520ab5(0x41d)]&&_0xab9a9c[_0x520ab5(0x3b0)]();if(_0xab9a9c['roomid']){log(_0x520ab5(0xa34)),log(_0x520ab5(0x3f4)),joinRoom(_0xab9a9c['roomid']);if(_0xab9a9c['include'][_0x520ab5(0xade)]){var _0x2738b0=Object[_0x520ab5(0x7db)](_0xab9a9c[_0x520ab5(0x2f4)]);for(var _0x5bcf77=0x0;_0x5bcf77<_0x2738b0['length'];_0x5bcf77++){_0xab9a9c['include'][_0x520ab5(0xa4f)](_0x2738b0[_0x5bcf77])&&(log(_0x520ab5(0xa94)+_0x2738b0[_0x5bcf77]),_0xab9a9c[_0x520ab5(0x932)](_0x2738b0[_0x5bcf77]));}}}else{var _0x2738b0=Object[_0x520ab5(0x7db)](_0xab9a9c[_0x520ab5(0x2f4)]);for(var _0x5bcf77=0x0;_0x5bcf77<_0x2738b0[_0x520ab5(0xade)];_0x5bcf77++){log(_0x520ab5(0xa94)+_0x2738b0[_0x5bcf77]),_0xab9a9c[_0x520ab5(0x932)](_0x2738b0[_0x5bcf77]);}}}else{pokeIframeAPI(_0x520ab5(0x24a),_0x520ab5(0x497)),pokeIframeAPI(_0x520ab5(0xb84),_0x520ab5(0x497));if(typeof fetchPerformerFromToken===_0x520ab5(0x32c))fetchPerformerFromToken()[_0x520ab5(0x998)](function(){var _0x714be7=_0x520ab5;typeof initTipNotifications===_0x714be7(0x32c)&&initTipNotifications();});else typeof initTipNotifications===_0x520ab5(0x32c)&&initTipNotifications();}},_0xab9a9c[_0xb468de(0xb20)]=function(_0xd87bae){var _0x39f34e=_0xb468de;for(var _0x57e33f in _0xab9a9c['rpcs']){if(_0xab9a9c[_0x39f34e(0x404)][_0x57e33f][_0x39f34e(0xa96)]===_0xd87bae)return log(_0x39f34e(0x2ec)),![];}if(_0xab9a9c[_0x39f34e(0x2f4)][_0xd87bae])return log(_0x39f34e(0x768)),![];return _0xab9a9c[_0x39f34e(0x932)](_0xd87bae),log('requesting\x20stream'),!![];},_0xab9a9c['ws'][_0xb468de(0xb95)]=async function(_0x4b0b5b){var _0x44ebeb=_0xb468de;clearTimeout(_0xab9a9c[_0x44ebeb(0x66a)]);try{var _0x44d467=JSON[_0x44ebeb(0xa4b)](_0x4b0b5b[_0x44ebeb(0x67b)]);}catch(_0x46b9c3){try{var _0x44d467=JSON[_0x44ebeb(0xa4b)](_0x4b0b5b[_0x44ebeb(0x67b)][_0x44ebeb(0xc7a)]());}catch(_0x32b514){errorlog(_0x32b514);return;}}_0x44d467[_0x44ebeb(0xa96)]&&(_0x44d467[_0x44ebeb(0xa96)]=_0xab9a9c[_0x44ebeb(0x58d)](_0x44d467['streamID']));if('remote'in _0x44d467){_0x44d467=await _0xab9a9c[_0x44ebeb(0x476)](_0x44d467);if(!_0x44d467)return;}if(_0xab9a9c[_0x44ebeb(0x98e)]){if(_0x44ebeb(0x208)in _0x44d467&&_0xab9a9c[_0x44ebeb(0xab9)]&&_0x44d467[_0x44ebeb(0x208)]===_0xab9a9c[_0x44ebeb(0xab9)])return;else log(_0x44d467);if(_0x44ebeb(0xab9)in _0x44d467){if(_0xab9a9c[_0x44ebeb(0xab9)]){if(_0x44d467[_0x44ebeb(0xab9)]!==_0xab9a9c[_0x44ebeb(0xab9)])return;}else return;delete _0x44d467[_0x44ebeb(0xab9)];}if(_0x44ebeb(0x70d)in _0x44d467){if(!_0xab9a9c[_0x44ebeb(0xbb7)])return;if(_0x44ebeb(0x7aa)in _0x44d467){if(_0x44d467[_0x44ebeb(0x7aa)]===_0x44ebeb(0x74e)){if(_0x44ebeb(0x70d)in _0x44d467){if(_0x44ebeb(0x357)in _0x44d467){if(_0x44d467[_0x44ebeb(0x357)]==_0xab9a9c[_0x44ebeb(0xab9)]){_0x44d467['request']=_0x44ebeb(0x9cf),_0xab9a9c['roomenc']=_0x44d467[_0x44ebeb(0x70d)];var _0x5c67cf={};_0x5c67cf[_0x44ebeb(0x7aa)]=_0x44ebeb(0x8ba),_0x5c67cf[_0x44ebeb(0x70d)]=_0xab9a9c[_0x44ebeb(0xbb7)],_0x5c67cf[_0x44ebeb(0xa96)]=_0xab9a9c[_0x44ebeb(0xa96)],_0xab9a9c['sendMsg'](_0x5c67cf);}else return;}else return;}else return;}else{if(_0x44d467[_0x44ebeb(0x70d)]!==_0xab9a9c['roomenc'])return;}}else{if(_0x44d467['roomid']!==_0xab9a9c[_0x44ebeb(0xbb7)])return;}delete _0x44d467[_0x44ebeb(0x70d)];}if(_0x44ebeb(0x827)in _0x44d467){if(_0xab9a9c['token']||_0xab9a9c[_0x44ebeb(0x830)])await checkToken();else _0x44d467['from']&&(_0xab9a9c[_0x44ebeb(0xb38)]=_0x44d467['from'],_0xab9a9c[_0x44ebeb(0x887)]=![],_0xab9a9c[_0x44ebeb(0x8a3)]=[],_0xab9a9c[_0x44ebeb(0x8a3)][_0x44ebeb(0x5e7)](_0xab9a9c[_0x44ebeb(0xb38)]),_0xab9a9c[_0x44ebeb(0xa79)]());delete _0x44d467[_0x44ebeb(0x827)];}_0x44ebeb(0x208)in _0x44d467&&(_0x44d467[_0x44ebeb(0xab9)]=_0x44d467[_0x44ebeb(0x208)],delete _0x44d467['from']);if(_0x44ebeb(0x7aa)in _0x44d467){if(_0x44d467['request']===_0x44ebeb(0x67d)){if(_0x44ebeb(0xa96)in _0x44d467){if(_0x44d467[_0x44ebeb(0xa96)]===_0xab9a9c[_0x44ebeb(0xa96)])_0x44d467['request']=_0x44ebeb(0x2cb);else return;}}else{if(_0x44d467[_0x44ebeb(0x7aa)]==='seed'){if(_0xab9a9c['view_set']){if(_0xab9a9c['view_set']['includes'](_0x44d467[_0x44ebeb(0xa96)])){play(_0x44d467[_0x44ebeb(0xa96)]);return;}else return;}}else{if(_0x44d467[_0x44ebeb(0x7aa)]===_0x44ebeb(0x8ba)){if(_0x44ebeb(0xa96)in _0x44d467){if(_0xab9a9c[_0x44ebeb(0x4f1)]){if(_0xab9a9c['view_set']['includes'](_0x44d467['streamID']))play(_0x44d467[_0x44ebeb(0xa96)]);else{}}else play(_0x44d467['streamID']);}_0x44d467['request']=_0x44ebeb(0x2cb);}}}}else{if('streamID'in _0x44d467){if(_0xab9a9c[_0x44ebeb(0x4f1)]){if(_0xab9a9c[_0x44ebeb(0x4f1)][_0x44ebeb(0xa4f)](_0x44d467[_0x44ebeb(0xa96)])){}else return;}else{if(_0xab9a9c['view']){if(_0xab9a9c[_0x44ebeb(0x7c7)]!==_0x44d467[_0x44ebeb(0xa96)])return;else{}}}}}}if(_0x44d467[_0x44ebeb(0x5ee)]||_0x44d467['mid']||_0x44d467['rmid']){let _0x172f07=_0x44d467[_0x44ebeb(0x5ee)]||_0x44d467[_0x44ebeb(0x1ac)]||_0x44d467[_0x44ebeb(0x65c)];if(_0xab9a9c[_0x44ebeb(0xc76)][_0x44d467[_0x44ebeb(0xab9)]]){if(_0xab9a9c[_0x44ebeb(0xc76)][_0x44d467['UUID']][_0x44ebeb(0xa4f)](_0x172f07))return;else _0xab9a9c[_0x44ebeb(0xc76)][_0x44d467[_0x44ebeb(0xab9)]][_0x44ebeb(0x5e7)](_0x172f07);}else _0xab9a9c['mids'][_0x44d467[_0x44ebeb(0xab9)]]=[_0x172f07];}if(_0x44d467[_0x44ebeb(0x7aa)]){if(_0x44d467[_0x44ebeb(0x7aa)]==_0x44ebeb(0x2cb)){_0xab9a9c[_0x44ebeb(0x27a)]&&_0xab9a9c[_0x44ebeb(0x70d)]&&(_0xab9a9c['pendingViewers']=_0xab9a9c[_0x44ebeb(0x3b3)]||{},_0xab9a9c[_0x44ebeb(0x3b3)][_0x44d467[_0x44ebeb(0xab9)]]={'timestamp':Date['now'](),'validating':!![]},log(_0x44ebeb(0x8dc)+_0x44d467[_0x44ebeb(0xab9)]+_0x44ebeb(0x72b)));if(_0xab9a9c['queue']){if(_0xab9a9c[_0x44ebeb(0x8a3)][_0x44ebeb(0x565)](_0x44d467[_0x44ebeb(0xab9)])>=0x0)_0xab9a9c['offerSDP'](_0x44d467[_0x44ebeb(0xab9)]);else{if(_0xab9a9c['director'])_0x44d467[_0x44ebeb(0xab9)]in _0xab9a9c['rpcs']&&_0xab9a9c['offerSDP'](_0x44d467[_0x44ebeb(0xab9)]);else{if(_0xab9a9c['queueType']==0x3||_0xab9a9c[_0x44ebeb(0x5df)]==0x4)_0xab9a9c[_0x44ebeb(0x2cb)](_0x44d467[_0x44ebeb(0xab9)]);else return;}}}else _0xab9a9c['offerSDP'](_0x44d467['UUID']);}else{if(_0x44d467[_0x44ebeb(0x7aa)]==_0x44ebeb(0x206)){log(_0x44d467);if(_0xab9a9c[_0x44ebeb(0xac0)]||_0xab9a9c['mainDirectorPassword'])await checkToken();else'director'in _0x44d467?(_0xab9a9c[_0x44ebeb(0xb38)]=_0x44d467[_0x44ebeb(0x827)],_0xab9a9c[_0x44ebeb(0x887)]=![],_0xab9a9c[_0x44ebeb(0x7f2)](),_0xab9a9c['directorList'][_0x44ebeb(0x5e7)](_0xab9a9c[_0x44ebeb(0xb38)]),_0xab9a9c['newMainDirectorSetup']()):(_0xab9a9c[_0x44ebeb(0xb38)]=![],_0xab9a9c[_0x44ebeb(0x887)]=![],_0xab9a9c['cleanDirectorList']());_0xab9a9c['director']&&_0xab9a9c[_0x44ebeb(0x27a)]&&window[_0x44ebeb(0x588)]&&_0xab9a9c['authToken']&&(_0x44ebeb(0x1aa)in _0x44d467&&_0x44d467['claim']===!![]&&setTimeout(async()=>{var _0x179e45=_0x44ebeb;await window[_0x179e45(0x588)][_0x179e45(0xa70)]();},0x3e8));if(_0xab9a9c[_0x44ebeb(0x27a)]&&window[_0x44ebeb(0x588)]&&_0x44d467[_0x44ebeb(0x832)]){const _0x51046f=[];for(const _0x2388f3 of _0x44d467[_0x44ebeb(0x832)]){if(_0x2388f3[_0x44ebeb(0xa96)]){const _0x42eec3=await window[_0x44ebeb(0x588)][_0x44ebeb(0xa7e)](_0x2388f3[_0x44ebeb(0xa96)]);_0x42eec3&&!_0x42eec3[_0x44ebeb(0xca1)]&&(_0xab9a9c[_0x44ebeb(0x493)]&&_0xab9a9c[_0x44ebeb(0x493)][_0x2388f3[_0x44ebeb(0xa96)]]&&(_0x2388f3['encryptedStreamID']=Object[_0x44ebeb(0x7db)](_0xab9a9c[_0x44ebeb(0x493)])[_0x44ebeb(0x54f)](_0x575f47=>_0xab9a9c[_0x44ebeb(0x493)][_0x575f47]===_0x2388f3['streamID'])),_0x51046f[_0x44ebeb(0x5e7)](_0x2388f3));}else _0x51046f['push'](_0x2388f3);}_0x44d467['list']=_0x51046f;}if(_0xab9a9c[_0x44ebeb(0x830)]){}else{if(_0x44ebeb(0x1aa)in _0x44d467){if(_0xab9a9c[_0x44ebeb(0xac0)]||_0x44d467[_0x44ebeb(0x1aa)]==![]){if(!_0xab9a9c['cleanOutput']){miniTranslate(getById(_0x44ebeb(0x908)),_0x44ebeb(0x65a));if(_0xab9a9c['directorPassword'])_0xab9a9c[_0x44ebeb(0x40a)]===null&&warnUser(getTranslation(_0x44ebeb(0xc5a)),![],![]);else _0xab9a9c['token']?setTimeout(function(){warnUser(getTranslation('token-room-is-claimed'),![],![]);},0x1):setTimeout(function(){var _0x313a0d=_0x44ebeb;warnUser(getTranslation(_0x313a0d(0x815)),![],![]);},0x1);}_0xab9a9c[_0x44ebeb(0x40a)]=![],pokeAPI(_0x44ebeb(0x827),![]),pokeIframeAPI('director',![]);}else _0xab9a9c[_0x44ebeb(0x40a)]=!![],pokeAPI(_0x44ebeb(0x827),!![]),pokeIframeAPI('director',!![]);}}_0xab9a9c[_0x44ebeb(0x9dc)]=_0x44d467[_0x44ebeb(0x832)],_0xab9a9c[_0x44ebeb(0xc2a)]['resolve'](_0x44d467[_0x44ebeb(0x832)]);}else{if(_0x44d467[_0x44ebeb(0x7aa)]==_0x44ebeb(0x9cf)){_0xab9a9c[_0x44ebeb(0xb3c)]=[],_0xab9a9c['transferred']=!![],_0xab9a9c[_0x44ebeb(0x78d)]=![],log('You\x27ve\x20been\x20transferred'),pokeIframeAPI(_0x44ebeb(0x9cf));let _0x592f43=![];const _0x12b4ca=_0xab9a9c[_0x44ebeb(0x5df)]||(_0xab9a9c[_0x44ebeb(0x9fc)]==0x3?0x3:_0xab9a9c[_0x44ebeb(0x9fc)]==0x4?0x4:![]);if(!_0xab9a9c['director']){if(_0xab9a9c[_0x44ebeb(0x9fc)]==0x2||_0xab9a9c[_0x44ebeb(0x5df)]==0x2)_0xab9a9c['queue']=!![],_0xab9a9c[_0x44ebeb(0x9cf)]=!![];else _0x12b4ca==0x3||_0x12b4ca==0x4?(_0xab9a9c[_0x44ebeb(0x9fc)]=![],_0xab9a9c[_0x44ebeb(0x5df)]=_0x12b4ca,_0x592f43=!![]):(_0xab9a9c[_0x44ebeb(0x9fc)]=![],_0xab9a9c[_0x44ebeb(0x9cf)]=!![]);}else _0xab9a9c['transferred']=!![];if(!_0x592f43){for(_0x44ec2a in _0xab9a9c['rpcs']){try{!_0xab9a9c[_0x44ebeb(0xa6b)]['includes'](_0xab9a9c['rpcs'][_0x44ec2a]['streamID'])&&(warnlog('transferred\x20and\x20closing'),_0xab9a9c[_0x44ebeb(0x35f)](_0x44ec2a));}catch(_0x45dcc0){}}for(_0x44ec2a in _0xab9a9c[_0x44ebeb(0x84f)]){try{log(_0x44ebeb(0x618)),_0xab9a9c[_0x44ebeb(0x219)](_0x44ec2a);}catch(_0x56f41e){}}_0xab9a9c[_0x44ebeb(0x2b7)]&&(!_0xab9a9c[_0x44ebeb(0x2b7)][_0x44ebeb(0x5e8)]&&(_0xab9a9c[_0x44ebeb(0x2b7)][_0x44ebeb(0x3ef)](),_0xab9a9c[_0x44ebeb(0x2b7)]=null));}if(!_0x592f43){if(_0xab9a9c[_0x44ebeb(0xac0)]||_0xab9a9c[_0x44ebeb(0x830)])await checkToken();else'director'in _0x44d467?(_0xab9a9c[_0x44ebeb(0xb38)]=_0x44d467[_0x44ebeb(0x827)],_0xab9a9c[_0x44ebeb(0x887)]=![],_0xab9a9c[_0x44ebeb(0x8a3)]=[],_0xab9a9c['directorList'][_0x44ebeb(0x5e7)](_0xab9a9c[_0x44ebeb(0xb38)]),_0xab9a9c[_0x44ebeb(0xa79)]()):(_0xab9a9c['directorUUID']=![],_0xab9a9c[_0x44ebeb(0x887)]=![],_0xab9a9c[_0x44ebeb(0x8a3)]=[]);youveBeenTransferred(),_0xab9a9c['totalRoomBitrate']=_0xab9a9c['totalRoomBitrate_default'],updateMixer();}else youveBeenActivated(),_0xab9a9c[_0x44ebeb(0x5df)]=![];log(_0x44ebeb(0x61f)),log(_0x44d467[_0x44ebeb(0x832)]);for(var _0x44ec2a in _0x44d467['list']){if(_0x44ebeb(0xab9)in _0x44d467[_0x44ebeb(0x832)][_0x44ec2a]){if(_0x44d467['list'][_0x44ec2a][_0x44ebeb(0xa96)]){if(_0x44d467[_0x44ebeb(0x832)][_0x44ec2a][_0x44ebeb(0xab9)]in _0xab9a9c[_0x44ebeb(0x404)])log(_0x44ebeb(0x4a9));else{var _0x35a56b=_0xab9a9c[_0x44ebeb(0x58d)](_0x44d467[_0x44ebeb(0x832)][_0x44ec2a][_0x44ebeb(0xa96)]);log(_0x44ebeb(0xc43)+_0x35a56b);if(_0xab9a9c['queue']){if(_0xab9a9c[_0x44ebeb(0x8a3)][_0x44ebeb(0x565)](_0x44d467['list'][_0x44ec2a][_0x44ebeb(0xab9)])>=0x0)_0xab9a9c[_0x44ebeb(0x5df)]==0x2&&play(_0x35a56b,_0x44d467[_0x44ebeb(0x832)][_0x44ec2a][_0x44ebeb(0xab9)]);else{if(_0xab9a9c['view_set']&&_0xab9a9c[_0x44ebeb(0x4f1)][_0x44ebeb(0xa4f)](_0x35a56b))play(_0x35a56b,_0x44d467[_0x44ebeb(0x832)][_0x44ec2a][_0x44ebeb(0xab9)]);else _0xab9a9c['queueList']['length']<0x1388&&(!(_0x35a56b in _0xab9a9c[_0x44ebeb(0x608)])&&!_0xab9a9c[_0x44ebeb(0xb3c)][_0x44ebeb(0xa4f)](_0x35a56b)&&_0xab9a9c[_0x44ebeb(0xb3c)]['push'](_0x35a56b));}}else play(_0x35a56b,_0x44d467[_0x44ebeb(0x832)][_0x44ec2a][_0x44ebeb(0xab9)]);}}}}updateQueue();}else{if(_0x44d467[_0x44ebeb(0x7aa)]==_0x44ebeb(0xcaa)){log(_0x44d467);if(_0xab9a9c[_0x44ebeb(0xac0)]||_0xab9a9c[_0x44ebeb(0x830)])await checkToken();else'director'in _0x44d467?(_0xab9a9c[_0x44ebeb(0xb38)]=_0x44d467[_0x44ebeb(0x827)],_0xab9a9c[_0x44ebeb(0x887)]=![],_0xab9a9c['directorList']=[],_0xab9a9c['directorList'][_0x44ebeb(0x5e7)](_0xab9a9c[_0x44ebeb(0xb38)]),_0xab9a9c['newMainDirectorSetup']()):(_0xab9a9c['directorUUID']=![],_0xab9a9c[_0x44ebeb(0x8a3)]=[],errorlog('This\x20shouldn\x27t\x20happen'));updateUserList();}else{if(_0x44d467['request']==_0x44ebeb(0x29b)){if(_0xab9a9c['token']||_0xab9a9c[_0x44ebeb(0x830)])await checkToken();else _0x44d467[_0x44ebeb(0x827)]&&(_0xab9a9c[_0x44ebeb(0xb38)]=_0x44d467[_0x44ebeb(0xab9)],_0xab9a9c[_0x44ebeb(0x887)]=![],_0xab9a9c[_0x44ebeb(0x7f2)](),_0xab9a9c[_0x44ebeb(0x8a3)]['push'](_0xab9a9c[_0x44ebeb(0xb38)]),_0xab9a9c[_0x44ebeb(0xa79)]());if('streamID'in _0x44d467){log(_0x44ebeb(0xcd3));if(_0xab9a9c[_0x44ebeb(0x9fc)]){if(_0xab9a9c[_0x44ebeb(0x8a3)][_0x44ebeb(0x565)](_0x44d467[_0x44ebeb(0xab9)])>=0x0)_0xab9a9c['queueType']==0x2&&play(_0x35a56b,_0x44d467[_0x44ebeb(0xab9)]);else{if(_0xab9a9c['view_set']&&_0xab9a9c[_0x44ebeb(0x4f1)][_0x44ebeb(0xa4f)](_0x35a56b))play(_0x35a56b,_0x44d467[_0x44ebeb(0xab9)]);else _0xab9a9c[_0x44ebeb(0xb3c)][_0x44ebeb(0xade)]<0x1388&&(!(_0x44d467[_0x44ebeb(0xa96)]in _0xab9a9c[_0x44ebeb(0x608)])&&!_0xab9a9c['queueList'][_0x44ebeb(0xa4f)](_0x44d467[_0x44ebeb(0xa96)])&&(_0xab9a9c[_0x44ebeb(0xb3c)][_0x44ebeb(0x5e7)](_0x44d467[_0x44ebeb(0xa96)]),updateQueue(!![])));}}else play(_0x44d467[_0x44ebeb(0xa96)]);}else log(_0x44ebeb(0x78f));}else{if(_0x44d467[_0x44ebeb(0x7aa)]==_0x44ebeb(0x6ae)){log('Someone\x20published\x20a\x20video\x20to\x20the\x20Room'),log(_0x44d467);if(_0xab9a9c[_0x44ebeb(0x9fc)]){if(_0xab9a9c[_0x44ebeb(0x8a3)][_0x44ebeb(0x565)](_0x44d467[_0x44ebeb(0xab9)])>=0x0)_0xab9a9c[_0x44ebeb(0x5df)]==0x2&&play(_0x35a56b,_0x44d467[_0x44ebeb(0xab9)]);else{if(_0xab9a9c[_0x44ebeb(0x4f1)]&&_0xab9a9c[_0x44ebeb(0x4f1)][_0x44ebeb(0xa4f)](_0x35a56b))play(_0x35a56b,_0x44d467[_0x44ebeb(0xab9)]);else _0xab9a9c['queueList'][_0x44ebeb(0xade)]<0x1388&&(!(_0x44d467[_0x44ebeb(0xa96)]in _0xab9a9c[_0x44ebeb(0x608)])&&!_0xab9a9c[_0x44ebeb(0xb3c)][_0x44ebeb(0xa4f)](_0x44d467[_0x44ebeb(0xa96)])&&(_0xab9a9c[_0x44ebeb(0xb3c)][_0x44ebeb(0x5e7)](_0x44d467['streamID']),updateQueue(!![])));}}else play(_0x44d467[_0x44ebeb(0xa96)]);}else{if(_0x44d467[_0x44ebeb(0x7aa)]==_0x44ebeb(0x4e4)){errorlog(_0x44d467),pokeIframeAPI(_0x44ebeb(0x4e4),_0x44d467[_0x44ebeb(0x3a3)]);if(_0xab9a9c[_0x44ebeb(0x268)]===![]){if(_0x44ebeb(0x3a3)in _0x44d467){if(_0x44d467[_0x44ebeb(0x3a3)]===_0x44ebeb(0x374))_0xab9a9c[_0x44ebeb(0x9c5)]<0x2?(_0xab9a9c[_0x44ebeb(0x9c5)]=parseInt(_0xab9a9c[_0x44ebeb(0x9c5)])+0x1,setTimeout(function(){_0xab9a9c['seedStream']();},0x1388)):(hangup(),!_0xab9a9c[_0x44ebeb(0xca4)]?_0xab9a9c[_0x44ebeb(0x6d8)]&&(_0xab9a9c[_0x44ebeb(0x6d8)][_0x44ebeb(0xade)]<0x3||_0xab9a9c[_0x44ebeb(0x6d8)]===_0x44ebeb(0x6f2))&&_0xab9a9c[_0x44ebeb(0x54c)]===_0xab9a9c[_0x44ebeb(0xa73)]?setTimeout(function(){var _0xf0c1cd=_0x44ebeb;warnUser(getTranslation(_0xf0c1cd(0x8d9)),![],![]);},0x1):setTimeout(function(){var _0x35343e=_0x44ebeb;warnUser(getTranslation(_0x35343e(0x5c6)),![],![]);},0x1):console[_0x44ebeb(0x2ea)](getTranslation(_0x44ebeb(0x5c6))));else{if(_0x44d467[_0x44ebeb(0x3a3)]===_0x44ebeb(0xcc4))!_0xab9a9c[_0x44ebeb(0xca4)]&&setTimeout(()=>{var _0x9b2563=_0x44ebeb;warnUser(_0x9b2563(0x5e3));},0x1);else{if(_0xab9a9c[_0x44ebeb(0xac0)]||_0xab9a9c['mainDirectorPassword']){}else{if(_0x44d467[_0x44ebeb(0x3a3)]===_0x44ebeb(0xb2e)){!_0xab9a9c[_0x44ebeb(0xca4)]&&(miniTranslate(getById('head4'),_0x44ebeb(0x65a)),_0xab9a9c[_0x44ebeb(0x959)]?_0xab9a9c[_0x44ebeb(0x40a)]===null&&warnUser(getTranslation(_0x44ebeb(0xc5a)),![],![]):setTimeout(function(){warnUser(getTranslation('room-is-claimed'),![],![]);},0x1));_0xab9a9c[_0x44ebeb(0x40a)]=![],pokeAPI(_0x44ebeb(0x827),![]),pokeIframeAPI(_0x44ebeb(0x827),![]);try{_0xab9a9c[_0x44ebeb(0x8bc)]();}catch(_0x4303c8){}}else!_0xab9a9c[_0x44ebeb(0xca4)]&&setTimeout(()=>{var _0xd90d5b=_0x44ebeb;warnUser(_0x44d467[_0xd90d5b(0x3a3)]);},0x1);}}}}}}else _0x44d467['request']==_0x44ebeb(0x2ea)?'message'in _0x44d467&&warnlog(_0x44d467['message']):log(_0x44d467);}}}}}}}else{if(_0x44d467['description'])'streamID'in _0x44d467&&(_0x44d467[_0x44ebeb(0xa96)]in _0xab9a9c[_0x44ebeb(0x608)]&&(clearTimeout(_0xab9a9c['watchTimeoutList'][_0x44d467[_0x44ebeb(0xa96)]]),delete _0xab9a9c[_0x44ebeb(0x608)][_0x44d467[_0x44ebeb(0xa96)]])),_0xab9a9c['processDescription'](_0x44d467);else{if(_0x44d467[_0x44ebeb(0x4f8)])log(_0x44ebeb(0xc2e)),_0xab9a9c['processIce'](_0x44d467);else{if(_0x44d467['candidates'])log(_0x44ebeb(0x418)),_0xab9a9c[_0x44ebeb(0x975)](_0x44d467);else{if(_0x44d467['bye']||_0x44d467[_0x44ebeb(0x7aa)]==_0x44ebeb(0x560))warnlog(_0x44ebeb(0x9c2)),_0x44d467[_0x44ebeb(0xab9)]in _0xab9a9c[_0x44ebeb(0x84f)]&&(log(_0x44ebeb(0x618)),_0xab9a9c[_0x44ebeb(0x219)](_0x44d467[_0x44ebeb(0xab9)])),_0x44d467[_0x44ebeb(0xab9)]in _0xab9a9c['rpcs']&&(warnlog(_0x44ebeb(0x876)),_0xab9a9c[_0x44ebeb(0x35f)](_0x44d467[_0x44ebeb(0xab9)]));else{if(_0x44d467['iceRestartRequest']&&_0x44d467[_0x44ebeb(0xab9)])warnlog('Viewer\x20requested\x20ICE\x20restart\x20via\x20WebSocket'),_0x44d467[_0x44ebeb(0xab9)]in _0xab9a9c['pcs']&&(_0xab9a9c['pcs'][_0x44d467[_0x44ebeb(0xab9)]][_0x44ebeb(0xc25)]?(log(_0x44ebeb(0x1b7)+_0x44d467[_0x44ebeb(0xab9)]),_0xab9a9c[_0x44ebeb(0x84f)][_0x44d467[_0x44ebeb(0xab9)]]['restartIce']()):(log(_0x44ebeb(0x5ad)+_0x44d467[_0x44ebeb(0xab9)]),_0xab9a9c[_0x44ebeb(0x729)](_0x44d467[_0x44ebeb(0xab9)],!![])));else{if(_0xab9a9c[_0x44ebeb(0x835)]&&_0x44d467[_0x44ebeb(0xac0)])_0xab9a9c[_0x44ebeb(0xad1)]=_0x44d467[_0x44ebeb(0xac0)],updateReshareLink();else{if(_0x44d467[_0x44ebeb(0x5ee)]&&_0x44d467['UUID'])try{_0x44ebeb(0xaf4)in _0x44d467?await _0xab9a9c['processRPCSOnMessage'](_0x44d467,_0x44d467[_0x44ebeb(0xab9)]+_0x44ebeb(0x6f3)):await _0xab9a9c[_0x44ebeb(0xad6)](_0x44d467,_0x44d467[_0x44ebeb(0xab9)]);}catch(_0xfccd36){warnlog(_0x44ebeb(0xaa0)),warnlog(_0xfccd36[_0x44ebeb(0x67b)]);}else{if(_0x44d467['rmid']&&_0x44d467[_0x44ebeb(0xab9)])try{_0x44ebeb(0xaf4)in _0x44d467?await _0xab9a9c[_0x44ebeb(0x539)](_0x44d467,_0x44d467['UUID']+_0x44ebeb(0x6f3),_0x44d467[_0x44ebeb(0xab9)]):await _0xab9a9c['processPCSOnMessage'](_0x44d467,_0x44d467[_0x44ebeb(0xab9)]);}catch(_0x4364ed){warnlog(_0x44ebeb(0xaa0)),warnlog(_0x4364ed[_0x44ebeb(0x67b)]);}else{if(_0x44d467[_0x44ebeb(0x1ac)]&&_0x44d467[_0x44ebeb(0xab9)])try{if(_0xab9a9c[_0x44ebeb(0x84f)][_0x44d467[_0x44ebeb(0xab9)]])_0x44ebeb(0xaf4)in _0x44d467?await _0xab9a9c[_0x44ebeb(0x539)](_0x44d467,_0x44d467['UUID']+_0x44ebeb(0x6f3),_0x44d467[_0x44ebeb(0xab9)]):await _0xab9a9c[_0x44ebeb(0x539)](_0x44d467,_0x44d467[_0x44ebeb(0xab9)]);else _0xab9a9c['rpcs'][_0x44d467['UUID']]?_0x44ebeb(0xaf4)in _0x44d467?await _0xab9a9c[_0x44ebeb(0xad6)](_0x44d467,_0x44d467[_0x44ebeb(0xab9)]+_0x44ebeb(0x6f3)):await _0xab9a9c['processRPCSOnMessage'](_0x44d467,_0x44d467[_0x44ebeb(0xab9)]):warnlog('couldn\x27t\x20find\x20matching\x20pc\x20for\x20incoming\x20\x20mid');}catch(_0x47d5ac){warnlog(_0x44ebeb(0xaa0)),warnlog(_0x47d5ac[_0x44ebeb(0x67b)]);}else log(_0x44ebeb(0x1c4));}}}}}}}}}},_0xab9a9c['ws']['onerror']=async function(_0x347e3a){warnlog(_0x347e3a);},_0xab9a9c['ws'][_0xb468de(0x38c)]=async function(_0x1c47fe){var _0x1df8b1=_0xb468de;clearTimeout(_0xab9a9c[_0x1df8b1(0x66a)]),pokeIframeAPI('hssConnection','closed'),pokeIframeAPI('hss-connection',_0x1df8b1(0x5e8));try{_0x1df8b1(0x882)in _0x1c47fe&&(_0x1c47fe[_0x1df8b1(0x882)]==0x1f7&&(_0x39076c==![]&&(clearTimeout(_0xab9a9c[_0x1df8b1(0x38f)]),!_0xab9a9c['cleanOutput']&&warnUser('Failed\x20to\x20connect\x20to\x20service:\x20Error\x20503Possibly\x20too\x20many\x20connections\x20from\x20the\x20same\x20address\x20tried\x20to\x20connect.Visit\x20https://discord.vdo.ninja\x20for\x20support.',0x7530,![]))));}catch(_0x240efd){errorlog(_0x240efd);}warnlog(_0x1df8b1(0x72a));if(_0xab9a9c[_0x1df8b1(0x915)]==![])try{_0xab9a9c['ws'][_0x1df8b1(0x70b)]===WebSocket[_0x1df8b1(0x21e)]&&(_0xab9a9c['ws']=null,setTimeout(()=>{try{_0xab9a9c['connect'](!![]);}catch(_0x54b18b){}},0x1388));}catch(_0x25b2e0){errorlog(_0x25b2e0);}};},_0xab9a9c[_0xf92ab0(0x1eb)]=function(_0x4c20e1,_0x11a15b=null){var _0x444d60=_0xf92ab0;_0xab9a9c[_0x444d60(0xa5b)]&&(log(_0x444d60(0x909)),_0x4c20e1[_0x444d60(0x5ee)]=++_0xab9a9c['mid'],!_0x11a15b?_0xab9a9c[_0x444d60(0x3ba)](_0x4c20e1):(_0x4c20e1['UUID']=_0x11a15b,_0xab9a9c[_0x444d60(0x3ba)](_0x4c20e1,_0x11a15b)));if(_0x11a15b==null){_0x4c20e1=JSON[_0x444d60(0x6d1)](_0x4c20e1);for(var _0x37c287 in _0xab9a9c['pcs']){try{if(!_0xab9a9c['pcs'][_0x37c287]['sendChannel'])continue;_0xab9a9c[_0x444d60(0x84f)][_0x37c287][_0x444d60(0x5cc)][_0x444d60(0x4c6)](_0x4c20e1);}catch(_0x45555f){_0xab9a9c[_0x444d60(0x84f)][_0x37c287][_0x444d60(0x334)]+0x186a0{var _0x4a819c=_0x18b8b3;_0x4a5571[_0x4a819c(0x50d)]&&(_0x4a5571[_0x4a819c(0x50d)][_0x4a819c(0x8e4)]=![]);});}try{document[_0x18b8b3(0x2b2)](_0x18b8b3(0x5d6))&&(!_0xab9a9c['syncState']&&(_0xab9a9c[_0x18b8b3(0x39f)]={}),_0xab9a9c[_0x18b8b3(0xa96)]&&(_0xab9a9c[_0x18b8b3(0x39f)][_0xab9a9c[_0x18b8b3(0xa96)]]=getDetailedState(_0xab9a9c[_0x18b8b3(0xa96)])),getById(_0x18b8b3(0x5d6))['parentNode']['removeChild'](getById(_0x18b8b3(0x5d6))),updateLockedElements());}catch(_0x469ca8){warnlog(_0x469ca8);}var _0x3c3558={};_0x3c3558[_0x18b8b3(0x6cc)]=!![],_0x3c3558[_0x18b8b3(0x3a2)]=!![],_0xab9a9c['sendMessage'](_0x3c3558),getById(_0x18b8b3(0x931))[_0x18b8b3(0x50f)](),_0xab9a9c[_0x18b8b3(0x52b)]&&_0xab9a9c['whipOut'][_0x18b8b3(0x77c)]&&warnlog(_0x18b8b3(0xc3f));}catch(_0xa46153){errorlog(_0x18b8b3(0x49d));}log(_0x18b8b3(0x32b));},_0xab9a9c[_0xf92ab0(0x729)]=function(_0x1178e6,_0x4eea60=![]){var _0x2f1682=_0xf92ab0;_0xab9a9c[_0x2f1682(0x84f)][_0x1178e6][_0x2f1682(0x729)]({'iceRestart':_0x4eea60})[_0x2f1682(0x998)](_0x1ea635=>{var _0x111ff3=_0x2f1682;log(_0x111ff3(0x5ca));if(SafariVersion&&SafariVersion<=0xd&&(iOS||iPad)){}else{if(_0xab9a9c[_0x111ff3(0x839)]==0x3||_0xab9a9c[_0x111ff3(0x839)]==0x5||_0xab9a9c[_0x111ff3(0x839)]==0x1)_0xab9a9c[_0x111ff3(0xafa)]&&Firefox?(_0x1ea635[_0x111ff3(0x728)]=CodecsHandler['setOpusAttributes'](_0x1ea635[_0x111ff3(0x728)],{'stereo':0x0}),log(_0x111ff3(0xc6b))):(_0x1ea635['sdp']=CodecsHandler[_0x111ff3(0x276)](_0x1ea635[_0x111ff3(0x728)],{'stereo':0x1}),log(_0x111ff3(0x857)));else{if(iOS||iPad){}else _0xab9a9c[_0x111ff3(0x839)]==0x4&&(_0x1ea635[_0x111ff3(0x728)]=CodecsHandler[_0x111ff3(0x276)](_0x1ea635['sdp'],{'stereo':0x2}),log(_0x111ff3(0x857)));}}(iOS||iPad)&&(_0xab9a9c['removeOrientationFlag']&&_0x1ea635[_0x111ff3(0x728)][_0x111ff3(0xa4f)](_0x111ff3(0x86a))&&(_0x1ea635[_0x111ff3(0x728)]=_0x1ea635['sdp'][_0x111ff3(0x35b)]('a=extmap:3\x20urn:3gpp:video-orientation\x0d\x0a','')));if(_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6][_0x111ff3(0xa42)])try{_0x1ea635['sdp']=CodecsHandler['preferCodec'](_0x1ea635[_0x111ff3(0x728)],_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6]['preferVideoCodec'],_0xab9a9c['preferredVideoErrorCorrection']),log(_0x111ff3(0x1d0)+_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6][_0x111ff3(0xa42)]+_0x111ff3(0x503));}catch(_0x97982b){errorlog(_0x97982b),warnlog('couldn\x27t\x20set\x20preferred\x20video\x20codec');}if(_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6][_0x111ff3(0x3c8)])try{if(_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6][_0x111ff3(0x3c8)]===_0x111ff3(0x46e))_0x1ea635[_0x111ff3(0x728)]=CodecsHandler[_0x111ff3(0x5b7)](_0x1ea635[_0x111ff3(0x728)]);else{if(_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6]['preferAudioCodec']===_0x111ff3(0xcc0)){if(_0xab9a9c['audioInputChannels']&&_0xab9a9c['audioInputChannels']==0x1)_0x1ea635[_0x111ff3(0x728)]=CodecsHandler[_0x111ff3(0x6e3)](_0x1ea635[_0x111ff3(0x728)],_0xab9a9c[_0x111ff3(0x5e9)]||0xbb80,![]);else _0xab9a9c[_0x111ff3(0x839)]?_0x1ea635[_0x111ff3(0x728)]=CodecsHandler[_0x111ff3(0x6e3)](_0x1ea635[_0x111ff3(0x728)],_0xab9a9c[_0x111ff3(0x5e9)]||0xbb80,!![]):_0x1ea635[_0x111ff3(0x728)]=CodecsHandler[_0x111ff3(0x6e3)](_0x1ea635[_0x111ff3(0x728)],_0xab9a9c[_0x111ff3(0x5e9)]||0xbb80,![]);}else _0x1ea635['sdp']=CodecsHandler['preferAudioCodec'](_0x1ea635[_0x111ff3(0x728)],_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6][_0x111ff3(0x3c8)],_0xab9a9c['predAudio'],_0xab9a9c[_0x111ff3(0x8ea)]);}log(_0x111ff3(0x1d0)+_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6][_0x111ff3(0x3c8)]+'\x20as\x20preferred\x20audio\x20codec\x20by\x20viewer\x20via\x20API\x20(offer)');}catch(_0x4b175b){errorlog(_0x4b175b),warnlog(_0x111ff3(0x980));}if(Android&&_0xab9a9c[_0x111ff3(0xaf7)]!==![]&&_0xab9a9c['AndroidFix']){var _0x228dc9=![];try{_0x228dc9=_0x1ea635&&_0x1ea635[_0x111ff3(0x728)]&&(_0x1ea635[_0x111ff3(0x728)][_0x111ff3(0x629)](/^m=video /mg)||[])['length']>0x1||![];}catch(_0x11ac56){}!_0x228dc9&&(_0x1ea635['sdp']=_0x1ea635[_0x111ff3(0x728)][_0x111ff3(0x35b)](/42e01f/gi,'42001f'));}_0xab9a9c[_0x111ff3(0x38e)]&&(_0x1ea635['sdp']=filterSDPLAN(_0x1ea635[_0x111ff3(0x728)])),_0xab9a9c[_0x111ff3(0x3d4)]&&(_0x1ea635['sdp']=filterStunOnly(_0x1ea635[_0x111ff3(0x728)])),_0xab9a9c[_0x111ff3(0x84f)][_0x1178e6][_0x111ff3(0x93c)](_0x1ea635)['then'](async function(){var _0x27afbe=_0x111ff3;if(_0xab9a9c[_0x27afbe(0xaf1)]){if(_0xab9a9c[_0x27afbe(0x84f)][_0x1178e6][_0x27afbe(0x6dd)]===null){let _0x3c1cf2;const _0x13ddd1=new Promise(_0x4660cf=>{_0x3c1cf2=_0x4660cf;});_0xab9a9c['pcs'][_0x1178e6][_0x27afbe(0x6dd)]={'promise':_0x13ddd1,'resolve':_0x3c1cf2},await _0xab9a9c[_0x27afbe(0x84f)][_0x1178e6][_0x27afbe(0x6dd)][_0x27afbe(0x376)];if(!_0xab9a9c[_0x27afbe(0x84f)][_0x1178e6])return;}}log('publishing\x20SDP\x20Offer:\x20'+_0x1178e6),_0xab9a9c[_0x27afbe(0xba7)](_0x1178e6);var _0x3d53ab={};_0x3d53ab[_0x27afbe(0xab9)]=_0x1178e6,_0x3d53ab[_0x27afbe(0xa96)]=_0xab9a9c[_0x27afbe(0xa96)],_0x3d53ab['description']=filterDescriptionIpv6(_0xab9a9c['pcs'][_0x1178e6][_0x27afbe(0xb04)]),_0x3d53ab[_0x27afbe(0x94a)]=_0xab9a9c[_0x27afbe(0x84f)][_0x1178e6][_0x27afbe(0x94a)];_0xab9a9c['customWSS']&&(_0x3d53ab[_0x27afbe(0x85d)]=_0xab9a9c[_0x27afbe(0x268)]);_0xab9a9c[_0x27afbe(0x96d)]!==![]&&(_0x3d53ab['slot']=_0xab9a9c['slot']);if(_0xab9a9c[_0x27afbe(0x32d)]!==![]){var _0x2b96ed=_0xab9a9c['screenStream'][_0x27afbe(0xb93)](),_0x5bc8b3=_0xab9a9c['pcs'][_0x1178e6]['getSenders'](),_0x2745b6=[];for(var _0x59698f=0x0;_0x59698f<_0x5bc8b3[_0x27afbe(0xade)];_0x59698f++){for(var _0x185d6b=0x0;_0x185d6b<_0x2b96ed[_0x27afbe(0xade)];_0x185d6b++){_0x5bc8b3[_0x59698f][_0x27afbe(0x50d)]&&_0x5bc8b3[_0x59698f]['track']['id']==_0x2b96ed[_0x185d6b]['id']&&_0x5bc8b3[_0x59698f]['track'][_0x27afbe(0xcd1)]==_0x2b96ed[_0x185d6b][_0x27afbe(0xcd1)]&&_0x2745b6[_0x27afbe(0x5e7)](_0x59698f);}}_0x2745b6[_0x27afbe(0xade)]&&(_0x3d53ab[_0x27afbe(0x30b)]=_0x2745b6);}_0xab9a9c['password']?_0xab9a9c[_0x27afbe(0xab5)](JSON[_0x27afbe(0x6d1)](_0x3d53ab[_0x27afbe(0x5dd)]))[_0x27afbe(0x998)](function(_0x44385d){var _0x2e7485=_0x27afbe;_0x3d53ab[_0x2e7485(0x5dd)]=_0x44385d[0x0],_0x3d53ab[_0x2e7485(0xa85)]=_0x44385d[0x1],_0xab9a9c[_0x2e7485(0x874)](_0x3d53ab);})[_0x27afbe(0xacc)](errorlog):_0xab9a9c[_0x27afbe(0x874)](_0x3d53ab);})[_0x111ff3(0xacc)](errorlog);})[_0x2f1682(0xacc)](errorlog);},_0xab9a9c['sendKeyFrameScenes']=function(){var _0x53579a=_0xf92ab0;for(var _0x444e80 in _0xab9a9c[_0x53579a(0x84f)]){_0xab9a9c[_0x53579a(0x84f)][_0x444e80]['scene']!==![]?(_0xab9a9c[_0x53579a(0xa03)](_0x444e80),log('FORCE\x20KEYFRAME\x20FOR\x20SCENE')):log(_0x53579a(0x9ed));}},_0xab9a9c[_0xf92ab0(0x219)]=function(_0x40f0a3,_0x3299eb=!![]){var _0x41de7e=_0xf92ab0;log('closePC');if(!(_0x40f0a3 in _0xab9a9c[_0x41de7e(0x84f)]))return;clearTimeout(_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3][_0x41de7e(0xbaa)]),clearTimeout(_0xab9a9c['pcs'][_0x40f0a3][_0x41de7e(0x808)]),clearInterval(_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3][_0x41de7e(0x9da)]),clearTimeout(_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3][_0x41de7e(0x957)]),pokeIframeAPI(_0x41de7e(0x435),![],_0x40f0a3);try{_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3][_0x41de7e(0xa39)]&&_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3][_0x41de7e(0xa39)][_0x41de7e(0x560)]&&_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3]['canvasOverlay']['cleanup'](),_0xab9a9c[_0x41de7e(0x365)]&&_0xab9a9c[_0x41de7e(0x365)]['includes'](_0x40f0a3)&&(_0xab9a9c['soloChatUUID'][_0x41de7e(0x954)](_0xab9a9c['soloChatUUID'][_0x41de7e(0x565)](_0x40f0a3),0x1),_0xab9a9c['applySoloChat'](![]));}catch(_0x5b8dae){errorlor(_0x5b8dae);}if('realUUID'in _0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3]){delete _0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3],applySceneState();return;}if(_0x40f0a3+_0x41de7e(0x6f3)in _0xab9a9c[_0x41de7e(0x84f)]&&_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)][_0x41de7e(0x2a8)]&&_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)]['realUUID']===_0x40f0a3){clearTimeout(_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)][_0x41de7e(0xbaa)]),clearTimeout(_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)]['closeTimeout']),clearInterval(_0xab9a9c['pcs'][_0x40f0a3+_0x41de7e(0x6f3)][_0x41de7e(0x9da)]),clearTimeout(_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)][_0x41de7e(0x957)]);try{_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)][_0x41de7e(0xa39)]&&_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+'_screen'][_0x41de7e(0xa39)][_0x41de7e(0x560)]&&_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)][_0x41de7e(0xa39)][_0x41de7e(0x560)]();}catch(_0x5991d){errorlor(_0x5991d);}_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)]=null,delete _0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3+_0x41de7e(0x6f3)];}try{_0xab9a9c[_0x41de7e(0x1eb)]({'bye':!![]},_0x40f0a3);}catch(_0x12b814){}try{_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3][_0x41de7e(0x3ef)]();}catch(_0x37883f){}_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3][_0x41de7e(0x1ab)]&&(_0xab9a9c['beepToNotify']&&(_0x3299eb&&(warnlog(_0x41de7e(0x207)),playtone(![],_0x41de7e(0x739))))),_0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3]=null,_0xab9a9c[_0x41de7e(0x915)]&&(!_0xab9a9c['cleanOutput']&&setTimeout(function _0x576e4f(){var _0x210625=_0x41de7e;warnUser(_0x210625(0xc37));},0x1)),delete _0xab9a9c[_0x41de7e(0x84f)][_0x40f0a3],_0xab9a9c[_0x41de7e(0x9be)](),applySceneState();},_0xab9a9c[_0xf92ab0(0xa3d)]=function(_0x3d0468){var _0x368636=_0xf92ab0;try{if(!_0xab9a9c||!_0xab9a9c[_0x368636(0x7d3)]||!_0x3d0468||!_0x3d0468[_0x368636(0x55f)])return![];let _0x55e670=Array[_0x368636(0x5b1)](_0xab9a9c[_0x368636(0x7d3)][_0x368636(0xae4)])?_0xab9a9c[_0x368636(0x7d3)][_0x368636(0xae4)][_0x368636(0x912)]():[];const _0x2d7517=[],_0x190a3a=[];for(const _0x3fd791 of _0x55e670){let _0x530ab1=_0x3fd791&&(_0x3fd791['urls']||_0x3fd791['url']||[]);typeof _0x530ab1===_0x368636(0x9cb)&&(_0x530ab1=[_0x530ab1]);const _0x283a9a=Array['isArray'](_0x530ab1)&&_0x530ab1[_0x368636(0x238)](_0xda224d=>typeof _0xda224d===_0x368636(0x9cb)&&(_0xda224d[_0x368636(0x602)](_0x368636(0x37e))||_0xda224d[_0x368636(0x602)](_0x368636(0xb7a))));_0x283a9a?_0x190a3a[_0x368636(0x5e7)](_0x3fd791):_0x2d7517['push'](_0x3fd791);}_0x190a3a['length']>0x1&&_0x190a3a[_0x368636(0x5e7)](_0x190a3a[_0x368636(0xa17)]());const _0x16811d=_0x2d7517[_0x368636(0x4a6)](_0x190a3a);try{_0x3d0468['setConfiguration']({'iceServers':_0x16811d,'sdpSemantics':_0xab9a9c[_0x368636(0x7d3)]['sdpSemantics']});}catch(_0x1cfea8){}return _0xab9a9c[_0x368636(0x7d3)][_0x368636(0xae4)]=_0x16811d,warnlog(_0x368636(0x393)),!![];}catch(_0xf6daf8){return errorlog(_0xf6daf8),![];}},_0xab9a9c[_0xf92ab0(0x1fb)]={},_0xab9a9c[_0xf92ab0(0x35f)]=function(_0x1ab0cc,_0x320bfc=![],_0x47b85a=![]){var _0x24459d=_0xf92ab0;_0x2c6ff3(_0x1ab0cc);if(!(_0x1ab0cc in _0xab9a9c['rpcs'])){log(_0x24459d(0xc3a));try{var _0x2ab6cf=document['getElementById']('container_'+_0x1ab0cc);if(_0x2ab6cf){warnlog('Removing\x20orphaned\x20control\x20box\x20for\x20UUID:\x20'+_0x1ab0cc);var _0x40b0ba=_0x2ab6cf['querySelector']('[data-sid]'),_0x7d57d6=_0x40b0ba?_0x40b0ba[_0x24459d(0xa18)]['sid']:null;_0x2ab6cf[_0x24459d(0x3d1)][_0x24459d(0x5be)](_0x2ab6cf),updateLockedElements();if(_0x7d57d6&&_0xab9a9c[_0x24459d(0x827)]&&typeof syncDirectorState===_0x24459d(0x32c))try{syncDirectorState({'dataset':{'sid':_0x7d57d6}});}catch(_0x162951){warnlog(_0x162951);}}var _0x195a4a=document['getElementById'](_0x24459d(0x66f)+_0x1ab0cc+_0x24459d(0x6f3));_0x195a4a&&(warnlog('Removing\x20orphaned\x20screen\x20container\x20for\x20UUID:\x20'+_0x1ab0cc),_0x195a4a[_0x24459d(0x3d1)][_0x24459d(0x5be)](_0x195a4a));}catch(_0x4e8f41){warnlog(_0x4e8f41);}return![];}warnlog(_0x24459d(0x35f)),clearInterval(_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x808)]),clearTimeout(_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x710)]);if(_0xab9a9c[_0x24459d(0x365)]&&_0xab9a9c[_0x24459d(0x365)]['includes'](_0x1ab0cc))try{_0xab9a9c[_0x24459d(0x365)][_0x24459d(0x954)](_0xab9a9c[_0x24459d(0x365)]['indexOf'](_0x1ab0cc),0x1),_0xab9a9c[_0x24459d(0x9be)](![]);}catch(_0x2b69fc){}if(_0xab9a9c[_0x24459d(0xa5b)]&&_0x47b85a)_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc]['stashed']=!![];else{if(_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x89e)]&&!_0x320bfc)return!![];else{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x89e)]=![];try{_0xab9a9c[_0x24459d(0x936)]({'bye':!![]},_0x1ab0cc),warnlog('SEND\x20BYE');}catch(_0x3f1624){}}}try{var _0x5d0e80=_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc]['streamID'];}catch(_0x3b2d00){}try{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc]['close']();}catch(_0x39b195){warnlog(_0x24459d(0x826));}if(_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc]['stashed'])return!![];_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc]['motionDetectionInterval']&&clearInterval(_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0xb96)]);try{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0xca7)]&&_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0xca7)][_0x24459d(0xb93)]()[_0x24459d(0x306)](function(_0x78f762){var _0x11d265=_0x24459d;_0x78f762[_0x11d265(0x638)](),log(_0x11d265(0x53b));});}catch(_0x4ea36d){}if(_0xab9a9c[_0x24459d(0x827)])try{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x7eb)]&&'recorder'in _0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x7eb)]&&_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc]['videoElement']['recorder'][_0x24459d(0x638)]();}catch(_0x444159){warnlog(_0x444159);}else!_0xab9a9c['roomid']&&(_0xab9a9c['beepToNotify']&&playtone(![],_0x24459d(0x739)));try{document[_0x24459d(0x2b2)](_0x24459d(0x66f)+_0x1ab0cc)&&(!_0xab9a9c['syncState']&&(_0xab9a9c[_0x24459d(0x39f)]={}),_0x5d0e80&&(_0xab9a9c['syncState'][_0x5d0e80]=getDetailedState(_0x5d0e80)),getById(_0x24459d(0x66f)+_0x1ab0cc)[_0x24459d(0x3d1)]['removeChild'](getById('container_'+_0x1ab0cc)),updateLockedElements());}catch(_0x1b0d70){warnlog(_0x1b0d70);}try{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x7eb)]&&_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x7eb)][_0x24459d(0x50f)]();}catch(_0x449907){}try{if(_0xab9a9c[_0x24459d(0x4e2)]!==![]){if(_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x7ca)]){try{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x7ca)][_0x24459d(0x50f)]();}catch(_0x3f4d57){errorlog(_0x3f4d57);}_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x7ca)]['remove']();}}}catch(_0x30bc1a){}try{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x6f9)]&&_0xab9a9c['rpcs'][_0x1ab0cc][_0x24459d(0x6f9)][_0x24459d(0x50f)]();}catch(_0x25d71a){}try{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0xc9a)]&&_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0xc9a)]['remove']();}catch(_0x5de194){}_0x24459d(0x9ef)in _0xab9a9c['rpcs'][_0x1ab0cc]&&clearInterval(_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x9ef)]);pokeIframeAPI(_0x24459d(0x6b5),![],_0x1ab0cc),pokeAPI('endViewConnection',_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0xa96)]);if(_0xab9a9c[_0x24459d(0x67e)])try{pokeDiscord(_0x24459d(0x4ec),{'streamID':_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0xa96)],'label':_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x5e0)],'session':_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x94a)],'startTime':_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x334)],'hangup':_0x320bfc});}catch(_0x18f685){console[_0x24459d(0x2ea)](_0x18f685);}_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc][_0x24459d(0x625)]&&(_0x5d0e80=![]);if(_0xab9a9c[_0x24459d(0x85c)])try{onGuestLeftMixMinus(_0x1ab0cc);}catch(_0x251880){}try{_0xab9a9c[_0x24459d(0x404)][_0x1ab0cc]=null,delete _0xab9a9c[_0x24459d(0x404)][_0x1ab0cc];}catch(_0x4a6c31){}try{_0xab9a9c[_0x24459d(0x35f)](_0x1ab0cc+_0x24459d(0x6f3));}catch(_0x5a14bc){}(!_0xab9a9c['director']||_0xab9a9c[_0x24459d(0xa5d)])&&setTimeout(function(){updateMixer();},0x1);if(typeof _0x5d0e80=='undefined')return![];try{warnlog(_0x24459d(0x556)),_0x5d0e80&&(_0x5d0e80 in _0xab9a9c['watchTimeoutList']&&(log(_0x24459d(0x7f4)+_0x5d0e80),clearTimeout(_0xab9a9c[_0x24459d(0x608)][_0x5d0e80]),delete _0xab9a9c[_0x24459d(0x608)][_0x5d0e80]),_0xab9a9c['watchTimeoutList'][_0x5d0e80]=setTimeout(function(_0x5d714b){var _0x577e8f=_0x24459d;try{delete _0xab9a9c[_0x577e8f(0x608)][_0x5d714b];}catch(_0x5e02de){return warnlog(_0x577e8f(0x3dd)),![];}log('watchTimeoutList2:'+_0x5d714b);try{for(var _0x555454 in _0xab9a9c[_0x577e8f(0x404)]){if(_0xab9a9c['rpcs'][_0x555454][_0x577e8f(0xa96)]===_0x5d714b){if(_0xab9a9c[_0x577e8f(0x404)][_0x555454][_0x577e8f(0xa83)]==='connected')return warnlog('\x20---\x20we\x20will\x20not\x20ask\x20again;\x20we\x27re\x20already\x20connected'),![];}}}catch(_0x226eb2){errorlog(_0x226eb2);}warnlog(_0x577e8f(0x530)),_0xab9a9c['watchStream'](_0x5d714b);},_0xab9a9c['retryTimeout'],_0x5d0e80));}catch(_0x14c4ab){errorlog(_0x14c4ab);}pokeIframeAPI(_0x24459d(0x2b9),![],_0x1ab0cc);_0x5d0e80!==null?pokeIframeAPI(_0x24459d(0x7fe),_0x5d0e80,_0x1ab0cc):pokeIframeAPI(_0x24459d(0x7fe),!![],_0x1ab0cc);try{closeModal(![],_0x24459d(0x64d)+_0x1ab0cc);}catch(_0x249d6c){}return updateUserList(),![];},_0xab9a9c[_0xf92ab0(0xb86)]=null,_0xab9a9c[_0xf92ab0(0x82e)]=function(){var _0x4b01c7=_0xf92ab0,_0x22d399=![];if(_0xab9a9c[_0x4b01c7(0x7c7)]){_0xab9a9c[_0x4b01c7(0x933)]&&clearTimeout(_0xab9a9c[_0x4b01c7(0xb86)]);if(_0xab9a9c['ws']===null||typeof _0xab9a9c['ws']!=='object'||_0xab9a9c['ws']['readyState']!==0x1){}else{var _0x16a030=_0xab9a9c[_0x4b01c7(0x7c7)]['split'](',');for(var _0x24c7f6 in _0x16a030){if(_0x16a030[_0x24c7f6]){var _0x316a6f=![];for(var _0x10ca1f in _0xab9a9c['rpcs']){if(_0xab9a9c['rpcs'][_0x10ca1f][_0x4b01c7(0xa96)]&&_0xab9a9c[_0x4b01c7(0x404)][_0x10ca1f]['streamID']===_0x16a030[_0x24c7f6]){_0x316a6f=!![];break;}}_0x16a030[_0x24c7f6]in _0xab9a9c[_0x4b01c7(0x608)]&&(_0x316a6f=!![]);if(_0x316a6f)continue;_0xab9a9c[_0x4b01c7(0x932)](_0x16a030[_0x24c7f6]),_0x22d399=!![];}}}_0xab9a9c[_0x4b01c7(0x933)]&&_0xab9a9c['forceRetry']<0xa&&(_0xab9a9c[_0x4b01c7(0x933)]=0xa),_0xab9a9c[_0x4b01c7(0x933)]&&(_0xab9a9c[_0x4b01c7(0xb86)]=setTimeout(function(){var _0xf03513=_0x4b01c7;log('retrying\x20at\x20an\x20interval'),_0xab9a9c[_0xf03513(0x82e)]();},_0xab9a9c['forceRetry']*0x3e8));}return _0x22d399;},_0xab9a9c['offerSDP']=async function(_0x2b7d37){var _0x27dbee=_0xf92ab0;if(_0x2b7d37 in _0xab9a9c[_0x27dbee(0x84f)]){if(_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa83)]===_0x27dbee(0xa40)||_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa83)]===_0x27dbee(0x5e8))log('closing\x206'),_0xab9a9c[_0x27dbee(0x219)](_0x2b7d37),warnlog(_0x27dbee(0x73c));else{if(iPad||iOS)log(_0x27dbee(0x345)),_0xab9a9c[_0x27dbee(0x219)](_0x2b7d37),warnlog('cleaning\x20up\x20lost\x20connection\x20--\x20disconnected\x20-\x20iOS\x20specific');else{if(_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa83)]!==_0x27dbee(0x497)){await sleep(0x3e8);if(_0xab9a9c['pcs'][_0x2b7d37]){if(_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa83)]!==_0x27dbee(0x497))log(_0x27dbee(0x4c9)),_0xab9a9c['closePC'](_0x2b7d37),warnlog(_0x27dbee(0x73c));else{warnlog(_0x27dbee(0xc91)+_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa83)]);return;}}}else{warnlog(_0x27dbee(0xc91)+_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa83)]);return;}}}}else log(_0x27dbee(0x789));if(_0xab9a9c[_0x27dbee(0x89b)]!==![]){if(Object[_0x27dbee(0x7db)](_0xab9a9c[_0x27dbee(0x84f)])[_0x27dbee(0xade)]>_0xab9a9c[_0x27dbee(0x89b)]){log(_0x27dbee(0x7e3)),log(_0x27dbee(0x92e)),_0xab9a9c[_0x27dbee(0x219)](_0x2b7d37);return;}}else{if(_0xab9a9c['maxconnections']!==![]){if(Object[_0x27dbee(0x7db)](_0xab9a9c[_0x27dbee(0x404)])[_0x27dbee(0xade)]+Object[_0x27dbee(0x7db)](_0xab9a9c['pcs'])[_0x27dbee(0xade)]>_0xab9a9c[_0x27dbee(0xb6c)]){log(_0x27dbee(0x449)),log(_0x27dbee(0x7a7)),_0xab9a9c[_0x27dbee(0x219)](_0x2b7d37);return;}}}!_0xab9a9c[_0x27dbee(0x7d3)]&&await chooseBestTURN();_0xab9a9c[_0x27dbee(0x288)]&&(_0xab9a9c[_0x27dbee(0x7d3)][_0x27dbee(0x288)]=!![]);_0xab9a9c['bundlePolicy']&&(_0xab9a9c[_0x27dbee(0x7d3)][_0x27dbee(0x8a9)]=_0xab9a9c[_0x27dbee(0x8a9)]);try{_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]=new RTCPeerConnection(_0xab9a9c[_0x27dbee(0x7d3)]);try{_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xb14)]=_0x42e614=>{warnlog(_0x42e614);};}catch(_0x262a7f){warnlog(_0x262a7f);}}catch(_0x1dbb7b){!_0xab9a9c['cleanOutput']&&warnUser(_0x27dbee(0xc40));errorlog(_0x1dbb7b);return;}if(_0xab9a9c[_0x27dbee(0x915)]){if(Object[_0x27dbee(0x7db)](_0xab9a9c['pcs'])[_0x27dbee(0xade)]>0x1){log('closing\x203'),log(_0x27dbee(0x7be)),_0xab9a9c[_0x27dbee(0x219)](_0x2b7d37);return;}}_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x287)]={},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x94a)]=_0xab9a9c[_0x27dbee(0xc19)]+_0xab9a9c[_0x27dbee(0x54d)](0x5),_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x71b)]=null,_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0x8b5)]=null,_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0x444)]={},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x444)][_0x27dbee(0x569)]=null,_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0x444)][_0x27dbee(0x4ea)]=null,_0xab9a9c['pcs'][_0x2b7d37]['obsState'][_0x27dbee(0xb68)]=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x444)][_0x27dbee(0x362)]=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x444)][_0x27dbee(0xbcf)]=null,_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0xbfc)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x402)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['solo']=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['layout']=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x6b7)]=![],_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0xaf0)]=null,_0xab9a9c['pcs'][_0x2b7d37]['maxBandwidth']=null,_0xab9a9c['pcs'][_0x2b7d37]['audioMutedOverride']=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x7ef)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x352)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['setBitrate']=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa62)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x1ab)]=![],_0xab9a9c['pcs'][_0x2b7d37]['limitAudio']=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['enhanceAudio']=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x48d)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x948)]=null,_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0xcb7)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x295)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x8c0)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x9e1)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['allowIframe']=![],_0xab9a9c['pcs'][_0x2b7d37]['allowWidget']=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xae2)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xb2b)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x918)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xc38)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x25d)]=![],_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0x61e)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xb3f)]=![],_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0xbe2)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['whipout']=null,_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0x829)]=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['screenWhepAllowed']=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xab9)]=_0x2b7d37,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa53)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x495)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x8d1)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x39a)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x447)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['scaleSnap']=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xb0c)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xa05)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x990)]=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x268)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x85e)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['keyframeTimeout']=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x5e0)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['order']=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['preferVideoCodec']=![],_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0x3c8)]=![],_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0x808)]=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x649)]=_0xab9a9c[_0x27dbee(0x649)],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x1d9)]=![],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x334)]=Date[_0x27dbee(0x30e)](),_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37]['needsPublishing']=null;function _0x2c8998(_0x1851b9=![]){var _0x2ac338=_0x27dbee;if(_0x1851b9)return;_0xab9a9c[_0x2ac338(0x84f)][_0x2b7d37][_0x2ac338(0x5cc)]=_0xab9a9c[_0x2ac338(0x84f)][_0x2b7d37][_0x2ac338(0x6e9)](_0x2ac338(0x5cc)),_0xab9a9c[_0x2ac338(0x84f)][_0x2b7d37][_0x2ac338(0x5cc)]['UUID']=_0x2b7d37,_0xab9a9c['pcs'][_0x2b7d37][_0x2ac338(0x5cc)][_0x2ac338(0x636)]=_0x1b5485=>{var _0x1acb2f=_0x2ac338;_0x1b5485[_0x1acb2f(0xca1)]&&_0x1b5485[_0x1acb2f(0xca1)][_0x1acb2f(0x95d)]&&_0x1b5485[_0x1acb2f(0xca1)][_0x1acb2f(0x95d)]!==0xc&&warnlog(_0x1b5485),log(_0x1acb2f(0xbc2)+_0x2b7d37);},_0xab9a9c[_0x2ac338(0x84f)][_0x2b7d37]['sendChannel']['onopen']=()=>{var _0x3bee8a=_0x2ac338;if(_0x1851b9)return;_0xab9a9c[_0x3bee8a(0x84f)][_0x2b7d37][_0x3bee8a(0x9f8)]=0x0,log(_0x3bee8a(0xc06)),msg={},msg[_0x3bee8a(0xa82)]={},msg[_0x3bee8a(0xa82)][_0x3bee8a(0x5e0)]=_0xab9a9c[_0x3bee8a(0x5e0)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0xab7)]=_0xab9a9c[_0x3bee8a(0xab7)],msg[_0x3bee8a(0xa82)]['order']=_0xab9a9c[_0x3bee8a(0x9ae)],msg[_0x3bee8a(0xa82)]['muted']=_0xab9a9c['muted'],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x8a2)]=_0xab9a9c[_0x3bee8a(0x9fc)];_0xab9a9c[_0x3bee8a(0xa4c)]&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0xa4c)]=_0xab9a9c[_0x3bee8a(0xa4c)]);_0xab9a9c[_0x3bee8a(0x1a6)]&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0xc71)]=!![],msg[_0x3bee8a(0xa82)]['tipId']=_0xab9a9c[_0x3bee8a(0x1a7)]||_0xab9a9c[_0x3bee8a(0xa96)],msg['info'][_0x3bee8a(0x21d)]=_0xab9a9c[_0x3bee8a(0x21d)],msg['info'][_0x3bee8a(0x3cf)]=_0xab9a9c[_0x3bee8a(0x3cf)],msg[_0x3bee8a(0xa82)]['tipCurrency']=_0xab9a9c[_0x3bee8a(0xba3)]);try{(_0xab9a9c['group'][_0x3bee8a(0xade)]||_0xab9a9c[_0x3bee8a(0xbb5)])&&(msg[_0x3bee8a(0xa82)]['initial_group']=_0xab9a9c['group'][_0x3bee8a(0x1e2)](','));}catch(_0x555dc1){}msg[_0x3bee8a(0xa82)][_0x3bee8a(0x50b)]=_0xab9a9c[_0x3bee8a(0x50b)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x1ee)]=_0xab9a9c[_0x3bee8a(0x1ee)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x8b4)]=_0xab9a9c[_0x3bee8a(0x8b4)],msg[_0x3bee8a(0xa82)]['directorMirror']=_0xab9a9c['permaMirrored'],msg['info']['directorFlip']=_0xab9a9c[_0x3bee8a(0x420)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x22c)]=_0xab9a9c[_0x3bee8a(0x6cc)];_0xab9a9c[_0x3bee8a(0x70d)]?msg[_0x3bee8a(0xa82)]['room_init']=!![]:msg[_0x3bee8a(0xa82)][_0x3bee8a(0xbe8)]=![];msg[_0x3bee8a(0xa82)][_0x3bee8a(0x51b)]=_0xab9a9c[_0x3bee8a(0x411)];_0xab9a9c['whipOutput']&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0x52b)]=!![]);_0xab9a9c['requestscenes']&&(msg['info']['requestScenes']=_0xab9a9c['requestscenes']);if(_0xab9a9c['director']){if(!_0xab9a9c['mainDirectorPassword']&&_0xab9a9c[_0x3bee8a(0xb38)]&&_0xab9a9c[_0x3bee8a(0xb38)]===_0x2b7d37)_0xab9a9c['newMainDirectorSetup']();else{msg[_0x3bee8a(0xc90)]={};_0xab9a9c['mainDirectorPassword']&&(msg[_0x3bee8a(0xc90)]['tokenDirector']=!![]);msg['directorSettings']['totalRoomBitrate']=_0xab9a9c['totalRoomBitrate'];_0xab9a9c[_0x3bee8a(0x365)]['length']&&!_0xab9a9c[_0x3bee8a(0x365)]['includes'](_0x2b7d37)&&(msg['info'][_0x3bee8a(0x8b2)]=!![]);var _0x449f8d=[];for(var _0x59198b in _0xab9a9c[_0x3bee8a(0x84f)]){_0xab9a9c[_0x3bee8a(0x84f)][_0x59198b]['coDirector']===!![]&&_0x449f8d[_0x3bee8a(0x5e7)](_0x59198b);}_0xab9a9c[_0x3bee8a(0x2ce)]&&(msg[_0x3bee8a(0xc90)][_0x3bee8a(0x73b)]=!![]),_0x449f8d['length']&&(msg[_0x3bee8a(0xc90)]['addCoDirector']=_0x449f8d);}_0xab9a9c[_0x3bee8a(0x2c9)]&&(msg[_0x3bee8a(0xa82)]['autoSync']=_0xab9a9c[_0x3bee8a(0x2c9)]);}_0xab9a9c[_0x3bee8a(0x4e2)]!==![]?msg['info']['broadcast_mode']=!![]:msg[_0x3bee8a(0xa82)]['broadcast_mode']=![];_0xab9a9c[_0x3bee8a(0x1d9)]?msg[_0x3bee8a(0xa82)][_0x3bee8a(0x1d9)]=!![]:msg['info'][_0x3bee8a(0x1d9)]=![];_0xab9a9c[_0x3bee8a(0x9e1)]?msg['info'][_0x3bee8a(0xb51)]=!![]:msg[_0x3bee8a(0xa82)]['allowdrawing']=![];if(_0xab9a9c[_0x3bee8a(0xbdf)])msg['info'][_0x3bee8a(0xa61)]=_0xab9a9c[_0x3bee8a(0xbdf)];else{if(_0xab9a9c[_0x3bee8a(0xbdf)]===![])msg[_0x3bee8a(0xa82)][_0x3bee8a(0xa61)]=![];else _0xab9a9c[_0x3bee8a(0x70d)]&&!_0xab9a9c[_0x3bee8a(0x827)]?msg[_0x3bee8a(0xa82)][_0x3bee8a(0xa61)]=![]:msg['info'][_0x3bee8a(0xa61)]=null;}_0xab9a9c[_0x3bee8a(0x858)]&&(msg['info'][_0x3bee8a(0x858)]=!![]);msg[_0x3bee8a(0xa82)]['screenshare_url']=_0xab9a9c['screenshare'];!_0xab9a9c[_0x3bee8a(0x55e)]&&(msg[_0x3bee8a(0xa82)]['smallScreen']=!![]);_0xab9a9c['notifyScreenShare']?msg[_0x3bee8a(0xa82)][_0x3bee8a(0xc15)]=!!_0xab9a9c['screenShareState']:msg[_0x3bee8a(0xa82)][_0x3bee8a(0xc15)]=![];msg[_0x3bee8a(0xa82)]['width_url']=_0xab9a9c[_0x3bee8a(0x28e)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x721)]=_0xab9a9c['height'];try{if(_0xab9a9c[_0x3bee8a(0xca7)]){let _0x2d4ebc=_0xab9a9c[_0x3bee8a(0xca7)][_0x3bee8a(0x5c7)]();if(_0x2d4ebc[_0x3bee8a(0xade)]){let _0x57699f=_0x2d4ebc[0x0]['getSettings']();msg['info'][_0x3bee8a(0xcb6)]=_0x57699f[_0x3bee8a(0x28e)]||![],msg['info'][_0x3bee8a(0x7f9)]=_0x57699f[_0x3bee8a(0x43f)]||![],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x4ae)]=parseInt(_0x57699f['frameRate'])||![];}}if(_0xab9a9c['screenStream']&&_0xab9a9c[_0x3bee8a(0x32d)]['srcObject']){let _0x465ec4=_0xab9a9c[_0x3bee8a(0x32d)]['srcObject'][_0x3bee8a(0x5c7)]();if(_0x465ec4[_0x3bee8a(0xade)]){let _0x18c269=_0x465ec4[0x0][_0x3bee8a(0x498)]();msg[_0x3bee8a(0xa82)][_0x3bee8a(0x5e1)]=_0x18c269[_0x3bee8a(0x28e)]||![],msg['info']['video_2_init_height']=_0x18c269[_0x3bee8a(0x43f)]||![],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x953)]=parseInt(_0x18c269[_0x3bee8a(0x453)])||![];}}}catch(_0x4945f1){errorlog(_0x4945f1);}(_0xab9a9c['midiIn']||_0xab9a9c[_0x3bee8a(0xa7a)])&&(msg['info']['midi_url']=!![]);msg['info'][_0x3bee8a(0x9f0)]=_0xab9a9c[_0x3bee8a(0xc5c)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0xa2f)]=_0xab9a9c[_0x3bee8a(0x504)],msg['info'][_0x3bee8a(0x204)]=_0xab9a9c[_0x3bee8a(0x89b)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x99f)]=_0xab9a9c[_0x3bee8a(0x839)],msg['info'][_0x3bee8a(0xbd9)]=_0xab9a9c['echoCancellation'],msg[_0x3bee8a(0xa82)]['agc_url']=_0xab9a9c[_0x3bee8a(0x765)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0xb65)]=_0xab9a9c[_0x3bee8a(0xc8e)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x52a)]=_0xab9a9c[_0x3bee8a(0xc11)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0xaf2)]=_0xab9a9c['version'],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x1d7)]=_0xab9a9c['audioGain'],msg[_0x3bee8a(0xa82)][_0x3bee8a(0xbc1)]=_0xab9a9c[_0x3bee8a(0x72e)],msg['info'][_0x3bee8a(0x7de)]=_0xab9a9c['micDelay'],msg[_0x3bee8a(0xa82)]['recording_audio_ctx_latency']=_0xab9a9c[_0x3bee8a(0x9e6)],msg[_0x3bee8a(0xa82)]['recording_audio_pipeline']=!_0xab9a9c[_0x3bee8a(0x232)],msg['info'][_0x3bee8a(0x91a)]=_0xab9a9c[_0x3bee8a(0x96b)],msg[_0x3bee8a(0xa82)][_0x3bee8a(0x7b2)]=_0xab9a9c[_0x3bee8a(0xa5c)],msg['info']['playback_audio_volume_meter']=_0xab9a9c[_0x3bee8a(0xaa7)];_0xab9a9c['pseudoguest']&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0x8d5)]=_0xab9a9c[_0x3bee8a(0x8d5)]);_0xab9a9c[_0x3bee8a(0x287)][_0x3bee8a(0x8a0)]&&(msg[_0x3bee8a(0xa82)]['conn_type']=_0xab9a9c['stats']['network_type']);_0xab9a9c['forceRotate']!==![]?_0xab9a9c['rotate']?msg[_0x3bee8a(0xa82)][_0x3bee8a(0x353)]=_0xab9a9c[_0x3bee8a(0x7e5)]+parseInt(_0xab9a9c[_0x3bee8a(0x359)]):msg[_0x3bee8a(0xa82)]['rotate_video']=_0xab9a9c[_0x3bee8a(0x7e5)]:msg[_0x3bee8a(0xa82)][_0x3bee8a(0x353)]=_0xab9a9c[_0x3bee8a(0x359)];msg[_0x3bee8a(0xa82)][_0x3bee8a(0x353)]&&msg[_0x3bee8a(0xa82)]['rotate_video']>=0x168&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0x353)]-=0x168);try{navigator&&navigator[_0x3bee8a(0xb01)]&&(msg['info']['useragent']=navigator[_0x3bee8a(0xb01)]);navigator&&navigator['platform']&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0xb85)]=navigator[_0x3bee8a(0xb85)]);gpgpuSupport&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0xaee)]=gpgpuSupport);cpuSupport&&(msg['info']['CPU']=cpuSupport);iOS&&(msg['info']['iPhone12Up']=iPhone12Up);if(SafariVersion)msg[_0x3bee8a(0xa82)]['Browser']=_0x3bee8a(0x986)+SafariVersion;else{if(getChromiumVersion()>0x3c)msg['info'][_0x3bee8a(0xb91)]=_0x3bee8a(0x5fe)+getChromiumVersion();else{if(Firefox)msg[_0x3bee8a(0xa82)][_0x3bee8a(0xb91)]=_0x3bee8a(0x81b);else navigator[_0x3bee8a(0xb01)]['indexOf']('CriOS')>=0x0?msg[_0x3bee8a(0xa82)][_0x3bee8a(0xb91)]=_0x3bee8a(0xaf5):msg[_0x3bee8a(0xa82)][_0x3bee8a(0xb91)]=_0x3bee8a(0xbb3);}}}catch(_0x1b6b59){}_0xab9a9c[_0x3bee8a(0x577)]&&(_0x3bee8a(0x9b1)in _0xab9a9c[_0x3bee8a(0x577)]&&(typeof _0xab9a9c[_0x3bee8a(0x577)][_0x3bee8a(0x9b1)]==_0x3bee8a(0xab6)?msg[_0x3bee8a(0xa82)][_0x3bee8a(0x481)]=parseInt(_0xab9a9c[_0x3bee8a(0x577)]['level']*0x64):msg[_0x3bee8a(0xa82)][_0x3bee8a(0x481)]=_0xab9a9c['batteryState']['level']),'charging'in _0xab9a9c['batteryState']&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0x29c)]=_0xab9a9c[_0x3bee8a(0x577)][_0x3bee8a(0x8bb)]));if(_0xab9a9c[_0x3bee8a(0x827)]&&_0xab9a9c[_0x3bee8a(0x436)]){if(_0xab9a9c[_0x3bee8a(0x249)]&&_0xab9a9c[_0x3bee8a(0x249)]>0x0)msg['setClock']=_0xab9a9c[_0x3bee8a(0x249)]-Date[_0x3bee8a(0x30e)]()/0x3e8,msg[_0x3bee8a(0x586)]=!![],msg[_0x3bee8a(0xab8)]=!![];else _0xab9a9c['roomTimer']&&_0xab9a9c[_0x3bee8a(0x249)]<0x0&&(msg[_0x3bee8a(0x2d8)]=_0xab9a9c['roomTimer']*-0x1,msg[_0x3bee8a(0x586)]=!![],msg[_0x3bee8a(0xab8)]=!![],msg['pauseClock']=!![]);_0xab9a9c['showRoomTime']&&(msg[_0x3bee8a(0xa0f)]=!![]);}_0xab9a9c['cpuLimited']&&(msg[_0x3bee8a(0xa82)][_0x3bee8a(0xb1f)]=_0xab9a9c[_0x3bee8a(0xb1f)]);try{_0xab9a9c[_0x3bee8a(0xa82)][_0x3bee8a(0x775)]&&(msg['miniInfo']={},msg[_0x3bee8a(0x964)]['out']={},msg[_0x3bee8a(0x964)][_0x3bee8a(0x775)]['c']=_0xab9a9c[_0x3bee8a(0xa82)][_0x3bee8a(0x775)]['c']);}catch(_0x47cdcd){}_0xab9a9c[_0x3bee8a(0x1eb)](msg,_0x2b7d37),pokeIframeAPI(_0x3bee8a(0xcb4),!![],_0x2b7d37),pokeIframeAPI(_0x3bee8a(0x435),!![],_0x2b7d37),updateUserList();},_0xab9a9c[_0x2ac338(0x84f)][_0x2b7d37][_0x2ac338(0x5cc)][_0x2ac338(0x38c)]=()=>{var _0x4087e3=_0x2ac338;pokeIframeAPI(_0x4087e3(0xcb4),![],_0x2b7d37),_0xab9a9c[_0x4087e3(0xb5e)](),warnlog('send\x20channel\x20closed');return;},_0xab9a9c['handlePublisherMessage']=async function(_0x100b5a,_0x477a18){var _0xdc6a3f=_0x2ac338;log('received\x20data\x20from\x20viewer');try{var _0x4676c4=JSON[_0xdc6a3f(0xa4b)](_0x100b5a['data']);}catch(_0x42ef64){warnlog('Couldn\x27t\x20parse\x20JSON;\x20will\x20attempt\x20as\x20ArrayBuffer\x20UINT8ARRAY'),log(_0x100b5a[_0xdc6a3f(0x67b)]);try{var _0x3843c4=new TextDecoder()['decode'](_0x100b5a['data']),_0x4676c4=JSON[_0xdc6a3f(0xa4b)](_0x3843c4);}catch(_0x3e0eae){try{var _0x4676c4=await new Response(_0x100b5a['data'])[_0xdc6a3f(0x525)]();_0x4676c4=JSON[_0xdc6a3f(0xa4b)](_0x4676c4);}catch(_0x2bdd7a){return;}}}log(_0x4676c4);if(_0xdc6a3f(0x1d9)in _0x4676c4)try{_0x4676c4=await _0xab9a9c[_0xdc6a3f(0x476)](_0x4676c4);if(!_0x4676c4)return;}catch(_0x1f4b50){errorlog(_0x1f4b50);}if(_0x4676c4[_0xdc6a3f(0x65c)]||_0x4676c4['mid']){let _0x3d69c0=_0x4676c4[_0xdc6a3f(0x65c)]||_0x4676c4['mid'];if(_0xab9a9c[_0xdc6a3f(0xc76)][_0x477a18]){if(_0xab9a9c[_0xdc6a3f(0xc76)][_0x477a18][_0xdc6a3f(0xa4f)](_0x3d69c0))return;else _0xab9a9c[_0xdc6a3f(0xc76)][_0x477a18][_0xdc6a3f(0x5e7)](_0x3d69c0);}else _0xab9a9c[_0xdc6a3f(0xc76)][_0x477a18]=[_0x3d69c0];}_0xdc6a3f(0xaf4)in _0x4676c4?await _0xab9a9c[_0xdc6a3f(0x539)](_0x4676c4,_0x477a18+_0xdc6a3f(0x6f3),_0x477a18):await _0xab9a9c[_0xdc6a3f(0x539)](_0x4676c4,_0x477a18);};}!_0xab9a9c[_0x27dbee(0x570)]&&_0x2c8998(![]);_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xce7)]=_0x11e54f=>{var _0x493d44=_0x27dbee;warnlog('data\x20channel\x20being\x20used\x20in\x20reverse;\x20this\x20shouldn\x27t\x20really\x20happen,\x20except\x20if\x20maybe\x20doing\x20a\x20file\x20transfer'),warnlog(_0x11e54f);if(_0x11e54f[_0x493d44(0xc8d)][_0x493d44(0x5e0)]&&_0x11e54f[_0x493d44(0xc8d)][_0x493d44(0x5e0)]!=='sendChannel'){_0xab9a9c['recieveFile'](_0xab9a9c['rpcs'],_0x2b7d37,_0x11e54f[_0x493d44(0xc8d)]);return;}},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xb9a)]=_0x3b08f6=>{var _0x233456=_0x27dbee;log(_0x233456(0x665));try{_0xab9a9c[_0x233456(0x729)](_0x2b7d37);}catch(_0x5250c3){warnlog(_0x5250c3);}},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x45e)]=_0x29f513=>{var _0x161a9b=_0x27dbee;errorlog(_0x161a9b(0xa31));},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0xbaa)]=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x397)]=[],_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x9f8)]=0xa,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x6dd)]=null,_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x7c1)]=_0x589b63=>{var _0x72e510=_0x27dbee;if(_0x589b63[_0x72e510(0x4f8)]==null){log(_0x72e510(0x4ce));_0xab9a9c[_0x72e510(0xaf1)]&&_0xab9a9c[_0x72e510(0x84f)][_0x2b7d37][_0x72e510(0x6dd)]&&(_0xab9a9c['pcs'][_0x2b7d37][_0x72e510(0x6dd)][_0x72e510(0x30f)](),_0xab9a9c['pcs'][_0x2b7d37][_0x72e510(0x6dd)]=![]);return;}else{if(_0xab9a9c[_0x72e510(0xaf1)]&&_0xab9a9c[_0x72e510(0x84f)][_0x2b7d37][_0x72e510(0x6dd)])return;}log(_0x589b63);try{if(_0xab9a9c[_0x72e510(0x2e3)]){if(_0x589b63['candidate'][_0x72e510(0x4f8)][_0x72e510(0x565)](_0xab9a9c[_0x72e510(0x2e3)])===-0x1){log('dropped\x20candidate\x20due\x20to\x20filter');return;}else log(_0x589b63['candidate']);}}catch(_0x2cc044){errorlog(_0x2cc044);}try{if(_0xab9a9c[_0x72e510(0x38e)]){if(!filterIceLAN(_0x589b63[_0x72e510(0x4f8)]))return;}if(_0xab9a9c[_0x72e510(0x3d4)]){if(!filterStunOnly(_0x589b63[_0x72e510(0x4f8)]))return;}}catch(_0x3ac57){errorlog(_0x3ac57);}if(_0xab9a9c[_0x72e510(0x84f)][_0x2b7d37]['iceTimer']!==null){_0xab9a9c['pcs'][_0x2b7d37]['iceBundle'][_0x72e510(0x5e7)](_0x589b63[_0x72e510(0x4f8)]);return;}_0xab9a9c[_0x72e510(0x84f)][_0x2b7d37][_0x72e510(0x397)]['push'](_0x589b63[_0x72e510(0x4f8)]),_0xab9a9c[_0x72e510(0x84f)][_0x2b7d37]['iceTimer']=setTimeout(function(_0x41428a){var _0xea102=_0x72e510;try{_0xab9a9c['pcs'][_0x41428a][_0xea102(0xbaa)]=null;}catch(_0x49a9d9){warnlog(_0xea102(0x297));return;}var _0x1971a1={};_0x1971a1[_0xea102(0xab9)]=_0x41428a,_0x1971a1[_0xea102(0x3c5)]=_0xea102(0x381);var _0xe82348=_0xab9a9c['pcs'][_0x41428a][_0xea102(0x397)];try{if(_0xab9a9c[_0xea102(0x64b)]){var _0x3a1986=filterIpv6FromCandidates(_0xe82348);_0xe82348=_0x3a1986[_0xea102(0x2a4)];}else _0xab9a9c['preferIpv4']!==![]&&(_0xe82348=reorderCandidatesIpv4First(_0xe82348));}catch(_0x3afdd7){warnlog(_0xea102(0x587),_0x3afdd7);}_0x1971a1[_0xea102(0x460)]=_0xe82348,_0x1971a1[_0xea102(0x94a)]=_0xab9a9c['pcs'][_0x41428a][_0xea102(0x94a)],_0xab9a9c[_0xea102(0x84f)][_0x41428a][_0xea102(0x397)]=[],_0xab9a9c['pcs'][_0x2b7d37][_0xea102(0x9f8)]=0x3e8,_0xab9a9c[_0xea102(0x54c)]?_0xab9a9c['encryptMessage'](JSON['stringify'](_0x1971a1['candidates']))[_0xea102(0x998)](function(_0x44835a){var _0x326980=_0xea102;_0x1971a1[_0x326980(0x460)]=_0x44835a[0x0],_0x1971a1[_0x326980(0xa85)]=_0x44835a[0x1],_0xab9a9c[_0x326980(0x874)](_0x1971a1);})[_0xea102(0xacc)](errorlog):_0xab9a9c[_0xea102(0x874)](_0x1971a1);},_0xab9a9c[_0x72e510(0x84f)][_0x2b7d37][_0x72e510(0x9f8)],_0x2b7d37);},_0xab9a9c['processPCSOnMessage']=async function(_0x3c51bb,_0x239a27,_0x4c574b=![]){var _0x722b8a=_0x27dbee;_0x3c51bb[_0x722b8a(0xab9)]=_0x239a27;if(_0x3c51bb[_0x722b8a(0x5dd)]){_0xab9a9c[_0x722b8a(0x7b1)](_0x3c51bb);return;}else{if(_0x3c51bb[_0x722b8a(0x4f8)]){log(_0x722b8a(0xc2e)),_0xab9a9c[_0x722b8a(0x842)](_0x3c51bb);return;}else{if(_0x3c51bb[_0x722b8a(0x460)]){log(_0x722b8a(0x71d)),_0xab9a9c[_0x722b8a(0x975)](_0x3c51bb);return;}else{if(_0x722b8a(0xb5e)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0xb7b)]=_0x3c51bb[_0x722b8a(0xb5e)],_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27),warnlog(_0x722b8a(0xaf3));return;}else{if(_0x722b8a(0xb7b)in _0x3c51bb){try{_0xab9a9c['pcs'][_0x239a27]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xa86)]=_0x3c51bb['pong'],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['lastPongAt']=Date['now']());}catch(_0x8c10ce){}warnlog(_0x722b8a(0x7f8));return;}else{if(_0x722b8a(0xa1d)in _0x3c51bb){warnlog('BYE'),log('closing\x2012'),_0xab9a9c[_0x722b8a(0x219)](_0x239a27);return;}else{if('iceRestartRequest'in _0x3c51bb){warnlog('Viewer\x20requested\x20ICE\x20restart\x20due\x20to\x20connection\x20failure');_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xc25)]?(log(_0x722b8a(0x1b7)+_0x239a27),_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xc25)]()):(log(_0x722b8a(0x5ad)+_0x239a27),_0xab9a9c[_0x722b8a(0x729)](_0x239a27,!![])));return;}}}}}}}if(_0xab9a9c[_0x722b8a(0x827)]){if(_0x722b8a(0xb80)in _0x3c51bb&&_0x722b8a(0xa85)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x959)])_0xab9a9c[_0x722b8a(0xc5d)]?_0xab9a9c[_0x722b8a(0x347)](_0x3c51bb[_0x722b8a(0xb80)],_0x3c51bb['vector'],_0xab9a9c[_0x722b8a(0xc5d)])[_0x722b8a(0x998)](function(_0x44ac44){var _0x3c3e41=_0x722b8a;if(_0x44ac44===_0xab9a9c[_0x3c3e41(0xc5d)]){_0xab9a9c[_0x3c3e41(0x84f)][_0x239a27][_0x3c3e41(0x352)]=!![],_0xab9a9c[_0x3c3e41(0x8a3)][_0x3c3e41(0x5e7)](_0x239a27),getById(_0x3c3e41(0x66f)+_0x239a27)['classList'][_0x3c3e41(0xada)](_0x3c3e41(0x2e2)),_0xab9a9c['announceCoDirector'](_0x239a27),_0xab9a9c['initialDirectorSync'](_0x239a27);var _0x30b987={};_0x30b987[_0x3c3e41(0xb61)]=_0x3c3e41(0xb80),_0xab9a9c[_0x3c3e41(0x1eb)](_0x30b987,_0x239a27);}else{warnlog(_0x3c3e41(0x982));var _0x30b987={};_0x30b987[_0x3c3e41(0x923)]=_0x3c3e41(0xb80),_0xab9a9c[_0x3c3e41(0x1eb)](_0x30b987,_0x239a27);}})[_0x722b8a(0xacc)](function(){var _0x12baf5=_0x722b8a;warnlog(_0x12baf5(0x66d));var _0x2acf83={};_0x2acf83[_0x12baf5(0x923)]=_0x12baf5(0xb80),_0xab9a9c[_0x12baf5(0x1eb)](_0x2acf83,_0x239a27);}):generateHash(_0xab9a9c[_0x722b8a(0x959)]+_0xab9a9c[_0x722b8a(0x7a9)]+_0x722b8a(0x9d8),0xc)[_0x722b8a(0x998)](function(_0x4d8115){var _0x442d3f=_0x722b8a;_0xab9a9c[_0x442d3f(0xc5d)]=_0x4d8115,_0xab9a9c[_0x442d3f(0x347)](_0x3c51bb['requestCoDirector'],_0x3c51bb['vector'],_0xab9a9c[_0x442d3f(0xc5d)])[_0x442d3f(0x998)](function(_0x3e8b34){var _0x5b918e=_0x442d3f;if(_0x3e8b34===_0xab9a9c['directorHash']){_0xab9a9c[_0x5b918e(0x84f)][_0x239a27][_0x5b918e(0x352)]=!![],_0xab9a9c[_0x5b918e(0x8a3)]['push'](_0x239a27),getById('container_'+_0x239a27)['classList'][_0x5b918e(0xada)](_0x5b918e(0x2e2)),_0xab9a9c[_0x5b918e(0x47f)](_0x239a27),_0xab9a9c[_0x5b918e(0x705)](_0x239a27);var _0x12e910={};_0x12e910['approved']=_0x5b918e(0xb80),_0xab9a9c['sendRequest'](_0x12e910,_0x239a27);}else{warnlog(_0x5b918e(0x982));var _0x12e910={};_0x12e910['rejected']='requestCoDirector',_0xab9a9c[_0x5b918e(0x936)](_0x12e910,_0x239a27);}})[_0x442d3f(0xacc)](function(){var _0x9430a=_0x442d3f;warnlog(_0x9430a(0x66d));var _0x497f64={};_0x497f64[_0x9430a(0x923)]=_0x9430a(0xb80),_0xab9a9c['sendRequest'](_0x497f64,_0x239a27);});return;})['catch'](errorlog);else{warnlog(_0x722b8a(0x656));var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]='requestCoDirector',_0xab9a9c[_0x722b8a(0x936)](_0x1048ae,_0x239a27);}}if(_0x722b8a(0x74e)in _0x3c51bb&&_0x722b8a(0x70d)in _0x3c51bb){log(_0x722b8a(0xbf1));if(_0xab9a9c[_0x722b8a(0x847)]){if(_0x239a27 in _0xab9a9c[_0x722b8a(0x84f)]&&_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['coDirector']===!![]){log(_0x722b8a(0x516));var _0x1048ae={};if(_0x3c51bb[_0x722b8a(0xbdc)]&&_0x3c51bb[_0x722b8a(0xbdc)][_0x722b8a(0x87a)])_0x1048ae[_0x722b8a(0x7aa)]='migrate',_0x1048ae[_0x722b8a(0xbdc)]=_0x3c51bb[_0x722b8a(0xbdc)],log(_0x1048ae),_0xab9a9c[_0x722b8a(0x936)](_0x1048ae,_0x3c51bb[_0x722b8a(0x74e)][_0x722b8a(0xc7a)](),function(){var _0x18af67=_0x722b8a,_0x181195={};_0x181195[_0x18af67(0x7aa)]=_0x18af67(0x74e),_0x181195[_0x18af67(0x70d)]=_0x3c51bb[_0x18af67(0x70d)],_0x181195[_0x18af67(0x357)]=_0x3c51bb[_0x18af67(0x74e)][_0x18af67(0xc7a)](),_0xab9a9c['sendMsg'](_0x181195);}),log(_0x1048ae);else{if(_0x3c51bb['transferSettings']&&_0x722b8a(0x4e2)in _0x3c51bb[_0x722b8a(0xbdc)])_0x1048ae[_0x722b8a(0x7aa)]=_0x722b8a(0x74e),_0x1048ae[_0x722b8a(0xbdc)]=_0x3c51bb[_0x722b8a(0xbdc)],delete _0x1048ae[_0x722b8a(0xbdc)][_0x722b8a(0x70d)],delete _0x1048ae['transferSettings'][_0x722b8a(0xbb7)],log(_0x1048ae),_0xab9a9c[_0x722b8a(0x936)](_0x1048ae,_0x3c51bb['migrate'][_0x722b8a(0xc7a)](),function(){var _0x16e63e=_0x722b8a,_0x46edbd={};_0x46edbd[_0x16e63e(0x7aa)]=_0x16e63e(0x74e),_0x46edbd[_0x16e63e(0x70d)]=_0x3c51bb[_0x16e63e(0x70d)],_0x46edbd[_0x16e63e(0x357)]=_0x3c51bb[_0x16e63e(0x74e)][_0x16e63e(0xc7a)](),_0xab9a9c[_0x16e63e(0x3ba)](_0x46edbd);}),log(_0x1048ae);else Object[_0x722b8a(0x7db)](_0x3c51bb['transferSettings'])[_0x722b8a(0xade)]?(_0x1048ae[_0x722b8a(0x7aa)]=_0x722b8a(0x74e),_0x1048ae['transferSettings']=_0x3c51bb[_0x722b8a(0xbdc)],delete _0x1048ae[_0x722b8a(0xbdc)]['roomid'],delete _0x1048ae[_0x722b8a(0xbdc)]['roomenc'],log(_0x1048ae),_0xab9a9c[_0x722b8a(0x936)](_0x1048ae,_0x3c51bb[_0x722b8a(0x74e)][_0x722b8a(0xc7a)](),function(){var _0x3b2204=_0x722b8a,_0x5a820b={};_0x5a820b[_0x3b2204(0x7aa)]=_0x3b2204(0x74e),_0x5a820b[_0x3b2204(0x70d)]=_0x3c51bb[_0x3b2204(0x70d)],_0x5a820b[_0x3b2204(0x357)]=_0x3c51bb['migrate'][_0x3b2204(0xc7a)](),_0xab9a9c[_0x3b2204(0x3ba)](_0x5a820b);}),log(_0x1048ae)):(_0x1048ae[_0x722b8a(0x7aa)]=_0x722b8a(0x74e),_0x1048ae[_0x722b8a(0x70d)]=_0x3c51bb[_0x722b8a(0x70d)],_0x1048ae[_0x722b8a(0x357)]=_0x3c51bb[_0x722b8a(0x74e)][_0x722b8a(0xc7a)](),_0xab9a9c[_0x722b8a(0x3ba)](_0x1048ae));}pokeIframeAPI('transfer',_0x3c51bb[_0x722b8a(0x70d)],_0x3c51bb[_0x722b8a(0x74e)][_0x722b8a(0xc7a)]());}}else{var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0xa7b),_0xab9a9c[_0x722b8a(0x936)](_0x1048ae,_0x239a27);}}}if(_0xab9a9c['director']&&_0x3c51bb[_0x722b8a(0x7aa)]===_0x722b8a(0x7e1)&&_0x3c51bb[_0x722b8a(0x1ab)]){try{if(_0x239a27 in _0xab9a9c[_0x722b8a(0x84f)]&&_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x352)]===!![]){var _0x22e29b={'directorSettings':{'addCoDirector':[_0x239a27]}};_0xab9a9c[_0x722b8a(0x936)](_0x22e29b,_0x3c51bb['guest']['toString']());}}catch(_0x52212e){errorlog(_0x52212e);}return;}if(_0x722b8a(0x38d)in _0x3c51bb){if(!_0x3c51bb[_0x722b8a(0xab9)]){log(_0x722b8a(0x6e4));return;}var _0x4c1295=_0x3c51bb[_0x722b8a(0x38d)];if(!_0xab9a9c['pcs'][_0x4c1295]){log('no\x20pcs[UUID]');return;}if(_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c1295)>=0x0){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x38d),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x3c51bb[_0x722b8a(0xab9)]),warnlog(_0x722b8a(0xcdf));return;}if(_0xab9a9c[_0x722b8a(0x827)]&&_0x3c51bb['request']===_0x722b8a(0x7e1)&&_0x3c51bb[_0x722b8a(0x1ab)]){try{if(_0x239a27 in _0xab9a9c[_0x722b8a(0x84f)]&&_0xab9a9c['pcs'][_0x239a27]['coDirector']===!![]){var _0x5e3e37={'directorSettings':{'addCoDirector':[_0x239a27]}};_0xab9a9c['sendRequest'](_0x5e3e37,_0x3c51bb[_0x722b8a(0x1ab)][_0x722b8a(0xc7a)]());}}catch(_0x20eec3){errorlog(_0x20eec3);}return;}if(_0xab9a9c[_0x722b8a(0x1d9)]){if('remote'in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x1d9)]===_0xab9a9c['remote']&&_0xab9a9c['remote']){}else{if(_0xab9a9c['remote']===!![]){}}}else{if(_0xab9a9c[_0x722b8a(0x8a3)]['indexOf'](_0x3c51bb[_0x722b8a(0xab9)])>=0x0){}else return;}_0x722b8a(0x757)in _0x3c51bb&&_0xab9a9c[_0x722b8a(0x757)](_0x4c1295,_0x3c51bb[_0x722b8a(0x757)]);_0x722b8a(0x989)in _0x3c51bb&&_0xab9a9c[_0x722b8a(0x989)](_0x4c1295,_0x3c51bb['targetAudioBitrate']);if(_0x722b8a(0x8eb)in _0x3c51bb)try{_0xab9a9c[_0x722b8a(0x97b)](_0x4c1295,_0x3c51bb[_0x722b8a(0x8eb)]['w'],_0x3c51bb[_0x722b8a(0x8eb)]['h'],_0x3c51bb[_0x722b8a(0x8eb)]['s'],_0x3c51bb[_0x722b8a(0x8eb)]['c']);}catch(_0xf231b3){errorlog(_0xf231b3);}return;}manageSceneState(_0x3c51bb,_0x239a27);try{if('info'in _0x3c51bb){_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x287)]['info']=_0x3c51bb[_0x722b8a(0xa82)];_0x722b8a(0x5e0)in _0x3c51bb['info']&&(typeof _0x3c51bb[_0x722b8a(0xa82)][_0x722b8a(0x5e0)]==_0x722b8a(0x9cb)?_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x5e0)]=sanitizeLabel(_0x3c51bb[_0x722b8a(0xa82)]['label']):_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x5e0)]=![]);_0x722b8a(0xc71)in _0x3c51bb[_0x722b8a(0xa82)]&&_0x3c51bb[_0x722b8a(0xa82)][_0x722b8a(0xc71)]&&(_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0xc71)]=!![],_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x1a7)]=_0x3c51bb[_0x722b8a(0xa82)][_0x722b8a(0x1a7)]||null,_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x21d)]=_0x3c51bb['info']['tipServer']||_0xab9a9c[_0x722b8a(0x21d)]||_0x722b8a(0x4a4),_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x3cf)]=_0x3c51bb[_0x722b8a(0xa82)][_0x722b8a(0x3cf)]||[0x5,0xa,0x19,0x32,0x64],_0xab9a9c['pcs'][_0x239a27]['tipCurrency']=_0x3c51bb[_0x722b8a(0xa82)][_0x722b8a(0xba3)]||_0x722b8a(0x520),_0xab9a9c['showTips']&&!_0xab9a9c[_0x722b8a(0xca4)]&&(typeof addTipIconToVideo===_0x722b8a(0x32c)&&addTipIconToVideo(_0x239a27)));if(_0x4c574b){if(_0x4c574b===_0xab9a9c[_0x722b8a(0xb38)])try{_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x287)][_0x722b8a(0xa82)][_0x722b8a(0x827)]=!![];}catch(_0x4e5cf9){}else{if(_0xab9a9c[_0x722b8a(0x8a3)]['indexOf'](_0x4c574b)>=0x0)try{_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x287)][_0x722b8a(0xa82)][_0x722b8a(0x352)]=!![];}catch(_0x38f375){}}}else{if(_0x239a27===_0xab9a9c[_0x722b8a(0xb38)])try{_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x287)][_0x722b8a(0xa82)][_0x722b8a(0x827)]=!![];}catch(_0x3ba3ed){}else{if(_0xab9a9c['directorList'][_0x722b8a(0x565)](_0x239a27)>=0x0)try{_0xab9a9c['pcs'][_0x239a27]['stats'][_0x722b8a(0xa82)][_0x722b8a(0x352)]=!![];}catch(_0x2878a1){}}}_0xab9a9c[_0x722b8a(0x8e2)]&&_0xab9a9c[_0x722b8a(0x827)]&&_0x722b8a(0x51f)in _0x3c51bb['info']&&_0x3c51bb[_0x722b8a(0xa82)][_0x722b8a(0x51f)]&&(broadcastSlotUpdate(_0x239a27),_0xab9a9c['obsSceneTriggers']?_0xab9a9c[_0x722b8a(0x1eb)]({'obsSceneTriggers':_0xab9a9c[_0x722b8a(0x681)],'layouts':_0xab9a9c[_0x722b8a(0x8e2)]},_0x239a27):_0xab9a9c[_0x722b8a(0x1eb)]({'layouts':_0xab9a9c[_0x722b8a(0x8e2)]},_0x239a27));if(Firefox||_0x3c51bb[_0x722b8a(0xa82)]['firefox']||(iOS||iPad)&&SafariVersion&&SafariVersion>0x10)try{_0x722b8a(0x5ba)in _0x3c51bb['info']&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x402)]===![]&&(_0x3c51bb[_0x722b8a(0xa82)]['vb_url']&&parseInt(_0x3c51bb[_0x722b8a(0xa82)][_0x722b8a(0x5ba)])>0x0&&(_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x402)]=parseInt(_0x3c51bb[_0x722b8a(0xa82)][_0x722b8a(0x5ba)]),_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xaf0)]&&clearTimeout(_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0xaf0)]),_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xaf0)]=setTimeout(function(_0xd2059){var _0x587d78=_0x722b8a;_0xab9a9c[_0x587d78(0x523)](_0xd2059,null);},0x3e8,_0x239a27))));}catch(_0x53710e){errorlog(_0x53710e);}pokeIframeAPI(_0x722b8a(0xb81),_0x3c51bb[_0x722b8a(0xa82)],_0x239a27);}if(_0x722b8a(0x48a)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x7dc)])try{_0xab9a9c[_0x722b8a(0x7dc)][_0x722b8a(0x602)](_0x722b8a(0xc07))&&processIframeSyncFeedback(_0x3c51bb['ifs'],_0x239a27);}catch(_0x1cbefd){errorlog(_0x1cbefd);}}_0x722b8a(0xc03)in _0x3c51bb&&_0xab9a9c['gotGenericData'](_0x3c51bb[_0x722b8a(0xc03)],_0x239a27);if(_0x722b8a(0xc17)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x9e1)]){!_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xa39)]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x2a8)]?_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['canvasOverlay']=receiveDrawingOnVideo(_0xab9a9c[_0x722b8a(0x6bf)],_0x239a27):_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xa39)]=receiveDrawingOnVideo(_0xab9a9c[_0x722b8a(0x7eb)],_0x239a27));if(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xa39)]){if(typeof _0x3c51bb['draw']==_0x722b8a(0x9cb)){if(_0x3c51bb[_0x722b8a(0xc17)]==_0x722b8a(0x3c1))_0xab9a9c['pcs'][_0x239a27]['canvasOverlay']['clearDrawing']();else{if(_0x3c51bb[_0x722b8a(0xc17)]==_0x722b8a(0x560))_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0xa39)][_0x722b8a(0x560)]();else _0x3c51bb[_0x722b8a(0xc17)]==_0x722b8a(0x8e0)&&_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xa39)][_0x722b8a(0xccf)]('undo');}}else _0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xa39)][_0x722b8a(0xccf)](_0x3c51bb[_0x722b8a(0xc17)]);}}return;}_0x722b8a(0xc75)in _0x3c51bb&&(_0xab9a9c['autoSyncObject']=_0x3c51bb[_0x722b8a(0xc75)],_0xab9a9c[_0x722b8a(0xcb5)](_0x239a27));_0x722b8a(0xbfc)in _0x3c51bb&&(_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0xbfc)]=parseInt(_0x3c51bb['optimizedBitrate']));_0x722b8a(0x6bb)in _0x3c51bb&&_0xab9a9c[_0x722b8a(0x368)](_0x239a27,_0x3c51bb[_0x722b8a(0x6bb)]);_0x722b8a(0x42f)in _0x3c51bb&&_0xab9a9c[_0x722b8a(0x523)](_0x239a27,_0x3c51bb[_0x722b8a(0x42f)]);_0x722b8a(0x757)in _0x3c51bb&&_0xab9a9c[_0x722b8a(0x757)](_0x239a27,_0x3c51bb['targetBitrate']);_0x722b8a(0x989)in _0x3c51bb&&_0xab9a9c['targetAudioBitrate'](_0x239a27,_0x3c51bb[_0x722b8a(0x989)]);if(_0x722b8a(0xaba)in _0x3c51bb){if(_0x722b8a(0x1d9)in _0x3c51bb){if(_0x3c51bb[_0x722b8a(0x1d9)]===_0xab9a9c[_0x722b8a(0x1d9)]&&_0xab9a9c[_0x722b8a(0x1d9)]||_0xab9a9c[_0x722b8a(0x1d9)]===!![]){_0xab9a9c[_0x722b8a(0xaba)]();return;}}}if(_0x722b8a(0x41f)in _0x3c51bb){if(_0xab9a9c['directorList']['indexOf'](_0x4c574b||_0x239a27)>=0x0){_0xab9a9c[_0x722b8a(0xaba)](!![]);return;}else{if('remote'in _0x3c51bb){if(_0x3c51bb['remote']===_0xab9a9c[_0x722b8a(0x1d9)]&&_0xab9a9c[_0x722b8a(0x1d9)]||_0xab9a9c[_0x722b8a(0x1d9)]===!![]){_0xab9a9c[_0x722b8a(0xaba)](!![]);return;}}}}if(_0x722b8a(0x1c8)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0){var _0x177430={};if(_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)])_0x177430['whipOut']=_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)];else for(var _0x280429 in _0xab9a9c[_0x722b8a(0x84f)]){if(_0x280429===_0x239a27)continue;_0x177430[_0x280429]=_0xab9a9c['pcs'][_0x280429][_0x722b8a(0x287)];}var _0x1048ae={};_0x1048ae['remoteStats']=_0x177430,_0xab9a9c['sendMessage'](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x1d9)in _0x3c51bb){if(_0x3c51bb['remote']===_0xab9a9c[_0x722b8a(0x1d9)]&&_0xab9a9c[_0x722b8a(0x1d9)]||_0xab9a9c[_0x722b8a(0x1d9)]===!![]){var _0x177430={};if(_0xab9a9c[_0x722b8a(0x52b)]['stats'])_0x177430[_0x722b8a(0x52b)]=_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)];else for(var _0x280429 in _0xab9a9c[_0x722b8a(0x84f)]){if(_0x280429===_0x239a27)continue;_0x177430[_0x280429]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)];}var _0x1048ae={};_0x1048ae[_0x722b8a(0x63e)]=_0x177430,_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}}else{var _0x177430={};if(_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)])_0x177430[_0x722b8a(0x52b)]=_0xab9a9c[_0x722b8a(0x52b)]['stats'];else for(var _0x280429 in _0xab9a9c['pcs']){if(_0x280429===_0x239a27)continue;if(!_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)])continue;if(_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x1ab)])continue;if(_0xab9a9c['roomid']){if(_0x722b8a(0x268)in _0xab9a9c['pcs'][_0x280429][_0x722b8a(0x287)]){if(_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x268)]===![])continue;}else continue;}_0x177430[_0x280429]={},_0xab9a9c[_0x722b8a(0x84f)][_0x280429]['stats']['video_bitrate_kbps']&&(_0x177430[_0x280429][_0x722b8a(0x71c)]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)]['video_bitrate_kbps']),_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x612)]&&(_0x177430[_0x280429]['nacks_per_second']=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x612)]),_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x99d)]&&(_0x177430[_0x280429]['available_outgoing_bitrate_kbps']=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x99d)]),_0xab9a9c['pcs'][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x268)]&&(_0x177430[_0x280429][_0x722b8a(0x268)]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x268)]),_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x5e0)]&&(_0x177430[_0x280429][_0x722b8a(0x5e0)]=_0xab9a9c['pcs'][_0x280429][_0x722b8a(0x5e0)]),_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x1ea)]&&(_0x177430[_0x280429][_0x722b8a(0x1ea)]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x1ea)]),_0xab9a9c['pcs'][_0x280429][_0x722b8a(0x287)][_0x722b8a(0xb90)]&&(_0x177430[_0x280429][_0x722b8a(0xb90)]=_0xab9a9c['pcs'][_0x280429][_0x722b8a(0x287)][_0x722b8a(0xb90)]);}var _0x1048ae={};_0x1048ae[_0x722b8a(0x63e)]=_0x177430,_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}}}if(_0x722b8a(0x716)in _0x3c51bb){clearInterval(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9da)]);if(_0xab9a9c[_0x722b8a(0x8a3)]['indexOf'](_0x4c574b||_0x239a27)>=0x0){if(_0x3c51bb[_0x722b8a(0x716)]){_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9da)]=setInterval(function(_0x42eac2){var _0x584efd=_0x722b8a,_0x303443={};if(_0xab9a9c[_0x584efd(0x52b)]['stats'])_0x303443[_0x584efd(0x52b)]=_0xab9a9c[_0x584efd(0x52b)]['stats'];else for(var _0x4a1d9c in _0xab9a9c['pcs']){if(_0x4a1d9c===_0x42eac2)continue;if(!_0xab9a9c['pcs'][_0x4a1d9c][_0x584efd(0x287)])continue;if(_0xab9a9c[_0x584efd(0x84f)][_0x4a1d9c][_0x584efd(0x1ab)])continue;_0x303443[_0x4a1d9c]=_0xab9a9c['pcs'][_0x4a1d9c][_0x584efd(0x287)];}var _0x51c736={};_0x51c736[_0x584efd(0x63e)]=_0x303443,_0xab9a9c[_0x584efd(0x1eb)](_0x51c736,_0x42eac2);},0xbb8,_0x239a27);var _0x177430={};if(_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)])_0x177430[_0x722b8a(0x52b)]=_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)];else for(var _0x280429 in _0xab9a9c[_0x722b8a(0x84f)]){if(_0x280429===_0x239a27)continue;if(!_0xab9a9c[_0x722b8a(0x84f)][_0x280429]['stats'])continue;if(_0xab9a9c[_0x722b8a(0x84f)][_0x280429]['guest'])continue;_0x177430[_0x280429]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)];}var _0x1048ae={};_0x1048ae[_0x722b8a(0x63e)]=_0x177430,_0xab9a9c['sendMessage'](_0x1048ae,_0x239a27);}}else{if('remote'in _0x3c51bb){if(_0x3c51bb[_0x722b8a(0x1d9)]===_0xab9a9c[_0x722b8a(0x1d9)]&&_0xab9a9c['remote']||_0xab9a9c[_0x722b8a(0x1d9)]===!![]){if(_0x3c51bb[_0x722b8a(0x716)]){_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9da)]=setInterval(function(_0x294998){var _0x444347=_0x722b8a,_0x33bdbc={};if(_0xab9a9c['whipOut'][_0x444347(0x287)])_0x33bdbc['whipOut']=_0xab9a9c['whipOut'][_0x444347(0x287)];else for(var _0x131353 in _0xab9a9c['pcs']){if(_0x131353===_0x294998)continue;if(!_0xab9a9c[_0x444347(0x84f)][_0x131353][_0x444347(0x287)])continue;if(_0xab9a9c['pcs'][_0x131353]['guest'])continue;_0x33bdbc[_0x131353]=_0xab9a9c['pcs'][_0x131353][_0x444347(0x287)];}var _0x432bc5={};_0x432bc5[_0x444347(0x63e)]=_0x33bdbc,_0xab9a9c[_0x444347(0x1eb)](_0x432bc5,_0x294998);},0xbb8,_0x239a27);var _0x177430={};if(_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)])_0x177430['whipOut']=_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)];else for(var _0x280429 in _0xab9a9c[_0x722b8a(0x84f)]){if(_0x280429===_0x239a27)continue;if(!_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)])continue;if(_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x1ab)])continue;_0x177430[_0x280429]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)];}var _0x1048ae={};_0x1048ae[_0x722b8a(0x63e)]=_0x177430,_0xab9a9c['sendMessage'](_0x1048ae,_0x239a27);}}}else{if(_0x3c51bb['requestStatsContinuous']){_0xab9a9c['pcs'][_0x239a27]['requestedStatsInterval']=setInterval(function(_0x2f2c49){var _0xee9a39=_0x722b8a,_0x59be31={};if(_0xab9a9c[_0xee9a39(0x52b)]['stats'])_0x59be31[_0xee9a39(0x52b)]=_0xab9a9c['whipOut']['stats'];else for(var _0x5f08d7 in _0xab9a9c[_0xee9a39(0x84f)]){if(_0x5f08d7===_0x2f2c49)continue;if(!_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7]['stats'])continue;if(_0xab9a9c['pcs'][_0x5f08d7][_0xee9a39(0x1ab)])continue;if(_0xab9a9c[_0xee9a39(0x70d)]){if(_0xee9a39(0x268)in _0xab9a9c['pcs'][_0x5f08d7]['stats']){if(_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x268)]===![])continue;}else continue;}_0x59be31[_0x5f08d7]={},_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7][_0xee9a39(0x287)]['video_bitrate_kbps']&&(_0x59be31[_0x5f08d7][_0xee9a39(0x71c)]=_0xab9a9c['pcs'][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x71c)]),_0xab9a9c['pcs'][_0x5f08d7][_0xee9a39(0x287)]['nacks_per_second']&&(_0x59be31[_0x5f08d7][_0xee9a39(0x612)]=_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x612)]),_0xab9a9c['pcs'][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x99d)]&&(_0x59be31[_0x5f08d7][_0xee9a39(0x99d)]=_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x99d)]),_0xab9a9c['pcs'][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x268)]&&(_0x59be31[_0x5f08d7][_0xee9a39(0x268)]=_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x268)]),_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7][_0xee9a39(0x5e0)]&&(_0x59be31[_0x5f08d7]['label']=_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7][_0xee9a39(0x5e0)]),_0xab9a9c['pcs'][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x1ea)]&&(_0x59be31[_0x5f08d7][_0xee9a39(0x1ea)]=_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7][_0xee9a39(0x287)][_0xee9a39(0x1ea)]),_0xab9a9c[_0xee9a39(0x84f)][_0x5f08d7]['stats'][_0xee9a39(0xb90)]&&(_0x59be31[_0x5f08d7][_0xee9a39(0xb90)]=_0xab9a9c['pcs'][_0x5f08d7]['stats'][_0xee9a39(0xb90)]);}var _0x299b92={};_0x299b92[_0xee9a39(0x63e)]=_0x59be31,_0xab9a9c[_0xee9a39(0x1eb)](_0x299b92,_0x2f2c49);},0xbb8,_0x239a27);var _0x177430={};if(_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x287)])_0x177430['whipOut']=_0xab9a9c['whipOut'][_0x722b8a(0x287)];else for(var _0x280429 in _0xab9a9c[_0x722b8a(0x84f)]){if(_0x280429===_0x239a27)continue;if(!_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)])continue;if(_0xab9a9c[_0x722b8a(0x84f)][_0x280429]['guest'])continue;if(_0xab9a9c[_0x722b8a(0x70d)]){if(_0x722b8a(0x268)in _0xab9a9c['pcs'][_0x280429][_0x722b8a(0x287)]){if(_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)]['scene']===![])continue;}else continue;}_0x177430[_0x280429]={},_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)]['video_bitrate_kbps']&&(_0x177430[_0x280429]['video_bitrate_kbps']=_0xab9a9c[_0x722b8a(0x84f)][_0x280429]['stats'][_0x722b8a(0x71c)]),_0xab9a9c['pcs'][_0x280429]['stats'][_0x722b8a(0x612)]&&(_0x177430[_0x280429]['nacks_per_second']=_0xab9a9c[_0x722b8a(0x84f)][_0x280429]['stats'][_0x722b8a(0x612)]),_0xab9a9c['pcs'][_0x280429]['stats']['available_outgoing_bitrate_kbps']&&(_0x177430[_0x280429][_0x722b8a(0x99d)]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)]['available_outgoing_bitrate_kbps']),_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)]['scene']&&(_0x177430[_0x280429][_0x722b8a(0x268)]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0x268)]),_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x5e0)]&&(_0x177430[_0x280429][_0x722b8a(0x5e0)]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429]['label']),_0xab9a9c['pcs'][_0x280429]['stats']['resolution']&&(_0x177430[_0x280429][_0x722b8a(0x1ea)]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)]['resolution']),_0xab9a9c[_0x722b8a(0x84f)][_0x280429]['stats'][_0x722b8a(0xb90)]&&(_0x177430[_0x280429][_0x722b8a(0xb90)]=_0xab9a9c[_0x722b8a(0x84f)][_0x280429][_0x722b8a(0x287)][_0x722b8a(0xb90)]);}var _0x1048ae={};_0x1048ae['remoteStats']=_0x177430,_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}}}}if(_0x722b8a(0x8eb)in _0x3c51bb)try{_0xab9a9c['setResolution'](_0x239a27,_0x3c51bb[_0x722b8a(0x8eb)]['w'],_0x3c51bb['requestResolution']['h'],_0x3c51bb['requestResolution']['s'],_0x3c51bb['requestResolution']['c']);}catch(_0x4cf0a1){errorlog(_0x4cf0a1);}'keyframe'in _0x3c51bb&&(_0x3c51bb['scene']?_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0?_0xab9a9c[_0x722b8a(0x28c)]():errorlog(_0x722b8a(0x83b)):_0xab9a9c['forcePLI'](_0x239a27));if('chat'in _0x3c51bb){var _0x2e3dd7=![],_0x45ce80=![];_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0&&(_0x2e3dd7=!![],_0x722b8a(0x202)in _0x3c51bb&&(_0x3c51bb['overlay']==!![]&&(_0x45ce80=!![]))),log('isDirector\x20'+_0x2e3dd7),getChatMessage(_0x3c51bb[_0x722b8a(0x927)],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x5e0)],_0x2e3dd7,_0x45ce80,_0x239a27);}'tip'in _0x3c51bb&&(typeof processTipMessage===_0x722b8a(0x32c)&&processTipMessage(_0x3c51bb[_0x722b8a(0x313)],_0x239a27));if(_0x722b8a(0x9ae)in _0x3c51bb){_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9ae)]=parseInt(_0x3c51bb[_0x722b8a(0x9ae)])||0x0;_0x239a27 in _0xab9a9c[_0x722b8a(0x404)]&&(_0xab9a9c['rpcs'][_0x239a27][_0x722b8a(0x9ae)]=_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9ae)]);if(_0xab9a9c[_0x722b8a(0x827)]){var _0x59789d=document[_0x722b8a(0x1bb)]('[data-action-type=\x22order-value\x22][data--u-u-i-d=\x22'+_0x239a27+'\x22]');log(_0x59789d),_0x59789d[0x0]&&(_0x59789d[0x0][_0x722b8a(0xae1)]=parseInt(_0x3c51bb[_0x722b8a(0x9ae)])||0x0);}updateMixer();}_0x722b8a(0xa53)in _0x3c51bb&&_0xab9a9c['setScale'](_0x239a27,_0x3c51bb[_0x722b8a(0xa53)]);if(_0xab9a9c[_0x722b8a(0x827)]&&_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x352)]&&_0x722b8a(0x40a)in _0x3c51bb){log(_0x3c51bb),_0xab9a9c[_0x722b8a(0x39f)]=_0x3c51bb[_0x722b8a(0x40a)];for(var _0x21dcba in _0xab9a9c[_0x722b8a(0x39f)]){syncSceneState(_0x21dcba),syncOtherState(_0x21dcba),syncLabelState(_0x21dcba);if(_0xab9a9c[_0x722b8a(0x442)]&&_0xab9a9c[_0x722b8a(0x39f)][_0x21dcba]&&_0xab9a9c[_0x722b8a(0x39f)][_0x21dcba][_0x722b8a(0xa51)]&&_0xab9a9c[_0x722b8a(0x39f)][_0x21dcba][_0x722b8a(0xa51)][_0x722b8a(0xa69)]){var _0x1de84e=![];for(var _0x25966d in _0xab9a9c['rpcs']){if(_0xab9a9c[_0x722b8a(0x404)][_0x25966d]['streamID']===_0x21dcba){_0x3447a7(_0x25966d),_0xab9a9c[_0x722b8a(0x55c)](_0x25966d),_0x1de84e=!![];break;}}!_0x1de84e&&!_0xab9a9c[_0x722b8a(0x1dd)]['includes'](_0x21dcba)&&_0xab9a9c[_0x722b8a(0x1dd)][_0x722b8a(0x5e7)](_0x21dcba);}else{if(_0xab9a9c[_0x722b8a(0x39f)][_0x21dcba]&&_0xab9a9c[_0x722b8a(0x39f)][_0x21dcba][_0x722b8a(0xa51)]&&!_0xab9a9c[_0x722b8a(0x39f)][_0x21dcba][_0x722b8a(0xa51)][_0x722b8a(0xa69)]){var _0x3d1e81=_0xab9a9c[_0x722b8a(0x1dd)][_0x722b8a(0x565)](_0x21dcba);_0x3d1e81>-0x1&&_0xab9a9c['pendingApprovalStreamIDs']['splice'](_0x3d1e81,0x1);}}}pokeAPI('details',_0x3c51bb[_0x722b8a(0x40a)]);for(var _0x576267 in _0xab9a9c[_0x722b8a(0x84f)]){_0xab9a9c[_0x722b8a(0x84f)][_0x576267][_0x722b8a(0x352)]&&_0x576267!==_0x239a27&&_0xab9a9c['sendMessage']({'directorState':_0x3c51bb['directorState']},_0x576267);}}if(_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)==-0x1){if('requestAudioHack'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x78b),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xcd9)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0xcd9),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x3af)in _0x3c51bb){var _0x1048ae={};_0x1048ae['rejected']=_0x722b8a(0x3af),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x1b3)in _0x3c51bb){var _0x1048ae={};_0x1048ae['rejected']='changeURL',_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('changeLabel'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x898),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xa6c)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]='requestChangeEQ',_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x3eb)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x3eb),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x7b3)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x7b3),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x6e7)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x6e7),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x27e)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x27e),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x4f9)in _0x3c51bb){var _0x1048ae={};_0x1048ae['rejected']=_0x722b8a(0x4f9),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('requestChangeMicDelay'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x1e7),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('lowerhand'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x349),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xaba)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]='hangup',_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x254)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x254),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('speakerMute'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]='speakerMute',_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x57d)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x57d),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xc36)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0xc36),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x36c)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x36c),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xb6b)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0xb6b),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('resumeClock'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0xc74),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('setClock'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x2d8),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('hideClock'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x7cb),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('showClock'in _0x3c51bb){var _0x1048ae={};_0x1048ae['rejected']=_0x722b8a(0x586),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xab8)in _0x3c51bb){var _0x1048ae={};_0x1048ae['rejected']=_0x722b8a(0xab8),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xa29)in _0x3c51bb){var _0x1048ae={};_0x1048ae['rejected']='pauseClock',_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xa0f)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0xa0f),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x34e)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x34e),_0xab9a9c['sendMessage'](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x359)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]='rotate',_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x2d2)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]='refreshMicrophone',_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x263)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x263),_0xab9a9c['sendMessage'](_0x1048ae,_0x239a27);}else{if('getConnectionMap'in _0x3c51bb){var _0x1048ae={};_0x1048ae['rejected']=_0x722b8a(0x31a),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0xcaf)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]='refreshVideo',_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if('refreshConnection'in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x548),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}else{if(_0x722b8a(0x348)in _0x3c51bb){var _0x1048ae={};_0x1048ae[_0x722b8a(0x923)]=_0x722b8a(0x348),_0xab9a9c['sendMessage'](_0x1048ae,_0x239a27);}else{if('reconnectPeer'in _0x3c51bb){var _0x1048ae={};_0x1048ae['rejected']=_0x722b8a(0xbe4),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x239a27);}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}else{if(_0x722b8a(0x78b)in _0x3c51bb){var _0x13b1f3=_0xab9a9c[_0x722b8a(0xca7)][_0x722b8a(0x568)]();_0x13b1f3['length']&&(_0x722b8a(0x651)in _0x3c51bb?applyAudioHack(_0x3c51bb[_0x722b8a(0x48c)],_0x3c51bb[_0x722b8a(0xb58)],_0x3c51bb[_0x722b8a(0x651)]):applyAudioHack(_0x3c51bb[_0x722b8a(0x48c)],_0x3c51bb[_0x722b8a(0xb58)]));}if('requestVideoRecord'in _0x3c51bb){if(_0x3c51bb['requestVideoRecord']){_0x3c51bb['googleDriveRecord']&&(_0xab9a9c[_0x722b8a(0x48f)]={},_0xab9a9c[_0x722b8a(0x48f)][_0x722b8a(0x538)]=_0x3c51bb[_0x722b8a(0xcad)]);if(_0xab9a9c['videoElement']){var _0xc57bb8=0x1770;if(_0x3c51bb[_0x722b8a(0x2f8)])_0xc57bb8=_0x3c51bb[_0x722b8a(0x2f8)];else _0x3c51bb[_0x722b8a(0xb58)]&&(_0xc57bb8=parseInt(_0x3c51bb[_0x722b8a(0xb58)]));recordLocalVideo(_0x722b8a(0x59f),_0xc57bb8,![],_0x3c51bb[_0x722b8a(0xaf4)]||![]);}}else _0xab9a9c['videoElement']&&recordLocalVideo(_0x722b8a(0x638),![],![],_0x3c51bb[_0x722b8a(0xaf4)]||![]);}if('changeOrder'in _0x3c51bb){_0xab9a9c[_0x722b8a(0x9ae)]==![]&&(_0xab9a9c['order']=0x0);_0xab9a9c['order']+=parseInt(_0x3c51bb[_0x722b8a(0x3af)])||0x0;var _0x1048ae={};_0x1048ae={},_0x1048ae[_0x722b8a(0x9ae)]=_0xab9a9c[_0x722b8a(0x9ae)],_0xab9a9c[_0x722b8a(0xae3)](_0x1048ae),updateMixer();}if(_0x722b8a(0x27e)in _0x3c51bb){var _0x21db5a=_0xab9a9c['micPanning'];if(_0x3c51bb[_0x722b8a(0xb58)]===_0x722b8a(0x9a1))_0xab9a9c[_0x722b8a(0x913)]=![];else{var _0x4ea6bb=parseInt(_0x3c51bb[_0x722b8a(0xb58)]);isNaN(_0x4ea6bb)&&(_0x4ea6bb=0x5a);if(_0x4ea6bb<0x0)_0x4ea6bb=0x0;if(_0x4ea6bb>0xb4)_0x4ea6bb=0xb4;_0xab9a9c[_0x722b8a(0x913)]=_0x4ea6bb;}if(_0xab9a9c[_0x722b8a(0x913)]!==![])try{_0xab9a9c[_0x722b8a(0x232)]=![];}catch(_0x2dc9b2){}if(_0x21db5a===![]&&_0xab9a9c[_0x722b8a(0x913)]!==![]||_0x21db5a!==![]&&_0xab9a9c[_0x722b8a(0x913)]===![]){try{_0xab9a9c[_0x722b8a(0x7eb)][_0x722b8a(0x5f6)]=outboundAudioPipeline();}catch(_0x46bbc7){errorlog(_0x46bbc7);}senderAudioUpdate();}else _0xab9a9c[_0x722b8a(0x913)]!==![]&&changeMicPanning(_0xab9a9c[_0x722b8a(0x913)],_0x3c51bb[_0x722b8a(0x50d)]);}'changeURL'in _0x3c51bb&&changeURL(_0x3c51bb['changeURL']);if(_0x722b8a(0x359)in _0x3c51bb){if(_0x3c51bb[_0x722b8a(0x359)]===!![])_0xab9a9c[_0x722b8a(0x359)]===![]?_0xab9a9c[_0x722b8a(0x359)]=0x5a:_0xab9a9c[_0x722b8a(0x359)]+=0x5a,_0xab9a9c[_0x722b8a(0x359)]>=0x168&&(_0xab9a9c[_0x722b8a(0x359)]-=0x168),_0xab9a9c[_0x722b8a(0x359)]===0x0&&(_0xab9a9c[_0x722b8a(0x359)]=![]);else _0x3c51bb['rotate']===!![]?_0xab9a9c[_0x722b8a(0x359)]=![]:_0xab9a9c[_0x722b8a(0x359)]=parseInt(_0x3c51bb[_0x722b8a(0x359)])||![];updateForceRotate(),updateMixer();}'stopClock'in _0x3c51bb&&stopClock();_0x722b8a(0xc74)in _0x3c51bb&&resumeClock();_0x722b8a(0x2d8)in _0x3c51bb&&setClock(_0x3c51bb[_0x722b8a(0x2d8)]);'hideClock'in _0x3c51bb&&hideClock();_0x722b8a(0x586)in _0x3c51bb&&showClock();_0x722b8a(0xab8)in _0x3c51bb&&startClock();_0x722b8a(0xa29)in _0x3c51bb&&pauseClock();if(_0x722b8a(0xa0f)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0xa0f)]!==![]){if(_0x3c51bb['showTime']&&!_0xab9a9c[_0x722b8a(0xa0f)])toggleClock(_0x3c51bb[_0x722b8a(0xba2)]||![]);else!_0x3c51bb[_0x722b8a(0xa0f)]&&_0xab9a9c['showTime']&&toggleClock(_0x3c51bb[_0x722b8a(0xba2)]||![]);}}_0x722b8a(0x36c)in _0x3c51bb&&toggleFileshare(_0x239a27);if(_0x722b8a(0x34e)in _0x3c51bb)try{_0x4c574b?(_0x3c51bb[_0x722b8a(0x34e)]?_0xab9a9c[_0x722b8a(0x797)]=_0x3c51bb[_0x722b8a(0x34e)][_0x722b8a(0x55a)](','):_0xab9a9c[_0x722b8a(0x797)]=[],_0xab9a9c[_0x722b8a(0x1eb)]({'group':_0x3c51bb[_0x722b8a(0x34e)],'altUUID':!![]})):(_0x3c51bb[_0x722b8a(0x34e)]?_0xab9a9c['group']=_0x3c51bb[_0x722b8a(0x34e)][_0x722b8a(0x55a)](','):_0xab9a9c['group']=[],_0xab9a9c[_0x722b8a(0x1eb)]({'group':_0x3c51bb[_0x722b8a(0x34e)]})),updateMixer(),pokeIframeAPI(_0x722b8a(0xbff),_0xab9a9c[_0x722b8a(0x34e)]);}catch(_0x50ffae){}if('changeLabel'in _0x3c51bb){if(_0x722b8a(0xb58)in _0x3c51bb){if(typeof _0x3c51bb[_0x722b8a(0xb58)]==_0x722b8a(0x9cb)){_0xab9a9c[_0x722b8a(0x5e0)]=sanitizeLabel(_0x3c51bb[_0x722b8a(0xb58)]),log(_0x722b8a(0x978)+_0xab9a9c[_0x722b8a(0x5e0)]);if(_0xab9a9c[_0x722b8a(0x827)]){var _0x59789d=getById('label_'+_0x239a27);if(_0xab9a9c['label'])_0x59789d['innerText']=_0xab9a9c[_0x722b8a(0x5e0)],_0x59789d[_0x722b8a(0xc93)][_0x722b8a(0x50f)](_0x722b8a(0x203));else _0xab9a9c[_0x722b8a(0xb38)]===(_0x4c574b||_0x239a27)?(miniTranslate(_0x59789d['innerHTML'],_0x722b8a(0x573)),_0x59789d[_0x722b8a(0xc93)][_0x722b8a(0x50f)](_0x722b8a(0x203))):(miniTranslate(_0x59789d['innerHTML'],_0x722b8a(0x7f6)),_0x59789d[_0x722b8a(0xc93)]['add']('addALabel'));}else _0xab9a9c['showlabels']&&updateMixer();!_0xab9a9c[_0x722b8a(0x827)]&&(_0xab9a9c[_0x722b8a(0x5e0)]?document[_0x722b8a(0xbb2)]=_0xab9a9c[_0x722b8a(0x5e0)]:document['title']=location['hostname']);var _0x154f00=encodeURIComponent(_0xab9a9c[_0x722b8a(0x5e0)]);urlParams[_0x722b8a(0x2f5)]('l')?updateURL('l='+_0x154f00,!![],![]):updateURL(_0x722b8a(0x9f1)+_0x154f00,!![],![]);var _0x1048ae={};_0x1048ae[_0x722b8a(0x898)]=!![],_0x1048ae['value']=_0xab9a9c[_0x722b8a(0x5e0)],_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae);}else{_0xab9a9c['label']=![];var _0x1048ae={};_0x1048ae[_0x722b8a(0x898)]=!![],_0x1048ae['value']=_0xab9a9c[_0x722b8a(0x5e0)],_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae);if(_0xab9a9c[_0x722b8a(0x827)]){var _0x59789d=getById(_0x722b8a(0x8e9)+_0x239a27);_0xab9a9c['directorUUID']===(_0x4c574b||_0x239a27)?(miniTranslate(_0x59789d['innerHTML'],_0x722b8a(0x573)),_0x59789d['classList'][_0x722b8a(0x50f)](_0x722b8a(0x203))):(miniTranslate(_0x59789d[_0x722b8a(0x247)],_0x722b8a(0x7f6)),_0x59789d[_0x722b8a(0xc93)][_0x722b8a(0xada)]('addALabel'));}else _0xab9a9c[_0x722b8a(0x6e5)]?(document[_0x722b8a(0xbb2)]=location[_0x722b8a(0x9f3)],updateMixer()):document['title']=location[_0x722b8a(0x9f3)];}}}if(_0x722b8a(0xa6c)in _0x3c51bb){if(_0x3c51bb['keyname']==_0x722b8a(0x7f5))changeLowEQ(parseFloat(_0x3c51bb[_0x722b8a(0xb58)]),_0x3c51bb['track']);else{if(_0x3c51bb[_0x722b8a(0x48c)]==_0x722b8a(0x1ac))changeMidEQ(parseFloat(_0x3c51bb[_0x722b8a(0xb58)]),_0x3c51bb['track']);else _0x3c51bb[_0x722b8a(0x48c)]==_0x722b8a(0x412)&&changeHighEQ(parseFloat(_0x3c51bb['value']),_0x3c51bb[_0x722b8a(0x50d)]);}}if(_0x722b8a(0x3eb)in _0x3c51bb){var _0x1c53da=_0xab9a9c[_0x722b8a(0x2c7)];if(_0x3c51bb[_0x722b8a(0xb58)]===_0x722b8a(0x9a1))_0xab9a9c['noisegate']=![],log(_0x722b8a(0x7b8));else _0x3c51bb[_0x722b8a(0xb58)]===_0x722b8a(0x372)?(_0xab9a9c[_0x722b8a(0x2c7)]=!![],log(_0x722b8a(0x93d))):_0xab9a9c[_0x722b8a(0x2c7)]=_0x3c51bb[_0x722b8a(0xb58)];_0xab9a9c[_0x722b8a(0x2c7)]!==_0x1c53da&&senderAudioUpdate();}if(_0x722b8a(0x7b3)in _0x3c51bb){var _0x1c53da=_0xab9a9c['compressor'];if(_0x3c51bb[_0x722b8a(0xb58)]===_0x722b8a(0x9a1))_0xab9a9c['compressor']=![],log(_0x722b8a(0x7b8));else{if(_0x3c51bb[_0x722b8a(0xb58)]==='1')_0xab9a9c[_0x722b8a(0x72e)]=0x1,log('noise\x20gate\x20on');else _0x3c51bb['value']==='2'?(_0xab9a9c[_0x722b8a(0x72e)]=0x2,log(_0x722b8a(0x93d))):_0xab9a9c[_0x722b8a(0x72e)]=parseInt(_0x3c51bb['value'])||![];}_0xab9a9c[_0x722b8a(0x72e)]!==_0x1c53da&&senderAudioUpdate();}if('requestChangeMicPanning'in _0x3c51bb){var _0x21db5a=_0xab9a9c[_0x722b8a(0x913)];if(_0x3c51bb[_0x722b8a(0xb58)]==='false')_0xab9a9c[_0x722b8a(0x913)]=![];else{var _0x4ea6bb=parseInt(_0x3c51bb[_0x722b8a(0xb58)]);isNaN(_0x4ea6bb)&&(_0x4ea6bb=0x5a);if(_0x4ea6bb<0x0)_0x4ea6bb=0x0;if(_0x4ea6bb>0xb4)_0x4ea6bb=0xb4;_0xab9a9c[_0x722b8a(0x913)]=_0x4ea6bb;}if(_0x21db5a===![]&&_0xab9a9c['micPanning']!==![]||_0x21db5a!==![]&&_0xab9a9c['micPanning']===![]){try{_0xab9a9c['videoElement'][_0x722b8a(0x5f6)]=outboundAudioPipeline();}catch(_0x2af436){errorlog(_0x2af436);}senderAudioUpdate();}else _0xab9a9c[_0x722b8a(0x913)]!==![]&&changeMicPanning(_0xab9a9c[_0x722b8a(0x913)],_0x3c51bb[_0x722b8a(0x50d)]);}_0x722b8a(0x1e7)in _0x3c51bb&&(_0xab9a9c[_0x722b8a(0x1c7)]===![]?(_0xab9a9c['micDelay']=parseInt(_0x3c51bb[_0x722b8a(0xb58)])||0x0,senderAudioUpdate()):(_0xab9a9c['micDelay']=parseInt(_0x3c51bb['value'])||0x0,changeMicDelay(_0xab9a9c['micDelay'],_0x3c51bb[_0x722b8a(0x50d)])));'requestChangeSubGain'in _0x3c51bb&&changeSubGain(parseFloat(_0x3c51bb[_0x722b8a(0xb58)]),_0x3c51bb[_0x722b8a(0x651)]);_0x722b8a(0x349)in _0x3c51bb&&(_0xab9a9c[_0x722b8a(0x57b)]&&lowerhand());if(_0x722b8a(0x7a5)in _0x3c51bb&&'mirrorGuestTarget'in _0x3c51bb){if(_0x3c51bb[_0x722b8a(0xc4a)]&&_0x3c51bb[_0x722b8a(0xc4a)]===!![])_0xab9a9c[_0x722b8a(0x5a7)]=_0x3c51bb[_0x722b8a(0x7a5)],_0xab9a9c[_0x722b8a(0xa3c)]=_0x3c51bb[_0x722b8a(0x7a5)],applyMirror(_0xab9a9c[_0x722b8a(0x5c8)]);else _0x3c51bb[_0x722b8a(0xc4a)]&&_0x3c51bb[_0x722b8a(0xc4a)]in _0xab9a9c[_0x722b8a(0x404)]&&(_0xab9a9c[_0x722b8a(0x404)][_0x3c51bb[_0x722b8a(0xc4a)]][_0x722b8a(0x4d6)]=_0x3c51bb[_0x722b8a(0x7a5)],_0xab9a9c[_0x722b8a(0x404)][_0x3c51bb['mirrorGuestTarget']][_0x722b8a(0x7eb)]&&applyMirrorGuest(_0x3c51bb[_0x722b8a(0x7a5)],_0xab9a9c['rpcs'][_0x3c51bb[_0x722b8a(0xc4a)]][_0x722b8a(0x7eb)],_0xab9a9c[_0x722b8a(0x404)][_0x3c51bb['mirrorGuestTarget']][_0x722b8a(0xb30)]));}if(_0x722b8a(0x5b4)in _0x3c51bb){var _0x1048ae={};_0x1048ae['UUID']=_0x239a27,_0x1048ae[_0x722b8a(0xa3f)]=listAudioSettingsPrep(),sendMediaDevices(_0x1048ae[_0x722b8a(0xab9)]),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x1048ae[_0x722b8a(0xab9)]);}if('getVideoSettings'in _0x3c51bb){var _0x1048ae={};_0x1048ae['UUID']=_0x239a27,_0x1048ae[_0x722b8a(0xbc5)]=listVideoSettingsPrep(),sendMediaDevices(_0x1048ae[_0x722b8a(0xab9)]),_0xab9a9c['sendMessage'](_0x1048ae,_0x1048ae[_0x722b8a(0xab9)]);}_0x722b8a(0x6fe)in _0x3c51bb&&changeAudioOutputDeviceById(_0x3c51bb[_0x722b8a(0x6fe)],_0x239a27);_0x722b8a(0x263)in _0x3c51bb&&changeAudioDeviceById(_0x3c51bb[_0x722b8a(0x263)],_0x239a27);_0x722b8a(0x2d2)in _0x3c51bb&&refreshMicrophoneDevice(_0x239a27);_0x722b8a(0xcaf)in _0x3c51bb&&refreshVideoDevice(_0x239a27);if('refreshConnection'in _0x3c51bb){for(var _0x4f8ef9 in _0xab9a9c[_0x722b8a(0x84f)]){if(_0xab9a9c['pcs'][_0x4f8ef9]&&_0xab9a9c[_0x722b8a(0x84f)][_0x4f8ef9]['restartIce'])try{_0xab9a9c[_0x722b8a(0x84f)][_0x4f8ef9][_0x722b8a(0xc25)](),log(_0x722b8a(0xadc)+_0x4f8ef9);}catch(_0x49d4ea){warnlog(_0x722b8a(0xba8)+_0x49d4ea);}}for(var _0x4f8ef9 in _0xab9a9c[_0x722b8a(0x404)]){if(_0xab9a9c['rpcs'][_0x4f8ef9]&&_0xab9a9c['rpcs'][_0x4f8ef9]['restartIce'])try{_0xab9a9c['rpcs'][_0x4f8ef9][_0x722b8a(0xc25)](),log(_0x722b8a(0x609)+_0x4f8ef9);}catch(_0x2ffb70){warnlog(_0x722b8a(0xc2d)+_0x2ffb70);}}}_0x722b8a(0x348)in _0x3c51bb&&(refreshMicrophoneDevice(_0x239a27),setTimeout(function(){refreshVideoDevice(_0x239a27);},0x1f4),setTimeout(function(){var _0x4a2cb1=_0x722b8a;for(var _0x56f3cf in _0xab9a9c['pcs']){if(_0xab9a9c[_0x4a2cb1(0x84f)][_0x56f3cf]&&_0xab9a9c[_0x4a2cb1(0x84f)][_0x56f3cf][_0x4a2cb1(0xc25)])try{_0xab9a9c[_0x4a2cb1(0x84f)][_0x56f3cf]['restartIce']();}catch(_0x14007e){}}for(var _0x56f3cf in _0xab9a9c[_0x4a2cb1(0x404)]){if(_0xab9a9c[_0x4a2cb1(0x404)][_0x56f3cf]&&_0xab9a9c[_0x4a2cb1(0x404)][_0x56f3cf][_0x4a2cb1(0xc25)])try{_0xab9a9c[_0x4a2cb1(0x404)][_0x56f3cf][_0x4a2cb1(0xc25)]();}catch(_0x2c0c7b){}}},0x3e8));if(_0x722b8a(0x429)in _0x3c51bb){if(_0xab9a9c['restartWhipConnection'])log(_0x722b8a(0x441)),_0xab9a9c[_0x722b8a(0x711)]();else{if(_0xab9a9c['whipOut']){log(_0x722b8a(0x855));try{_0xab9a9c[_0x722b8a(0x52b)][_0x722b8a(0x3ef)](),_0xab9a9c['whipOut']=null;}catch(_0x28776e){warnlog(_0x28776e);}}else log(_0x722b8a(0x6df));}}if(_0x722b8a(0xbe4)in _0x3c51bb){var _0xc063e3=_0x3c51bb[_0x722b8a(0xbe4)];if(_0xc063e3){if(_0xab9a9c[_0x722b8a(0x84f)][_0xc063e3])log('Reconnecting\x20pcs\x20peer:\x20'+_0xc063e3),_0xab9a9c[_0x722b8a(0x219)](_0xc063e3);else _0xab9a9c[_0x722b8a(0x404)][_0xc063e3]&&(log('Reconnecting\x20rpcs\x20peer:\x20'+_0xc063e3),_0xab9a9c[_0x722b8a(0x35f)](_0xc063e3));}}if(_0x722b8a(0x31a)in _0x3c51bb){var _0x3f4db2=_0x722b8a(0xbb3);if(SafariVersion)_0x3f4db2='Safari\x20'+SafariVersion;else{if(typeof getChromiumVersion===_0x722b8a(0x32c)&&getChromiumVersion()>0x3c)_0x3f4db2='Chrome\x20'+getChromiumVersion();else{if(Firefox)_0x3f4db2=_0x722b8a(0x81b);else navigator[_0x722b8a(0xb01)][_0x722b8a(0x565)](_0x722b8a(0x329))>=0x0&&(_0x3f4db2=_0x722b8a(0x872));}}var _0x39851e={'uuid':_0xab9a9c[_0x722b8a(0xab9)],'streamID':_0xab9a9c['streamID'],'label':_0xab9a9c[_0x722b8a(0x5e0)]||_0xab9a9c[_0x722b8a(0xa96)]||_0x722b8a(0x3cb),'browser':_0x3f4db2,'connections':[]};for(var _0x4f8ef9 in _0xab9a9c['pcs']){if(_0xab9a9c[_0x722b8a(0x84f)][_0x4f8ef9]){var _0xfefb47=_0xab9a9c[_0x722b8a(0x84f)][_0x4f8ef9],_0xf78a59={'peerUUID':_0x4f8ef9,'peerStreamID':_0xfefb47[_0x722b8a(0xa96)]||_0x4f8ef9,'direction':_0x722b8a(0x20e),'state':_0xfefb47[_0x722b8a(0xa83)]||'unknown','bandwidth':-0x1,'audioEnabled':!_0xab9a9c[_0x722b8a(0x8b2)],'videoEnabled':!_0xab9a9c['videoMuted'],'nackCount':0x0,'pliCount':0x0,'candidateType':_0xfefb47[_0x722b8a(0x287)]?_0xfefb47[_0x722b8a(0x287)]['candidateType_local']||'unknown':_0x722b8a(0x383)};_0xfefb47['stats']&&(_0xf78a59[_0x722b8a(0x87e)]=_0xfefb47[_0x722b8a(0x287)][_0x722b8a(0x87e)]||0x0,_0xf78a59[_0x722b8a(0x308)]=_0xfefb47['stats'][_0x722b8a(0x308)]||0x0),_0x39851e[_0x722b8a(0xa9a)][_0x722b8a(0x5e7)](_0xf78a59);}}for(var _0x4f8ef9 in _0xab9a9c[_0x722b8a(0x404)]){if(_0xab9a9c[_0x722b8a(0x404)][_0x4f8ef9]){var _0x550cc6=_0xab9a9c[_0x722b8a(0x404)][_0x4f8ef9],_0x4df1d3=_0x722b8a(0x383);_0x550cc6[_0x722b8a(0x287)]&&_0x550cc6['stats'][_0x722b8a(0xc09)]&&(_0x4df1d3=_0x550cc6['stats'][_0x722b8a(0xc09)][_0x722b8a(0xb74)]||'unknown');var _0xf78a59={'peerUUID':_0x4f8ef9,'peerStreamID':_0x550cc6[_0x722b8a(0xa96)]||_0x4f8ef9,'direction':_0x722b8a(0x23b),'state':_0x550cc6[_0x722b8a(0xa83)]||_0x722b8a(0x383),'bandwidth':_0x550cc6['bandwidth']||-0x1,'audioEnabled':!_0x550cc6[_0x722b8a(0x53f)],'videoEnabled':!_0x550cc6['videoMuted'],'nackCount':_0x550cc6[_0x722b8a(0x87e)]||0x0,'pliCount':_0x550cc6['pliCount']||0x0,'candidateType':_0x4df1d3};_0x39851e[_0x722b8a(0xa9a)][_0x722b8a(0x5e7)](_0xf78a59);}}_0x39851e[_0x722b8a(0xa26)]=_0x239a27,_0xab9a9c[_0x722b8a(0x1eb)]({'connectionMap':_0x39851e},_0x239a27),log(_0x722b8a(0x542)+_0x39851e['connections'][_0x722b8a(0xade)]+_0x722b8a(0x817));}_0x722b8a(0x2be)in _0x3c51bb&&changeVideoDeviceById(_0x3c51bb[_0x722b8a(0x2be)],_0x239a27);'requestChangeLowcut'in _0x3c51bb&&changeLowCut(parseFloat(_0x3c51bb[_0x722b8a(0xb58)]),_0x3c51bb['track']);_0x722b8a(0x64a)in _0x3c51bb&&changeLowCut(parseFloat(_0x3c51bb['value']),_0x3c51bb[_0x722b8a(0x50d)]);if(_0x722b8a(0xaba)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0xb38)]){if(_0x3c51bb[_0x722b8a(0x2ed)]&&_0xab9a9c['roomid'])try{var _0x158e74='vdo_block_'+sanitizeRoomName(_0xab9a9c[_0x722b8a(0x70d)])+'_'+(_0xab9a9c['hash']||'');setStorage(_0x158e74,!![],0x4),log(_0x722b8a(0xc65));}catch(_0x3f308c){errorlog(_0x722b8a(0xa5e)+_0x3f308c);}_0xab9a9c[_0x722b8a(0xaba)]();}}if(_0x722b8a(0x378)in _0x3c51bb){}if(_0x722b8a(0x57d)in _0x3c51bb){var _0x452b61=parseInt(_0x3c51bb[_0x722b8a(0x57d)])/0x64||0x0;_0xab9a9c[_0x722b8a(0xb47)]=parseInt(_0x3c51bb[_0x722b8a(0x57d)])||0x0;try{for(var _0x1fd8c6 in _0xab9a9c[_0x722b8a(0x635)]){log(_0x722b8a(0xae9)),_0xab9a9c[_0x722b8a(0x635)][_0x1fd8c6]['gainNode'][_0x722b8a(0x93f)][_0x722b8a(0x226)](_0x452b61,_0xab9a9c[_0x722b8a(0x635)][_0x1fd8c6][_0x722b8a(0x795)][_0x722b8a(0x674)]);}}catch(_0x19bad7){}updateVolume(!![]);}if('micIsolate'in _0x3c51bb){if(_0x3c51bb[_0x722b8a(0xab0)])_0xab9a9c[_0x722b8a(0x8a3)]['indexOf'](_0x4c574b||_0x239a27)>=0x0&&(_0xab9a9c[_0x722b8a(0xc36)][_0x722b8a(0x5e7)](_0x239a27),_0xab9a9c['applyIsolatedChat']());else{var _0x5d1cae=_0xab9a9c[_0x722b8a(0xc36)][_0x722b8a(0x565)](_0x239a27);_0x5d1cae>-0x1&&(_0xab9a9c[_0x722b8a(0xc36)]['splice'](_0x5d1cae,0x1),_0xab9a9c[_0x722b8a(0xba7)]());}}if(_0x722b8a(0x62e)in _0x3c51bb){if(_0x3c51bb[_0x722b8a(0x62e)])_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0&&(_0xab9a9c[_0x722b8a(0x62e)][_0x722b8a(0x5e7)](_0x239a27),_0xab9a9c[_0x722b8a(0x451)]());else{var _0x5d1cae=_0xab9a9c[_0x722b8a(0x62e)][_0x722b8a(0x565)](_0x239a27);_0x5d1cae>-0x1&&(_0xab9a9c['lowerVolume']['splice'](_0x5d1cae,0x1),_0xab9a9c[_0x722b8a(0x451)]());}}_0x722b8a(0x258)in _0x3c51bb&&(_0x3c51bb[_0x722b8a(0x258)]?(_0xab9a9c[_0x722b8a(0x50b)]=!![],_0xab9a9c[_0x722b8a(0x6ce)]()):(_0xab9a9c[_0x722b8a(0x50b)]=![],_0xab9a9c[_0x722b8a(0x6ce)]()));'displayMute'in _0x3c51bb&&(_0x3c51bb[_0x722b8a(0x254)]?(_0xab9a9c[_0x722b8a(0x1ee)]=!![],_0xab9a9c[_0x722b8a(0xcb9)]()):(_0xab9a9c[_0x722b8a(0x1ee)]=![],_0xab9a9c[_0x722b8a(0xcb9)]()));if(_0x722b8a(0x4f9)in _0x3c51bb){_0xab9a9c[_0x722b8a(0x4f9)]=_0x3c51bb['remoteVideoMuted'],toggleVideoMute(!![]);if(!_0xab9a9c[_0x722b8a(0x6cc)]){var _0x1048ae={};_0x1048ae[_0x722b8a(0x6cc)]=_0xab9a9c[_0x722b8a(0x4f9)],_0xab9a9c['sendMessage'](_0x1048ae);}}'changeParams'in _0x3c51bb&&applyNewParams(_0x3c51bb[_0x722b8a(0x875)]);}if(_0xab9a9c[_0x722b8a(0xb38)]===(_0x4c574b||_0x239a27)){if(_0x3c51bb[_0x722b8a(0x7aa)]===_0x722b8a(0x74e)){warnlog(_0x722b8a(0x895));if('transferSettings'in _0x3c51bb){_0x722b8a(0xbb7)in _0x3c51bb[_0x722b8a(0xbdc)]&&(_0xab9a9c['roomenc']=_0x3c51bb['roomenc']);'broadcast'in _0x3c51bb['transferSettings']&&(_0x3c51bb[_0x722b8a(0xbdc)]['broadcast']===!![]||_0x3c51bb['transferSettings']['broadcast']===null?(_0xab9a9c[_0x722b8a(0x4e2)]=null,_0xab9a9c[_0x722b8a(0x325)]===![]&&(_0xab9a9c[_0x722b8a(0x325)]=0x2),_0xab9a9c['style']===![]&&(_0xab9a9c[_0x722b8a(0x1b0)]=0x1),_0xab9a9c[_0x722b8a(0x591)]===null&&(_0xab9a9c[_0x722b8a(0x591)]=!![])):_0xab9a9c['broadcast']=_0x3c51bb['transferSettings'][_0x722b8a(0x4e2)],_0x3c51bb['transferSettings'][_0x722b8a(0x87a)]&&(_0xab9a9c[_0x722b8a(0x4e2)]!==![]?_0xab9a9c['broadcast']===null?updateURL(_0x722b8a(0x4e2),!![]):updateURL(_0x722b8a(0x2c8)+_0xab9a9c[_0x722b8a(0x4e2)],!![]):updateURL(_0x722b8a(0xba4),!![])));_0x722b8a(0x70d)in _0x3c51bb[_0x722b8a(0xbdc)]&&(_0xab9a9c[_0x722b8a(0x70d)]=_0x3c51bb[_0x722b8a(0xbdc)][_0x722b8a(0x70d)],_0x3c51bb[_0x722b8a(0xbdc)][_0x722b8a(0x87a)]&&updateURL(_0x722b8a(0xb56)+_0xab9a9c['roomid'],!![]));_0x722b8a(0x9fc)in _0x3c51bb[_0x722b8a(0xbdc)]&&(_0xab9a9c[_0x722b8a(0x9fc)]=_0x3c51bb['transferSettings'][_0x722b8a(0x9fc)],_0xab9a9c[_0x722b8a(0x9fc)]?!(_0x722b8a(0x5df)in _0x3c51bb['transferSettings'])&&(_0xab9a9c['queueType']=0x2):_0xab9a9c['queueType']=![],_0x3c51bb[_0x722b8a(0xbdc)][_0x722b8a(0x87a)]&&(_0xab9a9c['queue']?updateURL(_0x722b8a(0x9fc),!![]):updateURL(_0x722b8a(0x8fb),!![])));if(_0x722b8a(0x5df)in _0x3c51bb[_0x722b8a(0xbdc)])try{_0xab9a9c[_0x722b8a(0x9fc)]=!![],_0xab9a9c[_0x722b8a(0x5df)]=parseInt(_0x3c51bb['transferSettings'][_0x722b8a(0x5df)])||0x3;}catch(_0x498cff){errorlog(_0x498cff);}_0x722b8a(0x31b)in _0x3c51bb['transferSettings']&&(_0xab9a9c[_0x722b8a(0x9fc)]&&(_0xab9a9c['queue']=_0xab9a9c[_0x722b8a(0x5df)]||0x3,_0x3c51bb[_0x722b8a(0xbdc)][_0x722b8a(0x87a)]&&updateURL(_0x722b8a(0x8fb),!![])));}_0xab9a9c[_0x722b8a(0xbde)]&&_0xab9a9c[_0x722b8a(0x624)]&&(_0xab9a9c['layout']=![],_0xab9a9c['waitPage']=![],updateMixer());}try{if(_0x722b8a(0xc90)in _0x3c51bb&&_0x722b8a(0x770)in _0x3c51bb[_0x722b8a(0xc90)]){var _0x5ec026=_0x4c574b||_0x239a27;if(_0xab9a9c[_0x722b8a(0xb38)]&&_0x5ec026===_0xab9a9c['directorUUID'])for(var _0x4d1aee=0x0;_0x4d1aee<_0x3c51bb[_0x722b8a(0xc90)][_0x722b8a(0x770)][_0x722b8a(0xade)];_0x4d1aee++){var _0x576267=_0x3c51bb[_0x722b8a(0xc90)][_0x722b8a(0x770)][_0x4d1aee][_0x722b8a(0xc7a)]();if(!_0xab9a9c['directorList'][_0x722b8a(0xa4f)](_0x576267)){_0xab9a9c['directorList']['push'](_0x576267);var _0x1a0851=getById(_0x722b8a(0x66f)+_0x576267);_0x1a0851&&_0x1a0851['classList'][_0x722b8a(0xada)]('directorBlue');}_0x576267 in _0xab9a9c['pcs']&&_0xab9a9c[_0x722b8a(0x84f)][_0x576267][_0x722b8a(0x98a)]&&_0xab9a9c[_0x722b8a(0x5df)]==0x4&&_0xab9a9c['initialPublish'](_0x576267);if(_0x576267 in _0xab9a9c['pcs'])try{var _0x1048ae={};_0x1048ae[_0x722b8a(0xab9)]=_0x576267,_0x1048ae[_0x722b8a(0xa3f)]=listAudioSettingsPrep(),sendMediaDevices(_0x1048ae['UUID']),_0xab9a9c[_0x722b8a(0x1eb)](_0x1048ae,_0x1048ae[_0x722b8a(0xab9)]);}catch(_0xd7ef55){}}}}catch(_0x20fb8e){errorlog(_0x20fb8e);}if('cbid'in _0x3c51bb)try{_0xab9a9c[_0x722b8a(0x1eb)]({'cbid':_0x3c51bb['cbid']},_0x239a27);}catch(_0x401b16){errorlog(_0x401b16);}}if(_0x722b8a(0x23d)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0||_0xab9a9c[_0x722b8a(0x1d9)]===!![]||_0xab9a9c[_0x722b8a(0x1d9)]&&_0x722b8a(0x1d9)in _0x3c51bb&&_0x3c51bb['remote']===_0xab9a9c[_0x722b8a(0x1d9)])_0x722b8a(0x5a4)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x5a4)]?updateCameraConstraints(_0x3c51bb[_0x722b8a(0x48c)],_0x3c51bb[_0x722b8a(0xb58)],!![],_0x239a27):updateCameraConstraints(_0x3c51bb[_0x722b8a(0x48c)],_0x3c51bb['value'],![],![]);else return;}if('zoom'in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0||_0xab9a9c[_0x722b8a(0x1d9)]===!![]||_0xab9a9c[_0x722b8a(0x1d9)]&&_0x722b8a(0x1d9)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x1d9)]===_0xab9a9c[_0x722b8a(0x1d9)])_0xab9a9c['remoteZoom'](parseFloat(_0x3c51bb[_0x722b8a(0x94e)]),_0x3c51bb['abs']||![]);else return;}if(_0x722b8a(0x47e)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0||_0xab9a9c['remote']===!![]||_0xab9a9c[_0x722b8a(0x1d9)]&&_0x722b8a(0x1d9)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x1d9)]===_0xab9a9c[_0x722b8a(0x1d9)])_0xab9a9c[_0x722b8a(0x437)](parseFloat(_0x3c51bb[_0x722b8a(0x47e)]),_0x3c51bb[_0x722b8a(0x76f)]||![]);else return;}if(_0x722b8a(0x97c)in _0x3c51bb){if(_0xab9a9c['directorList']['indexOf'](_0x4c574b||_0x239a27)>=0x0||_0xab9a9c[_0x722b8a(0x1d9)]===!![]||_0xab9a9c[_0x722b8a(0x1d9)]&&'remote'in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x1d9)]===_0xab9a9c[_0x722b8a(0x1d9)])_0xab9a9c['setRemoteAutofocus'](_0x3c51bb[_0x722b8a(0x97c)]);else return;}if(_0x722b8a(0x82b)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x8a3)]['indexOf'](_0x4c574b||_0x239a27)>=0x0||_0xab9a9c[_0x722b8a(0x1d9)]===!![]||_0xab9a9c[_0x722b8a(0x1d9)]&&_0x722b8a(0x1d9)in _0x3c51bb&&_0x3c51bb['remote']===_0xab9a9c[_0x722b8a(0x1d9)])_0xab9a9c[_0x722b8a(0x80a)](parseFloat(_0x3c51bb[_0x722b8a(0x82b)]),_0x3c51bb[_0x722b8a(0x76f)]||![]);else return;}if(_0x722b8a(0xcd6)in _0x3c51bb){if(_0xab9a9c['directorList'][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0||_0xab9a9c[_0x722b8a(0x1d9)]===!![]||_0xab9a9c['remote']&&_0x722b8a(0x1d9)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x1d9)]===_0xab9a9c[_0x722b8a(0x1d9)])_0xab9a9c[_0x722b8a(0x40b)](parseFloat(_0x3c51bb[_0x722b8a(0xcd6)]),_0x3c51bb[_0x722b8a(0x76f)]||![]);else return;}if(_0x722b8a(0x63b)in _0x3c51bb){if(_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x4c574b||_0x239a27)>=0x0||_0xab9a9c[_0x722b8a(0x1d9)]===!![]||_0xab9a9c[_0x722b8a(0x1d9)]&&_0x722b8a(0x1d9)in _0x3c51bb&&_0x3c51bb['remote']===_0xab9a9c['remote'])_0xab9a9c[_0x722b8a(0x6ad)](parseFloat(_0x3c51bb[_0x722b8a(0x63b)]),_0x3c51bb[_0x722b8a(0x76f)]||![]);else return;}if('requestFile'in _0x3c51bb){log(_0x722b8a(0x952));try{_0xab9a9c['sendFile'](_0x239a27,_0x3c51bb[_0x722b8a(0x952)]);}catch(_0x46886e){errorlog(_0x46886e);}}'midi'in _0x3c51bb&&playbackMIDI(_0x3c51bb[_0x722b8a(0x2fd)],!![],_0x239a27);}catch(_0x4a69fa){errorlog(_0x4a69fa);}if(_0x722b8a(0x923)in _0x3c51bb){if(_0x3c51bb[_0x722b8a(0x923)]==_0x722b8a(0xad8)){if(_0xab9a9c[_0x722b8a(0x1d9)])warnUser(getTranslation(_0x722b8a(0x4b1)),0xbb8);else document[_0x722b8a(0xac5)](_0x722b8a(0xcc3))&&document[_0x722b8a(0xac5)](_0x722b8a(0xcc3))['value']?warnUser(getTranslation(_0x722b8a(0x9e7)),0x1b58):warnUser(getTranslation(_0x722b8a(0x401)),0x2710);getById('obsRemotePassword')['classList'][_0x722b8a(0x50f)](_0x722b8a(0x63d));}else{if(_0xab9a9c[_0x722b8a(0x827)])!_0xab9a9c[_0x722b8a(0xca4)]&&warnUser('The\x20request\x20('+_0x3c51bb[_0x722b8a(0x923)]+_0x722b8a(0x86d),0x1388);else!_0xab9a9c[_0x722b8a(0xca4)]&&(_0xab9a9c[_0x722b8a(0x1d9)]?warnUser(_0x722b8a(0x884),0x1388):warnUser(_0x722b8a(0xcac),0x1388));}errorlog('ACTION\x20REJECTED:\x20'+_0x3c51bb[_0x722b8a(0x923)]+',\x20isDirector:\x20'+_0xab9a9c[_0x722b8a(0x827)]),pokeIframeAPI(_0x722b8a(0x923),_0x3c51bb[_0x722b8a(0x923)],_0x239a27);return;}else{if(_0x722b8a(0xb61)in _0x3c51bb){log(_0x722b8a(0x733)+_0x3c51bb[_0x722b8a(0xb61)]),pokeIframeAPI(_0x722b8a(0xb61),_0x3c51bb[_0x722b8a(0xb61)],_0x239a27);return;}}if(_0x722b8a(0x544)in _0x3c51bb||'video'in _0x3c51bb){log(_0x722b8a(0x850));_0x3c51bb[_0x722b8a(0x544)]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x8c0)]=_0x3c51bb['audio']);if(_0xab9a9c['webp']&&_0x722b8a(0xbbb)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0xbbb)]!==![])_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xb2b)]=_0x3c51bb[_0x722b8a(0xbbb)],_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x295)]=![],setTimeout(function(){makeImages(!![]);},0x3e8);else _0x3c51bb[_0x722b8a(0xa55)]&&(_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x295)]=_0x3c51bb[_0x722b8a(0xa55)]);_0x722b8a(0x4e2)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x4e2)]!==![]&&(_0xab9a9c['pcs'][_0x239a27]['allowBroadcast']=_0x3c51bb[_0x722b8a(0x4e2)]);_0x722b8a(0xa99)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0xa99)]!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xae2)]=_0x3c51bb[_0x722b8a(0xa99)]);if(_0x722b8a(0xb51)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0xb51)]){_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['allowDrawing']=_0x3c51bb[_0x722b8a(0xb51)];try{_0xab9a9c[_0x722b8a(0x7eb)]&&_0xab9a9c['videoElement'][_0x722b8a(0x201)]&&_0xab9a9c['videoElement'][_0x722b8a(0x201)]();}catch(_0x118bd9){errorlog(_0x118bd9);}}'iframe'in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x275)]!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9fd)]=_0x3c51bb[_0x722b8a(0x275)]);const _0x126f2d=!(_0x722b8a(0x7ae)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x7ae)]===![]||'allowscreenwhipout'in _0x3c51bb&&_0x3c51bb[_0x722b8a(0xaa3)]===![]);_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x658)]=_0x126f2d;_0x722b8a(0x8cf)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x8cf)]!==![]&&(_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x670)]=_0x3c51bb[_0x722b8a(0x8cf)]);_0x722b8a(0x873)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x873)]!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xc38)]=_0x3c51bb[_0x722b8a(0x873)]);_0x722b8a(0xbae)in _0x3c51bb&&_0x3c51bb['allowresources']!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x61e)]=_0x3c51bb['allowresources']);'downloads'in _0x3c51bb&&_0x3c51bb['downloads']!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x918)]=_0x3c51bb[_0x722b8a(0x214)]);_0x722b8a(0xa7f)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0xa7f)]!==![]&&(_0xab9a9c['pcs'][_0x239a27]['allowScreenAudio']=!![],_0xab9a9c['pcs'][_0x239a27]['allowScreenVideo']=!![]);'allowscreenvideo'in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x25a)]!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xb3f)]=!![]);'allowscreenaudio'in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x8f1)]!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xbe2)]=!![]);if(_0xab9a9c[_0x722b8a(0xa42)])_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0xa42)]=_0xab9a9c[_0x722b8a(0xa42)];else _0x722b8a(0xa42)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0xa42)]!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xa42)]=_0x3c51bb[_0x722b8a(0xa42)]['toLowerCase']());if(_0xab9a9c[_0x722b8a(0x3c8)])_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x3c8)]=_0xab9a9c['preferAudioCodec'];else _0x722b8a(0x3c8)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x3c8)]!==![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x3c8)]=_0x3c51bb[_0x722b8a(0x3c8)]['toLowerCase']());'allowscreenmeshcast'in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x7ae)]===![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x829)]=![]);'allowscreenwhipout'in _0x3c51bb&&_0x3c51bb[_0x722b8a(0xaa3)]===![]&&(_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x829)]=![]);if(_0x722b8a(0x46d)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0x46d)]===![])_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x81c)]=![];else{if(_0x722b8a(0xc66)in _0x3c51bb&&_0x3c51bb[_0x722b8a(0xc66)]===![])_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x81c)]=![];else{if(_0xab9a9c[_0x722b8a(0xb87)]){if(_0xab9a9c['meshcast']==_0x722b8a(0xa55))_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x295)]=![];else{if(_0xab9a9c[_0x722b8a(0xb87)]==_0x722b8a(0x544))_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x8c0)]=![];else _0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x295)]==![]?_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['whipout']=![]:(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x8c0)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x295)]=![]);}}else _0xab9a9c[_0x722b8a(0x824)]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x8c0)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x295)]=![]);}}_0xab9a9c['whipPublishScreen']&&_0xab9a9c[_0x722b8a(0xc28)]&&_0x126f2d&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xbe2)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xb3f)]=![]);_0xab9a9c[_0x722b8a(0x69a)]&&_0xab9a9c[_0x722b8a(0x69a)][_0x722b8a(0x241)]&&broadcastWhepSettings(_0x722b8a(0xbc0));_0xab9a9c['whipPublishScreen']&&_0xab9a9c[_0x722b8a(0xc15)]&&_0xab9a9c[_0x722b8a(0xb26)]&&_0xab9a9c[_0x722b8a(0xb26)][_0x722b8a(0x241)]&&_0xab9a9c[_0x722b8a(0xb26)][_0x722b8a(0x6d4)]&&broadcastWhepSettings(_0x722b8a(0x30b));if(_0xab9a9c[_0x722b8a(0xa12)]){window[_0x722b8a(0x47e)]();_0xab9a9c[_0x722b8a(0xb2c)]&&playtone();var _0x204b33=![];_0x239a27 in _0xab9a9c[_0x722b8a(0x404)]&&_0xab9a9c[_0x722b8a(0x404)][_0x239a27][_0x722b8a(0x5e0)]&&(_0x204b33=_0xab9a9c[_0x722b8a(0x404)][_0x239a27]['label']||_0xab9a9c[_0x722b8a(0x404)][_0x239a27]['streamID']||![]);_0x204b33=_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x5e0)]||_0x204b33||_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0xa96)]||_0x239a27||_0x722b8a(0x431);var _0xc45c0f=await confirmAlt(_0x204b33+getTranslation(_0x722b8a(0x37d)),!![]);if(!_0xc45c0f){try{log('closing\x2013'),_0xab9a9c['closePC'](_0x239a27);}catch(_0x5a3462){}return;}}_0x722b8a(0x1ab)in _0x3c51bb&&(_0x3c51bb[_0x722b8a(0x1ab)]==!![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['guest']=!![],_0xab9a9c[_0x722b8a(0xb2c)]&&(playtone(![],'jointone'),showNotification(_0x722b8a(0xb4b),'')),pokeIframeAPI('guest-connected',_0x3c51bb[_0x722b8a(0x827)],_0x239a27)));_0x722b8a(0xcb7)in _0x3c51bb&&(_0x3c51bb['forceios']===!![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xcb7)]=!![]));_0x722b8a(0x1d9)in _0x3c51bb&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x1d9)]=_0x3c51bb[_0x722b8a(0x1d9)]);_0x722b8a(0xc87)in _0x3c51bb&&(_0x3c51bb[_0x722b8a(0xc87)]==!![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x6e6)]=!![]));'enhanceaudio'in _0x3c51bb&&(_0x3c51bb[_0x722b8a(0x2cd)]==!![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x2ba)]=!![]));_0x3c51bb[_0x722b8a(0x2e1)]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x48d)]=_0x3c51bb[_0x722b8a(0x2e1)]);if(_0x722b8a(0x85e)in _0x3c51bb)try{_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x85e)]=_0x3c51bb[_0x722b8a(0x85e)],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x85e)]&&setTimeout(function(_0x2306d0){var _0x3e8f2a=_0x722b8a;_0xab9a9c[_0x3e8f2a(0xa03)](_0x2306d0);},0x1388,_0x239a27);}catch(_0x243ce1){warnlog(_0x243ce1);}_0x722b8a(0x585)in _0x3c51bb&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x585)]=_0x3c51bb['solo']);if(_0x722b8a(0x624)in _0x3c51bb){const _0x54a414=_0x3c51bb[_0x722b8a(0x624)];if(_0x54a414&&_0x54a414!==!![]&&_0x54a414!==_0x722b8a(0x372)&&_0x54a414!==![]&&_0x54a414!=='false')_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['layout']=_0x54a414;else!_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x624)]&&(_0x54a414===!![]||_0x54a414===_0x722b8a(0x372))&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x624)]=_0x54a414);const _0x2d5ae5=normalizeLayoutState(_0x54a414);if(typeof _0x2d5ae5!=='undefined')_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x6b7)]=_0x2d5ae5;else typeof _0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x6b7)]===_0x722b8a(0x2cf)&&(_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x6b7)]=![]);}if('scene'in _0x3c51bb){if(_0x3c51bb[_0x722b8a(0x268)]!==![]){try{typeof _0x3c51bb[_0x722b8a(0x268)]==='string'?_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['scene']=_0x3c51bb['scene'][_0x722b8a(0x35b)](/[\W]+/g,'_'):_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x268)]=(parseInt(_0x3c51bb[_0x722b8a(0x268)])||0x0)+'',_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x287)][_0x722b8a(0x268)]=_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x268)],updateSceneList(_0xab9a9c['pcs'][_0x239a27]['scene']);}catch(_0x51f430){errorlog(_0x51f430);}_0x722b8a(0x990)in _0x3c51bb?_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x990)]=_0x3c51bb['showDirector']:_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x990)]=_0xab9a9c[_0x722b8a(0x990)];if(_0xab9a9c[_0x722b8a(0x827)]){if(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x990)]==![])_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x8c0)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x295)]=![],_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x9fd)]=![],_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x9e1)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x670)]=![],_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x81c)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xb2b)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xbe2)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xb3f)]=![];else{if(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x990)]==0x1)_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9fd)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['allowWidget']=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9e1)]=![];else{if(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['showDirector']==0x2)_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x8c0)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xbe2)]=![],_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x9fd)]=![],_0xab9a9c['pcs'][_0x239a27]['allowWidget']=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['allowDrawing']=![];else{if(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['showDirector']==0x3)_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x8c0)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x295)]=![],_0xab9a9c['pcs'][_0x239a27][_0x722b8a(0x9fd)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x670)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x81c)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xb2b)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9e1)]=![];else{if(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x990)]==0x4){}}}}}broadcastSlotUpdate(_0x239a27);}_0xab9a9c['pcs'][_0x239a27]['solo']?pokeIframeAPI(_0x722b8a(0x7fb),_0x3c51bb[_0x722b8a(0x268)],_0x239a27):pokeIframeAPI(_0x722b8a(0x427),_0x3c51bb[_0x722b8a(0x268)],_0x239a27);}_0xab9a9c['initialDirectorSync'](_0x239a27);}else _0x3c51bb[_0x722b8a(0x827)]&&((iOS||iPad)&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xcb7)]==!![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['guest']=!![])),_0xab9a9c[_0x722b8a(0xb2c)]&&(playtone(![],_0x722b8a(0x837)),showNotification(_0x722b8a(0x782),'Trying\x20to\x20join\x20at\x20least')),_0xab9a9c[_0x722b8a(0x705)](_0x239a27),pokeIframeAPI(_0x722b8a(0x869),_0x3c51bb[_0x722b8a(0x827)],_0x239a27));if(_0xab9a9c[_0x722b8a(0x827)])_0x722b8a(0x902)in _0x3c51bb&&(_0x3c51bb[_0x722b8a(0x902)]==!![]&&(_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x8c0)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x295)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['allowIframe']=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['allowWidget']=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x81c)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xb2b)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xbe2)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0xb3f)]=![],_0xab9a9c[_0x722b8a(0x84f)][_0x239a27][_0x722b8a(0x9e1)]=![])),_0xab9a9c[_0x722b8a(0x1f4)](_0x239a27);else{if(_0xab9a9c['queueType']==0x3&&!_0xab9a9c[_0x722b8a(0x827)])_0xab9a9c[_0x722b8a(0x84f)][_0x239a27]['needsPublishing']=!![];else _0xab9a9c[_0x722b8a(0x5df)]==0x4&&!_0xab9a9c[_0x722b8a(0x827)]?_0xab9a9c[_0x722b8a(0x8a3)][_0x722b8a(0x565)](_0x239a27)>=0x0?_0xab9a9c[_0x722b8a(0x1f4)](_0x239a27):_0xab9a9c['pcs'][_0x239a27]['needsPublishing']=!![]:_0xab9a9c[_0x722b8a(0x1f4)](_0x239a27);}}},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x5cc)][_0x27dbee(0xb95)]=_0x3a79e2=>_0xab9a9c[_0x27dbee(0x3a9)](_0x3a79e2,_0x2b7d37);function _0x34a504(){var _0x3361bf=_0x27dbee;try{var _0x29f8f7=document['querySelector']('[data-action-type=\x22solo-video\x22].altpress');if(_0x29f8f7)return{'type':'infocus2','target':_0x17b81a(_0x29f8f7)};var _0xeaaf8b=document[_0x3361bf(0xac5)](_0x3361bf(0x6f5));if(_0xeaaf8b)return{'type':_0x3361bf(0x940),'target':_0x17b81a(_0xeaaf8b)};var _0x6f1c57=typeof getById===_0x3361bf(0x32c)?getById('highlightDirector'):document[_0x3361bf(0x2b2)](_0x3361bf(0x32a));if(_0x6f1c57){var _0x3703d7=_0x17b81a(_0x6f1c57);if(_0x6f1c57[_0x3361bf(0xb42)]){if(_0x6f1c57[_0x3361bf(0xc93)]&&_0x6f1c57[_0x3361bf(0xc93)]['contains']('altpress'))return{'type':_0x3361bf(0x641),'target':_0x3703d7};return{'type':'infocus','target':_0x3703d7};}}}catch(_0x4a1680){errorlog(_0x4a1680);}return null;}function _0x17b81a(_0x4170df){var _0x58d2ce=_0x27dbee;if(!_0x4170df)return![];try{if(_0x4170df[_0x58d2ce(0xa18)]&&_0x4170df['dataset'][_0x58d2ce(0x2e8)])return _0x4170df['dataset'][_0x58d2ce(0x2e8)];}catch(_0x4c0fe2){}if(_0x4170df&&_0x4170df['id']===_0x58d2ce(0x32a)&&_0xab9a9c&&_0xab9a9c[_0x58d2ce(0xa96)])return _0xab9a9c[_0x58d2ce(0xa96)];return![];}_0xab9a9c['initialDirectorSync']=function(_0x5b99fd){var _0x3e0cbc=_0x27dbee;if(!(_0xab9a9c[_0x3e0cbc(0x40a)]||_0xab9a9c[_0x3e0cbc(0x268)]))return;try{var _0xb80e44={};_0xab9a9c[_0x3e0cbc(0x84f)][_0x5b99fd]&&(_0xb80e44[_0x3e0cbc(0xc90)]=getDirectorSettings(_0xab9a9c[_0x3e0cbc(0x84f)][_0x5b99fd][_0x3e0cbc(0x268)]));log('TRYING\x20TO\x20SYNC\x20WITH\x20SENDING:\x20'+_0x5b99fd);var _0x38576a=![];_0xab9a9c['alreadyJoinedMembers']&&_0xab9a9c['alreadyJoinedMembers'][_0x3e0cbc(0x306)](_0x425ee0=>{var _0x8aaf1=_0x3e0cbc;_0x425ee0[_0x8aaf1(0xab9)]===_0x5b99fd&&(_0x38576a=!![]);});!_0x38576a||_0xab9a9c[_0x3e0cbc(0x84f)][_0x5b99fd]&&_0xab9a9c[_0x3e0cbc(0x84f)][_0x5b99fd][_0x3e0cbc(0x352)]===!![]?_0xb80e44['directorState']=getDetailedState():warnlog(_0x3e0cbc(0x2dc));if(_0xab9a9c[_0x3e0cbc(0x827)]){var _0x37c3f8=_0x34a504();if(_0x37c3f8&&_0xab9a9c['pcs'][_0x5b99fd]){var _0x28aa8=_0x37c3f8['target'];if(_0x28aa8!==![]){var _0x37d233=_0xab9a9c[_0x3e0cbc(0x84f)][_0x5b99fd][_0x3e0cbc(0x6b7)],_0x3e19f3=!![];try{if(typeof isAutoLayoutState===_0x3e0cbc(0x32c))_0x3e19f3=isAutoLayoutState(_0x37d233);else{if(typeof normalizeLayoutStateValue===_0x3e0cbc(0x32c)){var _0x52f3d8=normalizeLayoutStateValue(_0x37d233);_0x3e19f3=_0x52f3d8===![]||typeof _0x52f3d8==='undefined'||_0x52f3d8===null;}else _0x3e19f3=!_0x37d233||_0x37d233===![];}}catch(_0x1d1494){errorlog(_0x1d1494);}if(!_0xab9a9c['pcs'][_0x5b99fd]['solo']&&_0x3e19f3){var _0x790e8e={};_0x790e8e[_0x37c3f8['type']]=_0x28aa8,_0xab9a9c[_0x3e0cbc(0x1eb)](_0x790e8e,_0x5b99fd);}}}}Object['keys'](_0xb80e44)[_0x3e0cbc(0xade)]&&_0xab9a9c[_0x3e0cbc(0xae3)](_0xb80e44,_0x5b99fd);}catch(_0x403452){}},_0xab9a9c[_0x27dbee(0x803)]=function(_0x2358dd){var _0x55bc67=_0x27dbee;log(_0x55bc67(0xc39));if(!_0xab9a9c['hostedFiles']||!_0xab9a9c[_0x55bc67(0xa9f)][_0x55bc67(0xade)])return;var _0x23ac24={},_0x7e0430=[];for(var _0xfc9adc=0x0;_0xfc9adc<_0xab9a9c['hostedFiles'][_0x55bc67(0xade)];_0xfc9adc++){(_0xab9a9c['hostedFiles'][_0xfc9adc][_0x55bc67(0x394)]===![]||_0xab9a9c[_0x55bc67(0xa9f)][_0xfc9adc][_0x55bc67(0x394)]===_0x2358dd)&&_0x7e0430[_0x55bc67(0x5e7)]({'id':_0xab9a9c[_0x55bc67(0xa9f)][_0xfc9adc]['id'],'name':_0xab9a9c[_0x55bc67(0xa9f)][_0xfc9adc]['name'],'size':_0xab9a9c[_0x55bc67(0xa9f)][_0xfc9adc][_0x55bc67(0x53c)]});}_0x23ac24[_0x55bc67(0x706)]=_0x7e0430;if(_0x2358dd in _0xab9a9c[_0x55bc67(0x84f)])_0xab9a9c[_0x55bc67(0x1eb)](_0x23ac24,_0x2358dd);else _0x2358dd in _0xab9a9c['rpcs']&&_0xab9a9c['sendRequest'](_0x23ac24,_0x2358dd);log(_0x23ac24);},_0xab9a9c['initialPublish']=function(_0x4596e9){var _0x357f02=_0x27dbee;log(_0x357f02(0x266)+_0x4596e9);if(_0x4596e9 in _0xab9a9c[_0x357f02(0x84f)])_0xab9a9c[_0x357f02(0x84f)][_0x4596e9]['needsPublishing']=![];else{errorlog('UUID\x20not\x20found\x20in\x20pcs');return;}getSenders2(_0x4596e9)[_0x357f02(0xade)]&&errorlog(_0x357f02(0x522)+getSenders2(_0x4596e9)['length']);if(_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0x9fd)]===!![]){if(_0xab9a9c[_0x357f02(0x7dc)]){var _0x27bd1c={};_0x27bd1c[_0x357f02(0x7dc)]=_0xab9a9c['iframeSrc'],_0xab9a9c[_0x357f02(0x7ca)]&&_0xab9a9c['iframeEle']['sendOnNewConnect']&&(_0xab9a9c[_0x357f02(0x7dc)]['startsWith'](_0x357f02(0xc07))&&(_0x27bd1c[_0x357f02(0x7dc)]+=_0x357f02(0x5bf)+parseInt(Math[_0x357f02(0xc48)](_0xab9a9c['iframeEle'][_0x357f02(0x7c8)][_0x357f02(0x48a)]['t']))+'')),_0xab9a9c['sendMessage'](_0x27bd1c,_0x4596e9);}}if(_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0x670)]===!![]){if(_0xab9a9c[_0x357f02(0x8cf)]&&_0xab9a9c['director']){var _0x27bd1c={};_0x27bd1c[_0x357f02(0x1e1)]=_0xab9a9c[_0x357f02(0x8cf)],_0xab9a9c[_0x357f02(0x1eb)](_0x27bd1c,_0x4596e9);}}_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0x918)]===!![]&&_0xab9a9c[_0x357f02(0x803)](_0x4596e9);_0xab9a9c['pcs'][_0x4596e9][_0x357f02(0x61e)]===!![]&&_0xab9a9c[_0x357f02(0x488)](_0x4596e9);let _0x25a44a=![];if(_0xab9a9c[_0x357f02(0x86f)]&&_0xab9a9c['pcs'][_0x4596e9][_0x357f02(0xae2)]){_0xab9a9c[_0x357f02(0x21b)](_0x4596e9);if(_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0xae2)]!==0x2)return;_0x25a44a=!![];}var _0x1168b=_0xab9a9c[_0x357f02(0x27d)]();log(_0x357f02(0x463)),log(_0x1168b[_0x357f02(0xb93)]());if(_0xab9a9c[_0x357f02(0x69a)]&&_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0x81c)]===null){_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0x81c)]=!![];var _0x27bd1c={};_0x27bd1c[_0x357f02(0x20a)]=_0xab9a9c['whipoutSettings'],_0xab9a9c[_0x357f02(0x1eb)](_0x27bd1c,_0x4596e9),warnlog(_0x27bd1c);}!_0x25a44a&&(_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0xb3f)]||_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0xbe2)])&&createSecondStream2(_0x4596e9);var _0x3739f1=![];!_0x25a44a&&_0x1168b[_0x357f02(0x5c7)]()[_0x357f02(0x306)](async _0x270d4d=>{var _0xd506b7=_0x357f02;try{if(_0xab9a9c['pcs'][_0x4596e9][_0xd506b7(0x295)]===!![]){if(_0x270d4d[_0xd506b7(0xcd1)]==_0xd506b7(0xa55)){if(_0xab9a9c[_0xd506b7(0x84f)][_0x4596e9][_0xd506b7(0x1ab)]===!![]&&_0xab9a9c['roombitrate']===0x0)log(_0xd506b7(0x946));else{let _0x128b8e=_0xab9a9c[_0xd506b7(0x84f)][_0x4596e9][_0xd506b7(0x311)](_0x270d4d,_0x1168b);if(_0x128b8e&&_0xab9a9c[_0xd506b7(0x288)])try{setupSenderTransform(_0x128b8e,_0x4596e9);}catch(_0x280acd){errorlog(_0x280acd);}warnlog(_0xd506b7(0x343)),_0x3739f1=!![],setTimeout(function(_0x1d2eaf){var _0x2508e6=_0xd506b7;try{_0xab9a9c[_0x2508e6(0x4da)](_0x1d2eaf);}catch(_0x5c6c52){warnlog(_0x5c6c52);}},_0xab9a9c[_0xd506b7(0x2b5)],_0x4596e9);}}}}catch(_0x268d18){errorlog(_0x268d18);}});if(_0xab9a9c[_0x357f02(0x5de)]){if(_0xab9a9c[_0x357f02(0x85c)]){var _0x21b7dd=createDirectorMixMinusForGuest(_0x4596e9);_0x21b7dd&&(_0x1168b=_0x21b7dd,onGuestJoinedMixMinus(_0x4596e9));}else _0x1168b=mixMinusAudio(_0x4596e9);}_0xab9a9c[_0x357f02(0x84f)][_0x4596e9]['allowAudio']&&(_0x1168b['getAudioTracks']()['forEach'](_0x5c7d77=>{var _0x196b28=_0x357f02;try{_0x5c7d77[_0x196b28(0xcd1)]=='audio'&&(_0xab9a9c[_0x196b28(0x84f)][_0x4596e9][_0x196b28(0x311)](_0x5c7d77,_0x1168b),warnlog(_0x196b28(0x6d2)));}catch(_0x162874){errorlog(_0x162874);}}),log(_0x357f02(0x9ba)),_0x1168b['getAudioTracks']()['length']&&(_0xab9a9c[_0x357f02(0x827)]!==![]&&_0xab9a9c[_0x357f02(0x9be)](),log('starting\x20kicker'),_0xab9a9c['pcs'][_0x4596e9]['limitAudio']===!![]&&(warnlog(_0x357f02(0xc92)),setTimeout(_0xab9a9c[_0x357f02(0xc9b)],0x3e8,_0x4596e9,0x7d00,0x0)),_0xab9a9c['pcs'][_0x4596e9][_0x357f02(0x2ba)]===!![]&&setTimeout(_0xab9a9c[_0x357f02(0x4ab)],0x3e8,_0x4596e9)));if(_0xab9a9c['pcs'][_0x4596e9]['degradationPreference'])setTimeout(_0xab9a9c[_0x357f02(0x48d)],0x3e8,_0x4596e9,_0xab9a9c[_0x357f02(0x84f)][_0x4596e9][_0x357f02(0x48d)]);else{if(_0xab9a9c['contentHint']&&SafariVersion){if(_0xab9a9c[_0x357f02(0xa15)]=='detail')setTimeout(_0xab9a9c[_0x357f02(0x48d)],0x3e8,_0x4596e9,_0x357f02(0x607));else _0xab9a9c[_0x357f02(0xa15)]==_0x357f02(0x399)&&setTimeout(_0xab9a9c[_0x357f02(0x48d)],0x3e8,_0x4596e9,_0x357f02(0x48b));}}if(iOS||iPad){if(SafariVersion&&SafariVersion<=0xd){}else _0x3739f1&&(setTimeout(function(_0x3d5989){_0xab9a9c['setScale'](_0x3d5989,null,!![]);},0x7d0,_0x4596e9),setTimeout(function(_0x13e87e){var _0x3ce12b=_0xab9a9c['refreshScale'](_0x13e87e);!_0x3ce12b&&_0xab9a9c['setScale'](_0x13e87e,0x64,!![]);},0x1388,_0x4596e9));}else setTimeout(function(_0x53ea41){_0xab9a9c['refreshScale'](_0x53ea41);},0x3e8,_0x4596e9);},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x678)]=function(_0x25e557){var _0x2463c3=_0x27dbee;if(!(_0x2b7d37 in _0xab9a9c[_0x2463c3(0x84f)]))return;try{if(this[_0x2463c3(0x413)]==='closed')log(_0x2463c3(0x64e));else{if(this[_0x2463c3(0x413)]===_0x2463c3(0x922))log(_0x2463c3(0x337));else{if(this[_0x2463c3(0x413)]===_0x2463c3(0xa40))log(_0x2463c3(0x229));else this[_0x2463c3(0x413)]===_0x2463c3(0x497)?log(_0x2463c3(0x88a)):log(this[_0x2463c3(0x413)]);}}}catch(_0x438d42){errorlog(_0x438d42);}},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x99e)]=function(_0xfb58d7){var _0x58388f=_0x27dbee;switch(_0xab9a9c['pcs'][_0x2b7d37][_0x58388f(0xa83)]){case _0x58388f(0x497):log(_0x58388f(0xb16)),clearTimeout(_0xab9a9c['pcs'][_0x2b7d37]['closeTimeout']);if(_0xab9a9c[_0x58388f(0x6b3)]&&_0xab9a9c['qosData']){_0xab9a9c[_0x58388f(0xa06)][_0x58388f(0x8f7)]++;try{setTimeout(processPcsQosStats,0x1f4,_0x2b7d37);}catch(_0x3c04d7){}}if(_0xab9a9c[_0x58388f(0x915)]){if(_0xab9a9c['ws'][_0x58388f(0x70b)]!==0x1){_0xab9a9c['ws'][_0x58388f(0x3ef)]();break;}_0xab9a9c['ws']['close'](),setTimeout(function(){var _0x575d61=_0x58388f;_0xab9a9c[_0x575d61(0xca4)]!=!![]&&warnUser(getTranslation('remote-peer-connected'));},0x1);}break;case'disconnected':log(_0x58388f(0xc0f)),clearTimeout(_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37][_0x58388f(0x808)]);try{const _0x343a23=Date[_0x58388f(0x30e)]();_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37]&&(_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37][_0x58388f(0xa86)]=undefined,_0xab9a9c['pcs'][_0x2b7d37][_0x58388f(0x475)]=_0x343a23);try{_0xab9a9c[_0x58388f(0x1eb)]({'ping':_0x343a23},_0x2b7d37);}catch(_0x3ec783){warnlog(_0x3ec783);}setTimeout(function(_0x42aade,_0x581d28){var _0x4b278a=_0x58388f;try{if(!(_0x42aade in _0xab9a9c[_0x4b278a(0x84f)]))return;if(_0xab9a9c[_0x4b278a(0x84f)][_0x42aade][_0x4b278a(0xa83)]!=='disconnected')return;if(_0xab9a9c[_0x4b278a(0x84f)][_0x42aade][_0x4b278a(0xa86)]!==_0x581d28){try{_0xab9a9c[_0x4b278a(0xa3d)]&&_0xab9a9c['rotateIceServersSimple'](_0xab9a9c[_0x4b278a(0x84f)][_0x42aade]);}catch(_0x2615ba){warnlog(_0x2615ba);}if(_0xab9a9c[_0x4b278a(0x84f)][_0x42aade][_0x4b278a(0xc25)])try{_0xab9a9c[_0x4b278a(0x84f)][_0x42aade]['restartIce']();}catch(_0x4ae60a){warnlog(_0x4ae60a);}try{_0xab9a9c[_0x4b278a(0x729)](_0x42aade,!![]);}catch(_0x1df789){warnlog(_0x1df789);}}}catch(_0x3285ab){errorlog(_0x3285ab);}},0xbb8,_0x2b7d37,_0x343a23);}catch(_0x361d56){errorlog(_0x361d56);}_0xab9a9c['pcs'][_0x2b7d37][_0x58388f(0x808)]=setTimeout(function(_0x3ed334){var _0x4dbb06=_0x58388f;_0x3ed334 in _0xab9a9c['pcs']?(warnlog(_0x4dbb06(0x46f)),_0xab9a9c[_0x4dbb06(0x219)](_0x3ed334)):errorlog(_0x4dbb06(0x49a));},navigator[_0x58388f(0xb85)]&&navigator['platform'][_0x58388f(0x85b)]()['includes'](_0x58388f(0xb46))?0x2710:0x1388,_0x2b7d37);break;case _0x58388f(0xa40):warnlog(_0x58388f(0xa44));_0xab9a9c['qosEnabled']&&_0xab9a9c[_0x58388f(0xa06)]&&_0xab9a9c[_0x58388f(0xa06)]['connectionFailures']++;_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37]&&(_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37]['delayIceSend']=0x0,_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37]['closeTimeout']&&(log('Close\x20timeout\x20cancelled\x20-\x20ice\x20failed\x20instead'),clearTimeout(_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37][_0x58388f(0x808)])),_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37][_0x58388f(0xc25)]?(log(_0x58388f(0xcc1)),_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37][_0x58388f(0xc25)](),_0xab9a9c[_0x58388f(0x6b3)]&&_0xab9a9c['qosData']&&_0xab9a9c['qosData'][_0x58388f(0xb7c)]++):(log(_0x58388f(0x251)),_0xab9a9c['createOffer'](_0x2b7d37,!![]),_0xab9a9c[_0x58388f(0x6b3)]&&_0xab9a9c[_0x58388f(0xa06)]&&_0xab9a9c[_0x58388f(0xa06)][_0x58388f(0xb7c)]++));break;case'closed':warnlog(_0x58388f(0x5b6)),log(_0x58388f(0x81e)),_0xab9a9c[_0x58388f(0x219)](_0x2b7d37);break;default:log(_0x58388f(0x676)+_0xab9a9c['pcs'][_0x2b7d37][_0x58388f(0xa83)]),clearTimeout(_0xab9a9c[_0x58388f(0x84f)][_0x2b7d37][_0x58388f(0x808)]);break;}},_0xab9a9c[_0x27dbee(0x84f)][_0x2b7d37][_0x27dbee(0x38c)]=function(_0x959e7){var _0x13a5f4=_0x27dbee;warnlog(_0x13a5f4(0xa7d)),log(_0x13a5f4(0x9fb)),_0xab9a9c[_0x13a5f4(0x219)](_0x2b7d37);},_0xab9a9c['pcs'][_0x2b7d37][_0x27dbee(0x97d)]=function _0xbe674(){var _0x3077f8=_0x27dbee;log(_0x3077f8(0xa38));};},_0xab9a9c[_0xf92ab0(0x36e)]=function(_0x1b7cad){var _0x95573f=_0xf92ab0,_0x3fbbf8=_0x1b7cad[_0x95573f(0xab9)];if(_0x1b7cad[_0x95573f(0x5dd)]['type']==_0x95573f(0x6c0))_0xab9a9c[_0x95573f(0x72d)](_0x1b7cad),_0xab9a9c[_0x95573f(0x977)](_0x1b7cad);else try{if(!(_0x1b7cad['UUID']in _0xab9a9c['pcs']))return;var _0xfd402e=_0xab9a9c[_0x95573f(0x504)];if(_0xab9a9c[_0x95573f(0x9ea)]&&_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x1ab)]==!![]&&_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]]['forceios']==![]){if(_0xfd402e===![]||_0xfd402e>_0xab9a9c['maxMobileBitrate']){var _0x567563=Object['keys'](_0xab9a9c[_0x95573f(0x84f)])[_0x95573f(0xade)];if(_0xab9a9c['flagship'])_0xfd402e=_0xab9a9c[_0x95573f(0x512)];else{if(_0x567563>0x4)_0xfd402e=_0xab9a9c['lowMobileBitrate'];else(iOS||iPad)&&SafariVersion&&SafariVersion<=0xd?_0xfd402e=_0xab9a9c[_0x95573f(0x8b3)]:_0xfd402e=_0xab9a9c['maxMobileBitrate'];}}if(iOS||iPad){if(_0xfd402e!==![]){if(_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad['UUID']][_0x95573f(0x402)]===![])_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]=_0xfd402e,_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0xafc)](_0x1b7cad['description'][_0x95573f(0x728)],'vp8',_0xab9a9c[_0x95573f(0xa1a)]),_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0x27c)](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)],{'min':parseInt(_0xfd402e/0xa)||0x1,'max':_0xfd402e});else _0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x402)]>_0xfd402e&&(_0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]=_0xfd402e,_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0xafc)](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)],_0x95573f(0x8ce),_0xab9a9c[_0x95573f(0xa1a)]),_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0x27c)](_0x1b7cad['description'][_0x95573f(0x728)],{'min':parseInt(_0xfd402e/0xa)||0x1,'max':_0xfd402e}));_0xfd402e=![];}}}else{if(_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x1ab)]==!![])_0xfd402e!==![]?_0xab9a9c[_0x95573f(0x9c3)]!==![]&&(_0xab9a9c[_0x95573f(0x9c3)]<_0xfd402e&&(_0xfd402e=_0xab9a9c[_0x95573f(0x9c3)])):_0xfd402e=_0xab9a9c['roombitrate'],(iOS||iPad)&&_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xcb7)]&&(_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x948)]=!![]);else{if(iOS||iPad){var _0x45d6ef=0x0;for(var _0x3b2762 in _0xab9a9c[_0x95573f(0x84f)]){_0x1b7cad['UUID']!==_0x3b2762&&(_0xab9a9c[_0x95573f(0x84f)][_0x3b2762]['encoder']===!![]&&(_0x45d6ef+=0x1));}if(_0x45d6ef>=0x3){if(_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad['UUID']][_0x95573f(0xcb7)])_0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x948)]=!![],_0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]&&_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]===_0x95573f(0xaa9)&&(_0x1b7cad[_0x95573f(0x5dd)]['sdp']=CodecsHandler[_0x95573f(0xafc)](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)],_0x95573f(0xaa9),_0xab9a9c['preferredVideoErrorCorrection']),log(_0x95573f(0x1d0)+_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]]['preferVideoCodec']+_0x95573f(0x68c)));else _0xab9a9c['pcs'][_0x1b7cad['UUID']][_0x95573f(0xa42)]&&_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]===_0x95573f(0x906)?(_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0xafc)](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)],'vp9',_0xab9a9c[_0x95573f(0xa1a)]),log('Trying\x20to\x20set\x20'+_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]+_0x95573f(0x68c)),_0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]]['encoder']=![]):(_0x1b7cad['description'][_0x95573f(0x728)]=CodecsHandler['preferCodec'](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)],_0x95573f(0x8ce),_0xab9a9c['preferredVideoErrorCorrection']),log(_0x95573f(0xa57)),_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x948)]=![]);}else _0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]&&_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]]['preferVideoCodec']!==_0x95573f(0xaa9)?_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]]['preferVideoCodec']===_0x95573f(0x906)||_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]]['preferVideoCodec']===_0x95573f(0x8ce)?(_0x1b7cad['description'][_0x95573f(0x728)]=CodecsHandler['preferCodec'](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)],_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)],_0xab9a9c[_0x95573f(0xa1a)]),log(_0x95573f(0x1d0)+_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]+_0x95573f(0x68c)),_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad['UUID']][_0x95573f(0x948)]=![]):_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad['UUID']]['encoder']=!![]:(_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad['UUID']][_0x95573f(0x948)]=!![],_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]&&_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad['UUID']][_0x95573f(0xa42)]===_0x95573f(0xaa9)&&(_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0xafc)](_0x1b7cad['description'][_0x95573f(0x728)],'h264',_0xab9a9c[_0x95573f(0xa1a)]),log(_0x95573f(0x1d0)+_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]+_0x95573f(0x68c))));}else _0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]&&(_0x1b7cad['description'][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0xafc)](_0x1b7cad[_0x95573f(0x5dd)]['sdp'],_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]]['preferVideoCodec'],_0xab9a9c[_0x95573f(0xa1a)]),log('Trying\x20to\x20set\x20'+_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa42)]+_0x95573f(0x68c)));}}try{if(_0xfd402e){var _0x22fe57=CodecsHandler['getVideoBitrates'](_0x1b7cad[_0x95573f(0x5dd)]['sdp']);log(_0x95573f(0x91f)+_0x22fe57);_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x402)]!==![]&&(_0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x402)]<_0xfd402e&&(_0xfd402e=![]));if(_0xfd402e===![])_0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]=_0x22fe57;else{if(_0x22fe57!==![]&&_0x22fe57>_0xfd402e){var _0x326fb2=CodecsHandler[_0x95573f(0x27f)](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)])||0x0;_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0x27c)](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)],{'min':parseInt(_0xfd402e/0xa)||0x1,'max':parseInt(_0xfd402e+_0x326fb2/0x400)}),_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]=_0xfd402e;}else{if(_0x22fe57===![]){var _0x326fb2=CodecsHandler['getOpusBitrate'](_0x1b7cad['description'][_0x95573f(0x728)])||0x0;_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler['setVideoBitrates'](_0x1b7cad[_0x95573f(0x5dd)]['sdp'],{'min':parseInt(_0xfd402e/0xa)||0x1,'max':parseInt(_0xfd402e+_0x326fb2/0x400)});if(_0xab9a9c[_0x95573f(0x8a7)]&&_0xab9a9c[_0x95573f(0x8a7)]>_0xfd402e)_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]=_0xfd402e;else _0xab9a9c[_0x95573f(0x8a7)]?_0xab9a9c['pcs'][_0x1b7cad['UUID']]['setBitrate']=_0xab9a9c[_0x95573f(0x8a7)]:_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x402)]=0x9c4;}else _0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]=_0x22fe57;}}}else{if(_0xab9a9c[_0x95573f(0x8a7)]!==![]){var _0x22fe57=CodecsHandler[_0x95573f(0x5f5)](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]);log('BITRATE\x202:\x20'+_0x22fe57);if(_0x22fe57===![]){var _0x326fb2=CodecsHandler[_0x95573f(0x27f)](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)])||0x0;_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler['setVideoBitrates'](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)],{'min':parseInt(_0xab9a9c[_0x95573f(0x8a7)]/0xa)||0x1,'max':parseInt(_0xab9a9c[_0x95573f(0x8a7)]+_0x326fb2/0x400)});}else _0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]===![]&&(_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]=_0x22fe57);}else _0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x29e)]===![]&&(_0xab9a9c['pcs'][_0x1b7cad['UUID']][_0x95573f(0x29e)]=CodecsHandler['getVideoBitrates'](_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]),log('BITRATE\x203:\x20'+_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad['UUID']][_0x95573f(0x29e)]));}}catch(_0x549b3e){warnlog(_0x95573f(0x4bb));}_0xab9a9c['outboundAudioBitrate']&&(_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=CodecsHandler[_0x95573f(0x276)](_0x1b7cad['description']['sdp'],{'maxaveragebitrate':_0xab9a9c['outboundAudioBitrate']*0x400,'cbr':_0xab9a9c['cbr']}));if('session'in _0x1b7cad&&_0x1b7cad[_0x95573f(0x94a)]!=_0xab9a9c[_0x95573f(0x84f)][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0x94a)]){errorlog('Answer\x20SDP\x20does\x20not\x20have\x20a\x20matching\x20session\x20ID');return;}_0xab9a9c[_0x95573f(0x38e)]&&(_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)]=filterSDPLAN(_0x1b7cad[_0x95573f(0x5dd)]['sdp'])),_0xab9a9c['stunOnly']&&(_0x1b7cad['description'][_0x95573f(0x728)]=filterStunOnly(_0x1b7cad[_0x95573f(0x5dd)][_0x95573f(0x728)])),_0xab9a9c['pcs'][_0x1b7cad[_0x95573f(0xab9)]][_0x95573f(0xa67)](_0x1b7cad[_0x95573f(0x5dd)])['then']()[_0x95573f(0xacc)](errorlog);}catch(_0x9ff700){errorlog(_0x9ff700);}},_0xab9a9c[_0xf92ab0(0x7b1)]=function(_0x2fb46b){var _0x3ff92c=_0xf92ab0;_0xab9a9c[_0x3ff92c(0x54c)]&&_0x2fb46b[_0x3ff92c(0xa85)]?_0xab9a9c['decryptMessage'](_0x2fb46b[_0x3ff92c(0x5dd)],_0x2fb46b[_0x3ff92c(0xa85)])['then'](function(_0x22a641){var _0xf49f80=_0x3ff92c;try{_0x2fb46b['description']=JSON['parse'](_0x22a641),_0xab9a9c[_0xf49f80(0x36e)](_0x2fb46b);}catch(_0x328562){errorlog(_0x328562);}})[_0x3ff92c(0xacc)](function(_0x4eff29){errorlog('Decryption\x20error:',_0x4eff29);}):_0xab9a9c[_0x3ff92c(0x36e)](_0x2fb46b);},_0xab9a9c[_0xf92ab0(0x842)]=function(_0x2c9ea1){var _0xa7eb52=_0xf92ab0;_0xab9a9c[_0xa7eb52(0x54c)]&&_0x2c9ea1[_0xa7eb52(0xa85)]?_0xab9a9c[_0xa7eb52(0x347)](_0x2c9ea1[_0xa7eb52(0x4f8)],_0x2c9ea1['vector'])['then'](function(_0xde8bff){var _0x5dcebb=_0xa7eb52;try{_0x2c9ea1['candidate']=JSON[_0x5dcebb(0xa4b)](_0xde8bff),_0xab9a9c[_0x5dcebb(0x6da)](_0x2c9ea1);}catch(_0x5f34dd){errorlog(_0x5f34dd);}})[_0xa7eb52(0xacc)](function(_0x5bb401){var _0x271199=_0xa7eb52;errorlog(_0x271199(0x5b9),_0x5bb401);}):_0xab9a9c[_0xa7eb52(0x6da)](_0x2c9ea1);},_0xab9a9c[_0xf92ab0(0x6da)]=function(_0x951b7f){var _0xbc743a=_0xf92ab0;try{if(_0xab9a9c['icefilter']){if(_0x951b7f[_0xbc743a(0x4f8)][_0xbc743a(0x4f8)][_0xbc743a(0x565)](_0xab9a9c['icefilter'])===-0x1){log('dropped\x20candidate\x20due\x20to\x20filter'),log(_0x951b7f[_0xbc743a(0x4f8)]);return;}else log(_0xbc743a(0x274)),log(_0x951b7f[_0xbc743a(0x4f8)]);}}catch(_0x5d3347){errorlog(_0x5d3347);}if(_0x951b7f[_0xbc743a(0x4f8)]&&_0xbc743a(0x4f8)in _0x951b7f[_0xbc743a(0x4f8)]&&_0x951b7f[_0xbc743a(0x4f8)][_0xbc743a(0x4f8)]=='')return;try{if(_0xab9a9c[_0xbc743a(0x38e)]){if(!filterIceLAN(_0x951b7f[_0xbc743a(0x4f8)]))return;}if(_0xab9a9c[_0xbc743a(0x3d4)]){if(!filterStunOnly(event['candidate']))return;}}catch(_0x5a8d3b){errorlog(_0x5a8d3b);}if(_0x951b7f[_0xbc743a(0xab9)]in _0xab9a9c[_0xbc743a(0x84f)]&&_0x951b7f[_0xbc743a(0x3c5)]==_0xbc743a(0x1d9)){log(_0xbc743a(0x4ee));if(_0xbc743a(0x94a)in _0x951b7f&&_0xab9a9c['pcs'][_0x951b7f[_0xbc743a(0xab9)]][_0xbc743a(0x94a)]!=_0x951b7f['session']){errorlog(_0xbc743a(0x7ac));return;}_0xab9a9c[_0xbc743a(0x84f)][_0x951b7f['UUID']][_0xbc743a(0x52c)](_0x951b7f[_0xbc743a(0x4f8)])[_0xbc743a(0x998)]()[_0xbc743a(0xacc)](function(_0x179086){var _0x11f72f=_0xbc743a;warnlog(_0x11f72f(0x717)+_0x179086[_0x11f72f(0x3a3)]);});}else{if(_0x951b7f[_0xbc743a(0xab9)]in _0xab9a9c[_0xbc743a(0x404)]&&_0x951b7f[_0xbc743a(0x3c5)]==_0xbc743a(0x381)){log(_0xbc743a(0xb4e));if('session'in _0x951b7f&&_0xab9a9c[_0xbc743a(0x404)][_0x951b7f[_0xbc743a(0xab9)]]['session']!=_0x951b7f[_0xbc743a(0x94a)]){errorlog('Incoming\x20Ice\x20Offer\x20does\x20not\x20match\x20Session');return;}if(_0xab9a9c[_0xbc743a(0x404)][_0x951b7f['UUID']]===null)return;_0xab9a9c[_0xbc743a(0x404)][_0x951b7f[_0xbc743a(0xab9)]]['addIceCandidate'](_0x951b7f['candidate'])['then']()[_0xbc743a(0xacc)](function(_0x57ab88){var _0x2ff816=_0xbc743a;warnlog(_0x2ff816(0x717)+_0x57ab88[_0x2ff816(0x3a3)]);});}else warnlog(_0x951b7f),errorlog('ICE\x20DID\x20NOT\x20FIND\x20A\x20PC\x20OPTION?\x20peer\x20might\x20have\x20left\x20before\x20ICE\x20complete?');}},_0xab9a9c['processIceBundle']=function(_0x13885f){var _0x10524b=_0xf92ab0;if(_0xab9a9c[_0x10524b(0x54c)]&&_0x13885f['vector'])_0xab9a9c[_0x10524b(0x347)](_0x13885f[_0x10524b(0x460)],_0x13885f[_0x10524b(0xa85)])['then'](function(_0x14a502){var _0x1f7af0=_0x10524b;_0x13885f[_0x1f7af0(0x460)]=JSON[_0x1f7af0(0xa4b)](_0x14a502);var _0x3c0028={};_0x3c0028[_0x1f7af0(0xab9)]=_0x13885f['UUID'],_0x3c0028[_0x1f7af0(0x3c5)]=_0x13885f[_0x1f7af0(0x3c5)];for(var _0x2e5ad0=0x0;_0x2e5ad0<_0x13885f[_0x1f7af0(0x460)][_0x1f7af0(0xade)];_0x2e5ad0++){_0x3c0028[_0x1f7af0(0x4f8)]=_0x13885f['candidates'][_0x2e5ad0],_0xab9a9c[_0x1f7af0(0x6da)](_0x3c0028);}});else{var _0x2f7bc1={};_0x2f7bc1[_0x10524b(0xab9)]=_0x13885f[_0x10524b(0xab9)],_0x2f7bc1[_0x10524b(0x3c5)]=_0x13885f[_0x10524b(0x3c5)];for(var _0xa32530=0x0;_0xa32530<_0x13885f[_0x10524b(0x460)][_0x10524b(0xade)];_0xa32530++){_0x2f7bc1[_0x10524b(0x4f8)]=_0x13885f[_0x10524b(0x460)][_0xa32530],_0xab9a9c[_0x10524b(0x6da)](_0x2f7bc1);}}},_0xab9a9c[_0xf92ab0(0x977)]=async function(_0x3e1ba5){var _0x37ac7a=_0xf92ab0;_0x37ac7a(0x30b)in _0x3e1ba5&&(_0xab9a9c[_0x37ac7a(0x404)][_0x3e1ba5['UUID']][_0x37ac7a(0xa65)]=_0x3e1ba5['screen'],log(_0x37ac7a(0x97a)),log(_0x3e1ba5[_0x37ac7a(0x30b)]));log(_0x3e1ba5);_0xab9a9c['removeOrientationFlag']&&_0x3e1ba5[_0x37ac7a(0x5dd)]&&_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]&&_0x3e1ba5['description']['sdp'][_0x37ac7a(0xa4f)](_0x37ac7a(0x86a))&&(_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]=_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)][_0x37ac7a(0x35b)]('a=extmap:3\x20urn:3gpp:video-orientation\x0d\x0a',''),warnlog('removed\x20from\x20SDP:\x20\x27a=extmap:3\x20urn:3gpp:video-orientation\x0d\x0a\x27'));_0xab9a9c['noPLIs']&&(_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]=CodecsHandler[_0x37ac7a(0xaac)](_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]));_0xab9a9c['noREMB']&&(_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]=CodecsHandler[_0x37ac7a(0x37f)](_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]));_0xab9a9c[_0x37ac7a(0x886)]&&(log(_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]),_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]=CodecsHandler[_0x37ac7a(0xce6)](_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]));_0xab9a9c[_0x37ac7a(0x38e)]&&(_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]=filterSDPLAN(_0x3e1ba5[_0x37ac7a(0x5dd)]['sdp']));_0xab9a9c[_0x37ac7a(0x3d4)]&&(_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x728)]=filterStunOnly(_0x3e1ba5['description']['sdp']));try{if(_0x3e1ba5[_0x37ac7a(0x5dd)]&&_0x3e1ba5[_0x37ac7a(0x5dd)][_0x37ac7a(0x3c5)]===_0x37ac7a(0x6c0)){const _0x49b111=_0xab9a9c[_0x37ac7a(0x404)][_0x3e1ba5[_0x37ac7a(0xab9)]];if(_0x49b111&&_0x49b111[_0x37ac7a(0x6d9)]&&_0x49b111['signalingState']!==_0x37ac7a(0x5c0))try{_0x49b111[_0x37ac7a(0x93c)]({'type':_0x37ac7a(0x22f)})[_0x37ac7a(0xacc)](warnlog);}catch(_0x539f22){warnlog(_0x539f22);}}}catch(_0x2911e6){warnlog(_0x2911e6);}_0xab9a9c[_0x37ac7a(0x404)][_0x3e1ba5[_0x37ac7a(0xab9)]][_0x37ac7a(0xa67)](_0x3e1ba5[_0x37ac7a(0x5dd)])[_0x37ac7a(0x998)](async function(){var _0x2fd8c0=_0x37ac7a;if(_0xab9a9c[_0x2fd8c0(0x404)][_0x3e1ba5[_0x2fd8c0(0xab9)]][_0x2fd8c0(0x48e)]['type']===_0x2fd8c0(0x6c0)){if(_0xab9a9c[_0x2fd8c0(0x404)][_0x3e1ba5['UUID']][_0x2fd8c0(0x6d9)]!==_0x2fd8c0(0xad7))return;_0xab9a9c[_0x2fd8c0(0x404)][_0x3e1ba5[_0x2fd8c0(0xab9)]][_0x2fd8c0(0x2cc)]()[_0x2fd8c0(0x998)](function(_0x1cb24d){var _0x20754f=_0x2fd8c0;log(_0x20754f(0x63c));if(_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5[_0x20754f(0xab9)]]['whip']){if(_0xab9a9c[_0x20754f(0x839)]&&_0xab9a9c[_0x20754f(0x839)]==0x4)_0x1cb24d['sdp']=CodecsHandler['setOpusAttributes'](_0x1cb24d['sdp'],{'stereo':0x2},!![]);else _0xab9a9c[_0x20754f(0x839)]&&!_0xab9a9c['mono']&&_0xab9a9c[_0x20754f(0x839)]!=0x3&&(_0x1cb24d['sdp']=CodecsHandler[_0x20754f(0x276)](_0x1cb24d[_0x20754f(0x728)],{'stereo':0x1},!![]));return _0xab9a9c[_0x20754f(0x404)][_0x3e1ba5[_0x20754f(0xab9)]]['setLocalDescription'](_0x1cb24d);}var _0x582b52=![];if(!_0xab9a9c[_0x20754f(0x827)]&&_0xab9a9c[_0x20754f(0x839)]==0x5)_0x582b52={'stereo':0x1,'maxaveragebitrate':(_0xab9a9c[_0x20754f(0xbee)]||_0xab9a9c['audiobitratePRO'])*0x400,'cbr':_0xab9a9c['cbr'],'useinbandfec':_0xab9a9c['noFEC']?0x0:0x1,'maxptime':_0xab9a9c[_0x20754f(0x2fa)],'minptime':_0xab9a9c[_0x20754f(0x36d)],'ptime':_0xab9a9c['ptime'],'dtx':_0xab9a9c[_0x20754f(0x95b)]},log(_0x20754f(0xb9c));else{if(_0xab9a9c[_0x20754f(0xafa)]&&Firefox)_0xab9a9c[_0x20754f(0xbee)]?_0x582b52={'stereo':0x0,'maxaveragebitrate':_0xab9a9c[_0x20754f(0xbee)]*0x400,'cbr':_0xab9a9c['cbr'],'useinbandfec':_0xab9a9c[_0x20754f(0xaf8)]?0x0:0x1,'maxptime':_0xab9a9c['maxptime'],'minptime':_0xab9a9c[_0x20754f(0x36d)],'ptime':_0xab9a9c[_0x20754f(0x336)],'dtx':_0xab9a9c['dtx']}:_0x582b52={'stereo':0x0,'useinbandfec':_0xab9a9c[_0x20754f(0xaf8)]?0x0:0x1,'maxptime':_0xab9a9c[_0x20754f(0x2fa)],'minptime':_0xab9a9c['minptime'],'ptime':_0xab9a9c[_0x20754f(0x336)],'dtx':_0xab9a9c[_0x20754f(0x95b)]};else{if(_0xab9a9c['stereo']==0x1||_0xab9a9c[_0x20754f(0x839)]==0x2||_0xab9a9c[_0x20754f(0x839)]==0x5)_0x582b52={'stereo':0x1,'maxaveragebitrate':(_0xab9a9c[_0x20754f(0xbee)]||_0xab9a9c[_0x20754f(0x4f6)])*0x400,'cbr':_0xab9a9c[_0x20754f(0x9b2)],'useinbandfec':_0xab9a9c[_0x20754f(0xaf8)]?0x0:0x1,'maxptime':_0xab9a9c[_0x20754f(0x2fa)],'minptime':_0xab9a9c['minptime'],'ptime':_0xab9a9c[_0x20754f(0x336)],'dtx':_0xab9a9c['dtx']},log(_0x20754f(0xb9c));else{if(_0xab9a9c[_0x20754f(0x839)]==0x4)_0x582b52={'stereo':0x2,'maxaveragebitrate':(_0xab9a9c[_0x20754f(0xbee)]||_0xab9a9c['audiobitratePRO'])*0x400,'cbr':_0xab9a9c['cbr'],'useinbandfec':_0xab9a9c[_0x20754f(0xaf8)]?0x0:0x1,'maxptime':_0xab9a9c['maxptime'],'minptime':_0xab9a9c[_0x20754f(0x36d)],'ptime':_0xab9a9c[_0x20754f(0x336)],'dtx':_0xab9a9c['dtx']};else{if(_0xab9a9c[_0x20754f(0xbee)])_0x582b52={'maxaveragebitrate':_0xab9a9c[_0x20754f(0xbee)]*0x400,'cbr':_0xab9a9c[_0x20754f(0x9b2)],'useinbandfec':_0xab9a9c[_0x20754f(0xaf8)]?0x0:0x1,'maxptime':_0xab9a9c[_0x20754f(0x2fa)],'minptime':_0xab9a9c['minptime'],'ptime':_0xab9a9c['ptime'],'dtx':_0xab9a9c[_0x20754f(0x95b)]};else{if(_0xab9a9c['noFEC'])_0x582b52={'useinbandfec':0x0,'maxptime':_0xab9a9c[_0x20754f(0x2fa)],'minptime':_0xab9a9c[_0x20754f(0x36d)],'ptime':_0xab9a9c[_0x20754f(0x336)],'dtx':_0xab9a9c['dtx']};else _0xab9a9c[_0x20754f(0x95b)]&&(_0x582b52={'maxptime':_0xab9a9c[_0x20754f(0x2fa)],'minptime':_0xab9a9c[_0x20754f(0x36d)],'ptime':_0xab9a9c[_0x20754f(0x336)],'dtx':_0xab9a9c[_0x20754f(0x95b)]});}}}}}_0xab9a9c[_0x20754f(0x839)]===0x6&&(!_0x582b52?_0x582b52={'stereo':0x1}:_0x582b52['stereo']=0x1);_0x582b52&&(_0x1cb24d['sdp']=CodecsHandler[_0x20754f(0x276)](_0x1cb24d['sdp'],_0x582b52));if(_0xab9a9c[_0x20754f(0xa25)])try{if(_0xab9a9c[_0x20754f(0xa25)]===_0x20754f(0x46e))_0x1cb24d['sdp']=CodecsHandler[_0x20754f(0x5b7)](_0x1cb24d['sdp']);else{if(_0xab9a9c['audioCodec']==='pcm'){if(_0xab9a9c[_0x20754f(0xafa)])_0x1cb24d['sdp']=CodecsHandler['modifyDescPCM'](_0x1cb24d[_0x20754f(0x728)],_0xab9a9c[_0x20754f(0xa5c)]||0xbb80,![],_0xab9a9c[_0x20754f(0x336)]);else _0xab9a9c[_0x20754f(0x839)]?_0x1cb24d['sdp']=CodecsHandler[_0x20754f(0x6e3)](_0x1cb24d['sdp'],_0xab9a9c['sampleRate']||0x7d00,!![],_0xab9a9c[_0x20754f(0x336)]):_0x1cb24d[_0x20754f(0x728)]=CodecsHandler['modifyDescPCM'](_0x1cb24d[_0x20754f(0x728)],_0xab9a9c[_0x20754f(0xa5c)]||0xbb80,![],_0xab9a9c['ptime']);}else _0x1cb24d['sdp']=CodecsHandler[_0x20754f(0x3c8)](_0x1cb24d[_0x20754f(0x728)],_0xab9a9c[_0x20754f(0xa25)],_0xab9a9c[_0x20754f(0x5d4)],_0xab9a9c[_0x20754f(0xb88)]);}}catch(_0x2fa435){errorlog(_0x2fa435),warnlog(_0x20754f(0x980));}if(_0xab9a9c[_0x20754f(0xb41)]&&_0xab9a9c[_0x20754f(0xb41)][_0x20754f(0xade)])for(var _0x2b1e07=_0xab9a9c[_0x20754f(0xb41)][_0x20754f(0xade)]-0x1;_0x2b1e07>=0x0;_0x2b1e07--){try{_0x1cb24d[_0x20754f(0x728)]=CodecsHandler[_0x20754f(0xafc)](_0x1cb24d[_0x20754f(0x728)],_0xab9a9c[_0x20754f(0xb41)][_0x2b1e07],_0xab9a9c['videoErrorCorrection']);}catch(_0x3ae778){errorlog(_0x3ae778);break;}}_0xab9a9c[_0x20754f(0x44b)]&&(_0x1cb24d[_0x20754f(0x728)]=CodecsHandler[_0x20754f(0xafc)](_0x1cb24d[_0x20754f(0x728)],_0xab9a9c[_0x20754f(0x44b)],_0xab9a9c[_0x20754f(0x769)]));try{var _0xf9bb22=![];try{_0xf9bb22=_0x1cb24d&&_0x1cb24d[_0x20754f(0x728)]&&(_0x1cb24d[_0x20754f(0x728)][_0x20754f(0x629)](/^m=video /mg)||[])[_0x20754f(0xade)]>0x1||![];}catch(_0x4c5156){}var _0xb6fe94=_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5['UUID']]&&_0xab9a9c['rpcs'][_0x3e1ba5['UUID']]['screenIndexes']&&_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5[_0x20754f(0xab9)]]['screenIndexes']['length']>0x0||![],_0x1b5f7e=_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5[_0x20754f(0xab9)]]&&_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5[_0x20754f(0xab9)]][_0x20754f(0x82f)]||![];_0xab9a9c[_0x20754f(0xaf7)]&&!(_0xf9bb22||_0xb6fe94||_0x1b5f7e)&&(log(_0x20754f(0x57f)),_0x1cb24d[_0x20754f(0x728)]=_0x1cb24d[_0x20754f(0x728)][_0x20754f(0x35b)](/42e01f/gi,_0xab9a9c[_0x20754f(0xaf7)]),_0x1cb24d[_0x20754f(0x728)]=_0x1cb24d[_0x20754f(0x728)][_0x20754f(0x35b)](/42001f/gi,_0xab9a9c['h264profile']),_0x1cb24d['sdp']=_0x1cb24d['sdp'][_0x20754f(0x35b)](/420029/gi,_0xab9a9c[_0x20754f(0xaf7)]),_0x1cb24d['sdp']=_0x1cb24d['sdp'][_0x20754f(0x35b)](/42a01e/gi,_0xab9a9c[_0x20754f(0xaf7)]),_0x1cb24d['sdp']=_0x1cb24d[_0x20754f(0x728)][_0x20754f(0x35b)](/42a014/gi,_0xab9a9c['h264profile']),_0x1cb24d[_0x20754f(0x728)]=_0x1cb24d['sdp'][_0x20754f(0x35b)](/42a00b/gi,_0xab9a9c[_0x20754f(0xaf7)]),_0x1cb24d['sdp']=_0x1cb24d[_0x20754f(0x728)][_0x20754f(0x35b)](/640c1f/gi,_0xab9a9c[_0x20754f(0xaf7)]),_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5[_0x20754f(0xab9)]]&&(_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5[_0x20754f(0xab9)]][_0x20754f(0x82f)]=!![]));}catch(_0x2f059a){errorlog(_0x2f059a);}_0xab9a9c[_0x20754f(0x3b7)]&&(_0x1cb24d[_0x20754f(0x728)]=CodecsHandler[_0x20754f(0xaac)](_0x1cb24d[_0x20754f(0x728)]));_0xab9a9c[_0x20754f(0xbc7)]&&(_0x1cb24d[_0x20754f(0x728)]=CodecsHandler[_0x20754f(0x37f)](_0x1cb24d['sdp']));_0xab9a9c[_0x20754f(0x886)]&&(log(_0x1cb24d[_0x20754f(0x728)]),_0x1cb24d[_0x20754f(0x728)]=CodecsHandler['disableNACK'](_0x1cb24d[_0x20754f(0x728)]));if(_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5[_0x20754f(0xab9)]][_0x20754f(0x9b6)])log(_0x20754f(0xcc2)),_0x1cb24d[_0x20754f(0x728)]=_0x2fb2bd(_0x1cb24d[_0x20754f(0x728)],_0xab9a9c['rpcs'][_0x3e1ba5[_0x20754f(0xab9)]][_0x20754f(0x9b6)]);else _0xab9a9c[_0x20754f(0x42f)]&&(log(_0x20754f(0xcc2)),_0x1cb24d[_0x20754f(0x728)]=_0x2fb2bd(_0x1cb24d['sdp'],_0xab9a9c[_0x20754f(0x42f)]));return _0xab9a9c[_0x20754f(0x38e)]&&(_0x1cb24d[_0x20754f(0x728)]=filterSDPLAN(_0x1cb24d[_0x20754f(0x728)])),_0xab9a9c[_0x20754f(0x3d4)]&&(_0x1cb24d[_0x20754f(0x728)]=filterStunOnly(_0x1cb24d[_0x20754f(0x728)])),log(_0x1cb24d),_0xab9a9c[_0x20754f(0x404)][_0x3e1ba5['UUID']][_0x20754f(0x93c)](_0x1cb24d);})['then'](function _0x3e2653(){var _0x13da09=_0x2fd8c0;log(_0x13da09(0xbb0));if(_0xab9a9c[_0x13da09(0x404)][_0x3e1ba5['UUID']][_0x13da09(0x625)]){_0xab9a9c[_0x13da09(0x404)][_0x3e1ba5[_0x13da09(0xab9)]][_0x13da09(0xcb0)]&&_0xab9a9c[_0x13da09(0x404)][_0x3e1ba5[_0x13da09(0xab9)]][_0x13da09(0xcb0)]();return;}var _0x826f12={};_0x826f12[_0x13da09(0xab9)]=_0x3e1ba5['UUID'],_0x826f12[_0x13da09(0x5dd)]=filterDescriptionIpv6(_0xab9a9c[_0x13da09(0x404)][_0x3e1ba5[_0x13da09(0xab9)]][_0x13da09(0xb04)]),_0x826f12[_0x13da09(0x94a)]=_0xab9a9c['rpcs'][_0x3e1ba5[_0x13da09(0xab9)]][_0x13da09(0x94a)],_0xab9a9c[_0x13da09(0x54c)]&&_0xab9a9c[_0x13da09(0x404)][_0x3e1ba5[_0x13da09(0xab9)]][_0x13da09(0xa85)]?_0xab9a9c[_0x13da09(0xab5)](JSON[_0x13da09(0x6d1)](_0x826f12['description']))[_0x13da09(0x998)](function(_0x289c7e){var _0x4f3b7d=_0x13da09;_0x826f12[_0x4f3b7d(0x5dd)]=_0x289c7e[0x0],_0x826f12[_0x4f3b7d(0xa85)]=_0x289c7e[0x1],_0xab9a9c['anyrequest'](_0x826f12);})[_0x13da09(0xacc)](errorlog):_0xab9a9c[_0x13da09(0x3fb)](_0x826f12);})[_0x2fd8c0(0xacc)](errorlog);}else _0xab9a9c[_0x2fd8c0(0x404)][_0x3e1ba5[_0x2fd8c0(0xab9)]]['remoteDescription'][_0x2fd8c0(0x3c5)]===_0x2fd8c0(0x3b6)&&errorlog(_0x2fd8c0(0x8ca));})[_0x37ac7a(0xacc)](function(_0x5b0e2b){var _0x16dc32=_0x37ac7a;errorlog(_0x5b0e2b),_0x3e1ba5[_0x16dc32(0x5dd)]&&errorlog(_0x3e1ba5['description'][_0x16dc32(0x728)]);});},_0xab9a9c[_0xf92ab0(0x1cc)]=function(_0xfb8eeb,_0x6d2feb={}){var _0x3c587c=_0xf92ab0;const _0x2469b6=_0xab9a9c['rpcs'][_0xfb8eeb],_0x138502=typeof _0x6d2feb[_0x3c587c(0x3ff)]===_0x3c587c(0xab6)?_0x6d2feb[_0x3c587c(0x3ff)]:0xc8;if(!_0x2469b6||!_0x2469b6[_0x3c587c(0x287)])return _0x138502;if(!(_0x138502>0x0))return 0x0;const _0x36d45d=_0x6d2feb[_0x3c587c(0x60b)]==='audio'?_0x3c587c(0x544):_0x3c587c(0xa55),_0x208064=_0x6d2feb['clamp']||{},_0x43832a=typeof _0x208064['min']===_0x3c587c(0xab6)?_0x208064['min']:0x0,_0x170a62=_0x36d45d===_0x3c587c(0x544)?0x1388:0x7530,_0x1e454f=typeof _0x208064[_0x3c587c(0x642)]===_0x3c587c(0xab6)?_0x208064['max']:_0x170a62;let _0x5413fc=_0x138502,_0x10c774=_0x138502,_0x51836b=_0x138502,_0x5e2ab3=_0x138502,_0x554bc9=![],_0x23fd93=Infinity;const _0x5cfd28=_0x36d45d==='audio'?_0x2469b6[_0x3c587c(0x287)][_0x3c587c(0x340)]:_0x2469b6[_0x3c587c(0x287)]['chunked_mode_video'];if(_0x5cfd28){typeof _0x5cfd28[_0x3c587c(0x54e)]===_0x3c587c(0xab6)&&Number[_0x3c587c(0x9e8)](_0x5cfd28[_0x3c587c(0x54e)])&&(_0x10c774=_0x5cfd28['buffer_buffer']);typeof _0x5cfd28['buffer_delta']===_0x3c587c(0xab6)&&Number[_0x3c587c(0x9e8)](_0x5cfd28[_0x3c587c(0x62f)])&&(_0x51836b=_0x5cfd28[_0x3c587c(0x62f)]);if(typeof _0x5cfd28['buffer_level']===_0x3c587c(0xab6)&&Number['isFinite'](_0x5cfd28[_0x3c587c(0x5b0)]))_0x5e2ab3=_0x5cfd28[_0x3c587c(0x5b0)];else{const _0x1a8109=typeof _0x5cfd28[_0x3c587c(0x54e)]===_0x3c587c(0xab6)?_0x5cfd28[_0x3c587c(0x54e)]:_0x138502;_0x5e2ab3=Math[_0x3c587c(0x642)](0x0,_0x1a8109-_0x51836b);}_0x554bc9=!!_0x5cfd28[_0x3c587c(0xb49)],_0x5cfd28[_0x3c587c(0x2c3)]&&Number[_0x3c587c(0x9e8)](_0x5cfd28[_0x3c587c(0x2c3)])&&(_0x23fd93=Date[_0x3c587c(0x30e)]()-_0x5cfd28[_0x3c587c(0x2c3)]);}_0x554bc9&&(_0x5413fc=Math[_0x3c587c(0x642)](_0x5413fc,_0x138502+Math[_0x3c587c(0x642)](0x50,_0x138502*0.25)));_0x23fd93<0x1388&&(_0x5413fc=Math[_0x3c587c(0x642)](_0x5413fc,_0x138502+Math[_0x3c587c(0x642)](0x78,_0x138502*0.4)));if(_0x51836b<=_0x138502*0.3){const _0x392468=_0x138502-_0x51836b;_0x392468>0x14&&(_0x5413fc=Math[_0x3c587c(0x642)](_0x5413fc,_0x138502+Math[_0x3c587c(0x642)](0x3c,_0x392468*0.6)));}const _0x5777b9=_0x138502*0.3;if(_0x5e2ab3<=_0x5777b9){const _0x28c629=_0x5777b9-_0x5e2ab3;_0x28c629>0x0&&(_0x5413fc=Math[_0x3c587c(0x642)](_0x5413fc,_0x138502+Math[_0x3c587c(0x642)](0x3c,_0x28c629*1.2)));}const _0x62186e=_0x2469b6[_0x3c587c(0x287)][_0x3c587c(0xc09)]||_0x2469b6['stats'][_0x3c587c(0x9c8)];if(_0xab9a9c[_0x3c587c(0xa13)]&&_0x62186e&&_0x62186e['Round_Trip_Time_ms']){const _0x4fa39a=parseFloat(_0x62186e[_0x3c587c(0x469)]);Number[_0x3c587c(0x9e8)](_0x4fa39a)&&_0x4fa39a>0x0&&(_0x5413fc=Math[_0x3c587c(0x642)](_0x5413fc,_0x138502+_0x4fa39a/0x2));}Number[_0x3c587c(0x9e8)](_0x2469b6[_0x3c587c(0x287)][_0x3c587c(0x619)])&&_0x2469b6[_0x3c587c(0x287)][_0x3c587c(0x619)]>0.03&&(_0x5413fc=Math[_0x3c587c(0x642)](_0x5413fc,_0x138502+_0x2469b6[_0x3c587c(0x287)][_0x3c587c(0x619)]*0x3e8));Number[_0x3c587c(0x9e8)](_0x2469b6['stats'][_0x3c587c(0xbac)])&&_0x2469b6[_0x3c587c(0x287)][_0x3c587c(0xbac)]>0.02&&(_0x5413fc=Math[_0x3c587c(0x642)](_0x5413fc,_0x138502+_0x2469b6['stats'][_0x3c587c(0xbac)]*0xfa0));if(!_0x554bc9&&_0x23fd93>0x1388&&_0x10c774>_0x138502){const _0xaf0657=Math['max'](0x14,_0x138502*0.1),_0x2dbabf=Math['max'](_0x138502,_0x10c774-_0xaf0657);_0x5413fc=Math['max'](_0x5413fc,_0x2dbabf);}return _0x5413fc=Math[_0x3c587c(0x642)](_0x43832a,Math[_0x3c587c(0xb03)](_0x5413fc,_0x1e454f)),Math['round'](_0x5413fc);},_0xab9a9c[_0xf92ab0(0x1e0)]=async function(_0x36d2e6){var _0x27c7c3=_0xf92ab0;errorlog(_0x27c7c3(0x994)),await new Promise(_0xd3dbea=>setTimeout(_0xd3dbea,0x7d0));try{_0xab9a9c[_0x27c7c3(0x6b1)]=null,await _0xab9a9c[_0x27c7c3(0xba0)](_0x36d2e6),log(_0x27c7c3(0x9ad));}catch(_0xbe397e){errorlog(_0x27c7c3(0xb92),_0xbe397e),!_0xab9a9c[_0x27c7c3(0xca4)]&&warnUser(_0x27c7c3(0x69c));}},_0xab9a9c[_0xf92ab0(0x27d)]=function(){var _0x1ead49=_0xf92ab0;if(_0xab9a9c['videoElement']&&_0xab9a9c['videoElement']['srcObject'])return _0xab9a9c[_0x1ead49(0x7eb)][_0x1ead49(0x5f6)];else return _0xab9a9c[_0x1ead49(0x7eb)]&&_0xab9a9c[_0x1ead49(0x7eb)][_0x1ead49(0x5e6)]&&_0xab9a9c[_0x1ead49(0xca7)]?_0xab9a9c[_0x1ead49(0xca7)]:(log('checkBasicStreamsExist'),checkBasicStreamsExist(),_0xab9a9c['videoElement'][_0x1ead49(0x5f6)]);};var _0xf8a630=0x0,_0xcbc3a2=0x0;_0xab9a9c[_0xf92ab0(0xba0)]=async function(_0x2d8a03=null){var _0x56fc68=_0xf92ab0;if(_0xab9a9c[_0x56fc68(0x6b1)]!==null)return;else _0xab9a9c['chunkedVideoEnabled']=![];!_0x2d8a03&&_0xab9a9c['stats']['Chunked_video']&&(_0x2d8a03=_0xab9a9c[_0x56fc68(0x287)][_0x56fc68(0x2b0)]);let _0x5a4ee7=0x0;var _0x4dae77=_0xab9a9c[_0x56fc68(0x27d)]()['getVideoTracks']();if(!_0x4dae77||!_0x4dae77[_0x56fc68(0xade)]){warnlog(_0x56fc68(0x838)),_0xab9a9c[_0x56fc68(0x6b1)]=null;return;}_0x4dae77=_0x4dae77[0x0];var _0x44c5c2=new MediaStreamTrackProcessor(_0x4dae77),_0x38aa11=_0x44c5c2['readable'];const _0x3b20c5=_0x38aa11[_0x56fc68(0xbf2)]();_0xcbc3a2+=0x1,_0x3b20c5['counterWebCodec']=_0xcbc3a2;var _0x39c3bf=![],_0x1394ff=-0x1,_0x4abea0=-0x1;let _0x335441=![];const _0x399212=()=>{var _0x41f7a0=_0x56fc68;if(_0x335441)return;_0x335441=!![];try{_0x3b20c5[_0x41f7a0(0x688)]();}catch(_0x4fc98f){}};let _0x2d97e8;const _0x167d1c={'output':async _0x409631=>{var _0x36253c=_0x56fc68;if(!_0xab9a9c[_0x36253c(0xbc9)]||!_0xab9a9c[_0x36253c(0xbc9)][_0x36253c(0x1f8)])return;else{if(_0x409631[_0x36253c(0x8c5)][_0x36253c(0x6d5)]==_0x36253c(0xbc8)){let _0x59c8fe=new Uint8Array(_0x409631[_0x36253c(0xc04)]);_0x409631['copyTo'](_0x59c8fe);typeof _0xab9a9c['chunkedRecorder'][_0x36253c(0x8e8)]===_0x36253c(0x32c)?_0xab9a9c[_0x36253c(0xbc9)][_0x36253c(0x8e8)]({'media':_0x36253c(0xa55),'frameType':_0x409631['type'],'timestamp':_0x409631[_0x36253c(0x9f5)]-_0x4abea0,'data':_0x59c8fe}):(_0xab9a9c[_0x36253c(0x2ca)]['push']([_0x409631[_0x36253c(0x9f5)]-_0x4abea0,_0x409631[_0x36253c(0x3c5)]]),_0xab9a9c[_0x36253c(0x2ca)][_0x36253c(0x5e7)](_0x59c8fe));_0xab9a9c[_0x36253c(0xc30)]&&pokeIframeAPI(_0x36253c(0x4cd),{'type':_0x409631[_0x36253c(0x3c5)],'ts':_0x409631[_0x36253c(0x9f5)]-_0x4abea0});try{await _0xab9a9c[_0x36253c(0xbc9)][_0x36253c(0x1f8)](_0x36253c(0xa55));}catch(_0xa14184){errorlog(_0xa14184);}}}},'error':_0x49f7b6=>{var _0xd4fc58=_0x56fc68;errorlog(_0x49f7b6),_0x39c3bf=!![],_0xab9a9c[_0xd4fc58(0x6b1)]=null;_0xab9a9c['chunkedRecorder']&&(_0xab9a9c[_0xd4fc58(0xbc9)]['needKeyFrame']=!![]);try{_0x3b20c5&&_0x3b20c5[_0xd4fc58(0x60a)]()[_0xd4fc58(0xacc)](()=>{});}catch(_0x54b244){}_0x399212();try{_0x2d97e8&&_0x2d97e8[_0xd4fc58(0x4e0)]!==_0xd4fc58(0x5e8)&&_0x2d97e8[_0xd4fc58(0x3ef)]();}catch(_0xa5abd4){}_0xab9a9c[_0xd4fc58(0x1e0)](_0x2d8a03);}};_0x2d97e8=new VideoEncoder(_0x167d1c),_0x2d97e8[_0x56fc68(0x8da)]=_0x2d8a03,_0x2d97e8[_0x56fc68(0x8e5)](_0x2d8a03),_0xab9a9c[_0x56fc68(0x287)][_0x56fc68(0x2b0)]=_0x2d8a03,_0xab9a9c[_0x56fc68(0xbc9)][_0x56fc68(0x72c)]=_0x2d97e8;var _0x5919e1,_0x484f52=new Promise((_0xd2f835,_0x1189f3)=>{_0x5919e1=_0xd2f835;});return _0x484f52[_0x56fc68(0x30f)]=_0x5919e1,_0x3b20c5[_0x56fc68(0x993)]()['then'](function _0x26a19d({done:_0x1ae454,value:_0x48fda0}){var _0xddfaf2=_0x56fc68;if(_0x1ae454||_0x39c3bf){try{_0x2d97e8[_0xddfaf2(0x3ef)]();}catch(_0x51727f){}_0x48fda0&&_0x48fda0[_0xddfaf2(0x3ef)]();warnlog(_0xddfaf2(0x95f)),_0x399212();return;}else{if(_0x2d97e8[_0xddfaf2(0x4e0)]=='closed'){_0x48fda0&&_0x48fda0[_0xddfaf2(0x3ef)]();warnlog('\x20else\x20if\x20(encoder.state\x20==\x20\x27closed\x27'),_0x399212();return;}}_0x4abea0==-0x1&&(_0x4abea0=_0x48fda0[_0xddfaf2(0x9f5)],_0xab9a9c[_0xddfaf2(0x287)][_0xddfaf2(0x2b0)][_0xddfaf2(0xac7)]=Date['now'](),_0x484f52[_0xddfaf2(0x30f)]());_0x1394ff==_0x48fda0['timestamp']&&(_0x48fda0[_0xddfaf2(0x9f5)]+=0x1,warnlog(_0xddfaf2(0xa5a)));if(!_0x39c3bf){_0x1394ff=_0x48fda0[_0xddfaf2(0x9f5)],_0x5a4ee7++;if(_0xab9a9c[_0xddfaf2(0xbc9)][_0xddfaf2(0x1ff)]){const _0x573975=_0x5a4ee7>=0x3c;_0x573975&&(_0x5a4ee7=0x0,_0xab9a9c[_0xddfaf2(0xbc9)]['needKeyFrame']=![],warnlog(_0xddfaf2(0x960)));try{_0x2d97e8['encode'](_0x48fda0,{'keyFrame':_0x573975});}catch(_0x5b71d8){errorlog(_0x5b71d8);}}else try{_0x2d97e8[_0xddfaf2(0x736)](_0x48fda0,{'keyFrame':![]});}catch(_0x3cbb03){errorlog(_0x3cbb03);}}_0x48fda0[_0xddfaf2(0x3ef)](),_0x3b20c5[_0xddfaf2(0x993)]()['then'](_0x26a19d);}),_0xab9a9c[_0x56fc68(0x6b1)]=!![],_0x484f52;},_0xab9a9c[_0xf92ab0(0x654)]=async function(_0x24180c){var _0x245d04=_0xf92ab0;if(_0xab9a9c[_0x245d04(0xb1a)]!==null)return;else _0xab9a9c[_0x245d04(0xb1a)]=![];!_0x24180c&&_0xab9a9c['stats'][_0x245d04(0x77a)]&&(_0x24180c=_0xab9a9c[_0x245d04(0x287)][_0x245d04(0x77a)]);var _0x4d4986=_0xab9a9c[_0x245d04(0x27d)](),_0x5602de=_0x4d4986['getAudioTracks']();if(!_0x5602de||!_0x5602de[_0x245d04(0xade)]){_0xab9a9c[_0x245d04(0xb1a)]=null;return;}_0x5602de=_0x5602de[0x0];var _0x2b085a=_0x5602de[_0x245d04(0x498)]();_0x24180c[_0x245d04(0x51c)]>_0x2b085a[_0x245d04(0xa87)]&&(_0x24180c[_0x245d04(0x51c)]=_0x2b085a['channelCount'],_0x24180c['channels']=_0x2b085a[_0x245d04(0xa87)]);if(_0x24180c['sampleRate']!=_0x2b085a['sampleRate'])try{_0x4d4986=outboundAudioPipeline();}catch(_0x5498fc){errorlog(_0x5498fc);}var _0x92f519=new MediaStreamTrackProcessor(_0x4d4986['getAudioTracks']()[0x0]),_0x28023f=_0x92f519['readable'];const _0xc783fa=_0x28023f[_0x245d04(0xbf2)]();var _0x53cd4f=![],_0x1550fc=-0x1,_0x1a916e=-0x1;const _0x15ca24={'output':async _0x57d3a9=>{var _0x513515=_0x245d04;if(!_0xab9a9c['chunkedRecorder']||!_0xab9a9c[_0x513515(0xbc9)]['sendChunks'])return;else{if(_0x57d3a9['constructor'][_0x513515(0x6d5)]==_0x513515(0x7e2)){let _0x4668c6=new Uint8Array(_0x57d3a9[_0x513515(0xc04)]);_0x57d3a9['copyTo'](_0x4668c6);typeof _0xab9a9c['chunkedRecorder'][_0x513515(0x8e8)]===_0x513515(0x32c)?_0xab9a9c[_0x513515(0xbc9)]['enqueueFrame']({'media':_0x513515(0x544),'frameType':'audio','timestamp':_0x57d3a9[_0x513515(0x9f5)]-_0x1a916e,'data':_0x4668c6}):(_0xab9a9c[_0x513515(0x2ca)]['push']([_0x57d3a9[_0x513515(0x9f5)]-_0x1a916e,_0x513515(0x544)]),_0xab9a9c[_0x513515(0x2ca)][_0x513515(0x5e7)](_0x4668c6));_0xab9a9c[_0x513515(0xc30)]&&pokeIframeAPI(_0x513515(0x4cd),{'type':_0x513515(0x544),'ts':_0x57d3a9[_0x513515(0x9f5)]-_0x1a916e});try{await _0xab9a9c[_0x513515(0xbc9)][_0x513515(0x1f8)](_0x513515(0x544));}catch(_0x2604f3){errorlog(_0x2604f3);if(!_0xab9a9c[_0x513515(0xbc9)]){}}}}},'error':_0x4a0dca=>{errorlog(_0x4a0dca);}};let _0x31e06c=new AudioEncoder(_0x15ca24);_0x31e06c[_0x245d04(0x8da)]=_0x24180c,_0x31e06c[_0x245d04(0x8e5)](_0x24180c),_0xab9a9c[_0x245d04(0x287)][_0x245d04(0x77a)]={},_0xab9a9c['stats'][_0x245d04(0x77a)]['codec']=_0x24180c[_0x245d04(0x44b)],_0xab9a9c['stats'][_0x245d04(0x77a)][_0x245d04(0x51c)]=_0x24180c[_0x245d04(0x51c)],_0xab9a9c[_0x245d04(0x287)][_0x245d04(0x77a)]['sampleRate']=_0x24180c[_0x245d04(0xa5c)],_0xab9a9c[_0x245d04(0x287)][_0x245d04(0x77a)][_0x245d04(0x42f)]=_0x24180c[_0x245d04(0x737)][_0x245d04(0x42f)];var _0x4429e0,_0x286815=new Promise((_0x177362,_0x5cbe61)=>{_0x4429e0=_0x177362;});return _0x286815[_0x245d04(0x30f)]=_0x4429e0,_0xc783fa['read']()['then'](function _0x46c3ea({done:_0xfc779c,value:_0x3e87fc}){var _0x5f1baf=_0x245d04;if(_0xfc779c||_0x53cd4f){_0x31e06c[_0x5f1baf(0x3ef)]();_0x3e87fc&&_0x3e87fc['close']();_0xab9a9c[_0x5f1baf(0xb1a)]=null;return;}else{if(_0x31e06c[_0x5f1baf(0x4e0)]==_0x5f1baf(0x5e8)){_0x3e87fc&&_0x3e87fc['close']();_0xab9a9c['chunkedAudioEnabled']=null;return;}}try{_0x1a916e==-0x1&&(_0x1a916e=_0x3e87fc['timestamp'],_0xab9a9c['stats'][_0x5f1baf(0x77a)]['realTime']=Date['now'](),_0x286815[_0x5f1baf(0x30f)]());_0x1550fc==_0x3e87fc['timestamp']&&(_0x3e87fc[_0x5f1baf(0x9f5)]+=0x1);if(!_0x53cd4f){_0x1550fc=_0x3e87fc[_0x5f1baf(0x9f5)];try{_0x31e06c[_0x5f1baf(0x736)](_0x3e87fc);}catch(_0x57200d){errorlog(_0x57200d);}}_0x3e87fc[_0x5f1baf(0x3ef)](),_0xc783fa[_0x5f1baf(0x993)]()['then'](_0x46c3ea);}catch(_0x57c7a8){errorlog(_0x57c7a8),errorlog(_0x3e87fc),errorlog(_0xfc779c);}}),_0xab9a9c[_0x245d04(0xb1a)]=!![],_0x286815;},_0xab9a9c[_0xf92ab0(0x468)]=function(_0x40330d,_0x2557eb={}){var _0x59ddba=_0xf92ab0;warnlog(_0x59ddba(0x4d0));const _0x443623=new window['AudioContext']({'sampleRate':_0x2557eb[_0x59ddba(0xa5c)]||0xbb80}),_0x517dc9=_0x443623['createMediaStreamSource'](_0x40330d),_0x1981f4=0x800,_0x23f6da=(_0x443623[_0x59ddba(0xb37)]||_0x443623[_0x59ddba(0x69d)])[_0x59ddba(0xb7e)](_0x443623,_0x1981f4,0x1,0x1);return _0x23f6da['onaudioprocess']=async function(_0x3a1337){var _0x7b6a23=_0x59ddba,_0x84eaa3=new Uint8Array(_0x3a1337[_0x7b6a23(0x40c)][_0x7b6a23(0xbef)](0x0)[_0x7b6a23(0x657)]);_0xab9a9c[_0x7b6a23(0x2ca)][_0x7b6a23(0x5e7)]([0x0,_0x7b6a23(0xcc0)]),_0xab9a9c[_0x7b6a23(0x2ca)][_0x7b6a23(0x5e7)](_0x84eaa3);try{await _0xab9a9c[_0x7b6a23(0xbc9)][_0x7b6a23(0x1f8)](_0x7b6a23(0xcc0));}catch(_0x230c1c){errorlog(_0x230c1c),!_0xab9a9c[_0x7b6a23(0xbc9)]&&encoder[_0x7b6a23(0x3ef)]();}},_0x517dc9[_0x59ddba(0x3ea)](_0x23f6da),_0x23f6da[_0x59ddba(0x3ea)](_0x443623[_0x59ddba(0x5bb)]),_0xab9a9c[_0x59ddba(0x287)][_0x59ddba(0x77a)]={},_0xab9a9c[_0x59ddba(0xb1a)]=!![],_0x23f6da;},_0xab9a9c['retransmitChunkedStream']=async function(_0x4eaa32=![],_0x5e0b93=![]){var _0x586347=_0xf92ab0;if(!_0xab9a9c[_0x586347(0xbc9)]){warnlog(_0x586347(0x8ed));var _0x13a5c5=null;_0xab9a9c[_0x586347(0xbc9)]={};const _0x122316=Number[_0x586347(0x9e8)](_0xab9a9c[_0x586347(0xbfb)])?_0xab9a9c[_0x586347(0xbfb)]:parseInt(_0xab9a9c['chunkchunksize'])||0x4000,_0xb940b7=Math[_0x586347(0x642)](0x800,Math[_0x586347(0xb03)](0x10000,_0x122316)),_0x4d0ade=Number[_0x586347(0x9e8)](_0xab9a9c[_0x586347(0xa37)])?_0xab9a9c[_0x586347(0xa37)]:parseInt(_0xab9a9c[_0x586347(0xa37)])||0x0,_0x14477e=_0x4d0ade>=0x2?_0x4d0ade:0x0,_0x535bf6=!!_0xab9a9c[_0x586347(0xa2d)],_0x9f17b7=_0x535bf6||_0x14477e>=0x2,_0x5beadd=Number[_0x586347(0x9e8)](_0xab9a9c[_0x586347(0x801)])?_0xab9a9c[_0x586347(0x801)]:parseInt(_0xab9a9c['chunkretry'])||0x0,_0x5bbd35=Number['isFinite'](_0xab9a9c[_0x586347(0xc47)])?_0xab9a9c['chunkcache']:parseInt(_0xab9a9c[_0x586347(0xc47)])||0x0,_0x181a61=Number[_0x586347(0x9e8)](_0xab9a9c[_0x586347(0x5ce)])?_0xab9a9c[_0x586347(0x5ce)]:0x1f4,_0x470d13=_0x5bbd35||Math[_0x586347(0x642)](0x1770,_0x5beadd||0x0,_0x181a61*0x6),_0x52d3fd=0x4000000;_0xab9a9c['chunkedRecorder'][_0x586347(0x224)]={'enabled':_0x9f17b7,'chunkSize':_0xb940b7,'fecRate':_0x14477e,'nack':_0x535bf6,'cache':new Map(),'cacheOrder':[],'cacheBytes':0x0,'maxCacheMs':_0x470d13,'maxCacheBytes':_0x52d3fd,'nextFrameId':Math[_0x586347(0x2f1)]()*0x7fffffff|0x0,'stats':{'fecParityBlocks':0x0,'retransmits':0x0}},_0xab9a9c[_0x586347(0xbc9)][_0x586347(0x40f)]=function(){var _0x4fb257=_0x586347;const _0x5ab0d3=_0xab9a9c[_0x4fb257(0xbc9)]['reliability'];return _0x5ab0d3[_0x4fb257(0x38a)]=_0x5ab0d3[_0x4fb257(0x38a)]+0x1>>>0x0,_0x5ab0d3['nextFrameId']>0xfffffffe&&(_0x5ab0d3[_0x4fb257(0x38a)]=0x1),_0x5ab0d3[_0x4fb257(0x38a)];},_0xab9a9c[_0x586347(0xbc9)][_0x586347(0x2de)]=function(){var _0x29d9b1=_0x586347;const _0x2dda79=_0xab9a9c[_0x29d9b1(0xbc9)]['reliability'];if(!_0x2dda79['enabled'])return;const _0x24b8d8=Date['now']();while(_0x2dda79[_0x29d9b1(0x25b)][_0x29d9b1(0xade)]){const _0xb4d90e=_0x2dda79[_0x29d9b1(0x25b)][0x0],_0x3f35bf=_0x2dda79[_0x29d9b1(0xb71)][_0x29d9b1(0x666)](_0xb4d90e);if(!_0x3f35bf){_0x2dda79['cacheOrder']['shift']();continue;}const _0x2d5fbc=_0x24b8d8-_0x3f35bf[_0x29d9b1(0x55b)];if(_0x2dda79[_0x29d9b1(0x386)]>_0x2dda79[_0x29d9b1(0x445)]||_0x2d5fbc>_0x2dda79[_0x29d9b1(0x697)])_0x2dda79['cache'][_0x29d9b1(0x6ea)](_0xb4d90e),_0x2dda79['cacheOrder'][_0x29d9b1(0xa17)](),_0x2dda79[_0x29d9b1(0x386)]=Math[_0x29d9b1(0x642)](0x0,_0x2dda79['cacheBytes']-_0x3f35bf['totalBytes']);else break;}},_0xab9a9c[_0x586347(0xbc9)][_0x586347(0x2bd)]=function(_0x4ae4eb){var _0x1213cc=_0x586347;const _0x518960=_0xab9a9c['chunkedRecorder']['reliability'];if(!_0x518960[_0x1213cc(0x8e4)])return;_0x518960[_0x1213cc(0xb71)][_0x1213cc(0x43d)](_0x4ae4eb[_0x1213cc(0x9d7)],_0x4ae4eb),_0x518960[_0x1213cc(0x25b)][_0x1213cc(0x5e7)](_0x4ae4eb['frameId']),_0x518960['cacheBytes']+=_0x4ae4eb[_0x1213cc(0x64f)],_0xab9a9c[_0x1213cc(0xbc9)]['pruneReliabilityCache'](),!_0xab9a9c['stats']&&(_0xab9a9c[_0x1213cc(0x287)]={}),_0xab9a9c['stats']['chunkedFecParity']=_0x518960['stats'][_0x1213cc(0xbd7)];},_0xab9a9c['chunkedRecorder'][_0x586347(0x8e8)]=function({media:_0x16842f,frameType:_0x21385a,timestamp:_0x572012,data:_0x59f012}){var _0x3adbb6=_0x586347;if(!(_0x59f012 instanceof Uint8Array))return;const _0x2ae7f9=_0xab9a9c[_0x3adbb6(0xbc9)]['reliability'],_0x3221ae=Math['max'](0x1,Math['min'](0x40000,_0x2ae7f9[_0x3adbb6(0x227)]||_0x59f012['byteLength'])),_0x2f6f56=_0xab9a9c[_0x3adbb6(0xbc9)][_0x3adbb6(0x40f)](),_0x25f7b5=_0xab9a9c[_0x3adbb6(0xbc9)]['adaptation'];if(_0x16842f===_0x3adbb6(0xa55)&&_0x21385a!=='key'&&_0x25f7b5&&(_0x25f7b5[_0x3adbb6(0x967)]===_0x3adbb6(0x1ef)||_0x25f7b5['mode']===_0x3adbb6(0xb3e))&&_0x25f7b5[_0x3adbb6(0x4fc)]>=0x1){_0x25f7b5[_0x3adbb6(0x4fc)]=Math[_0x3adbb6(0x642)](0x0,_0x25f7b5[_0x3adbb6(0x4fc)]-0x1),_0x25f7b5[_0x3adbb6(0x814)]=(_0x25f7b5[_0x3adbb6(0x814)]||0x0)+0x1,_0xab9a9c[_0x3adbb6(0x287)][_0x3adbb6(0x66c)]=_0x25f7b5[_0x3adbb6(0x814)];return;}const _0x2ae602=[],_0x279a42=[];let _0x14150b=0x0,_0x3c0357=0x0;while(_0x14150b<_0x59f012[_0x3adbb6(0xc04)]){const _0x4058c6=Math[_0x3adbb6(0xb03)](_0x59f012[_0x3adbb6(0xc04)],_0x14150b+Math[_0x3adbb6(0x642)](0x1,_0x3221ae)),_0x176f9a=_0x59f012[_0x3adbb6(0xc94)](_0x14150b,_0x4058c6);_0x2ae602[_0x3adbb6(0x5e7)](_0x176f9a),_0x279a42[_0x3adbb6(0x5e7)]({'index':_0x3c0357,'size':_0x176f9a['byteLength'],'group':_0x2ae7f9[_0x3adbb6(0x8e3)]>=0x2?Math[_0x3adbb6(0x502)](_0x3c0357/_0x2ae7f9[_0x3adbb6(0x8e3)]):0x0}),_0x14150b=_0x4058c6,_0x3c0357+=0x1;}!_0x2ae602[_0x3adbb6(0xade)]&&(_0x2ae602['push'](new Uint8Array(0x0)),_0x279a42[_0x3adbb6(0x5e7)]({'index':0x0,'size':0x0,'group':0x0}));const _0x723c29=[],_0x312ffe=[];if(_0x2ae7f9[_0x3adbb6(0x8e4)]&&_0x2ae7f9['fecRate']>=0x2){for(let _0x3bc011=0x0;_0x3bc011<_0x2ae602[_0x3adbb6(0xade)];_0x3bc011+=_0x2ae7f9[_0x3adbb6(0x8e3)]){const _0x11ddfb=_0x2ae602[_0x3adbb6(0x912)](_0x3bc011,_0x3bc011+_0x2ae7f9['fecRate']);if(!_0x11ddfb[_0x3adbb6(0xade)])continue;const _0xc7e69e=_0x11ddfb[_0x3adbb6(0x24b)]((_0x353da2,_0xc1fc7)=>Math[_0x3adbb6(0x642)](_0x353da2,_0xc1fc7['byteLength']),0x0),_0x16d975=new Uint8Array(_0xc7e69e);if(_0xc7e69e)for(const _0x5b3d21 of _0x11ddfb){for(let _0x5cdceb=0x0;_0x5cdceb<_0xc7e69e;_0x5cdceb++){const _0xd2dc56=_0x5cdceb<_0x5b3d21[_0x3adbb6(0xc04)]?_0x5b3d21[_0x5cdceb]:0x0;_0x16d975[_0x5cdceb]^=_0xd2dc56;}}_0x723c29['push'](_0x16d975),_0x312ffe[_0x3adbb6(0x5e7)]({'group':Math[_0x3adbb6(0x502)](_0x3bc011/_0x2ae7f9[_0x3adbb6(0x8e3)]),'groupStart':_0x3bc011,'groupSize':_0x11ddfb['length'],'size':_0xc7e69e});}_0x2ae7f9[_0x3adbb6(0x287)][_0x3adbb6(0xbd7)]+=_0x723c29[_0x3adbb6(0xade)];}const _0x2a25c5={'version':0x1,'frameId':_0x2f6f56,'media':_0x16842f,'frameType':_0x21385a,'totalChunks':_0x2ae602[_0x3adbb6(0xade)],'parityChunks':_0x723c29[_0x3adbb6(0xade)],'chunkSize':_0x3221ae,'fecRate':_0x2ae7f9[_0x3adbb6(0x8e3)],'nack':_0x2ae7f9[_0x3adbb6(0x50c)],'dataBytes':_0x59f012[_0x3adbb6(0xc04)],'descriptors':_0x279a42,'parityDescriptors':_0x312ffe},_0x6a0b03=[_0x572012,_0x21385a,_0x2a25c5];_0xab9a9c[_0x3adbb6(0x2ca)][_0x3adbb6(0x5e7)](_0x6a0b03);for(const _0x3c18fe of _0x2ae602){_0xab9a9c['chunksQueue'][_0x3adbb6(0x5e7)](_0x3c18fe);}for(const _0x359ca1 of _0x723c29){_0xab9a9c[_0x3adbb6(0x2ca)][_0x3adbb6(0x5e7)](_0x359ca1);}_0xab9a9c[_0x3adbb6(0xbc9)][_0x3adbb6(0x2bd)]({'frameId':_0x2f6f56,'timestamp':_0x572012,'media':_0x16842f,'frameType':_0x21385a,'dataChunks':_0x2ae602,'parityChunks':_0x723c29,'parityDescriptors':_0x312ffe,'createdAt':Date[_0x3adbb6(0x30e)](),'totalBytes':_0x59f012[_0x3adbb6(0xc04)]+_0x723c29[_0x3adbb6(0x24b)]((_0x44ee59,_0x359d06)=>_0x44ee59+_0x359d06['byteLength'],0x0)});},_0xab9a9c[_0x586347(0xbc9)]['handleNack']=function(_0x140732,_0x5a6790){var _0x2b5e42=_0x586347;const _0x382132=_0xab9a9c[_0x2b5e42(0xbc9)][_0x2b5e42(0x224)];if(!_0x382132||!_0x382132[_0x2b5e42(0x8e4)])return;if(!_0x5a6790||typeof _0x5a6790[_0x2b5e42(0x9d7)]===_0x2b5e42(0x2cf))return;const _0x13125c=_0x5a6790[_0x2b5e42(0x9d7)]>>>0x0,_0x13f926=_0xab9a9c[_0x2b5e42(0x532)][_0x140732];if(!_0x13f926||_0x13f926[_0x2b5e42(0x70b)]!==_0x2b5e42(0xa48))return;const _0x8e18bf=_0x382132[_0x2b5e42(0xb71)][_0x2b5e42(0x666)](_0x13125c);if(!_0x8e18bf)return;const _0x29777a=!!_0x5a6790[_0x2b5e42(0x73f)];let _0x40ca17=Number[_0x2b5e42(0x9e8)](_0x5a6790[_0x2b5e42(0x8bd)])?parseInt(_0x5a6790[_0x2b5e42(0x8bd)]):-0x1,_0xc5167c=Number[_0x2b5e42(0x9e8)](_0x5a6790[_0x2b5e42(0x4cb)])?parseInt(_0x5a6790[_0x2b5e42(0x4cb)]):-0x1,_0x22cb06=null,_0x2def63=null;_0x29777a?(_0xc5167c<0x0&&(_0xc5167c=Math[_0x2b5e42(0x642)](0x0,_0x40ca17)),_0x22cb06=_0x8e18bf[_0x2b5e42(0x8ee)][_0xc5167c],_0x2def63=_0x8e18bf[_0x2b5e42(0x53d)][_0xc5167c]||null):_0x22cb06=_0x8e18bf[_0x2b5e42(0x3b4)][_0x40ca17];if(!_0x22cb06)return;const _0x5be959=_0x8e18bf['parityDescriptors']||[],_0x2e3b2b={'version':0x1,'frameId':_0x8e18bf[_0x2b5e42(0x9d7)],'media':_0x8e18bf[_0x2b5e42(0x60b)],'frameType':_0x8e18bf[_0x2b5e42(0x601)],'totalChunks':_0x8e18bf[_0x2b5e42(0x3b4)][_0x2b5e42(0xade)],'parityChunks':_0x8e18bf[_0x2b5e42(0x8ee)][_0x2b5e42(0xade)],'chunkSize':_0x382132[_0x2b5e42(0x227)],'fecRate':_0x382132['fecRate'],'nack':_0x382132[_0x2b5e42(0x50c)],'resend':!![],'parity':_0x29777a,'chunkIndex':_0x29777a?_0xc5167c:_0x40ca17,'group':_0x29777a&&_0x2def63?_0x2def63['group']:_0x382132[_0x2b5e42(0x8e3)]>=0x2?Math[_0x2b5e42(0x502)](Math[_0x2b5e42(0x642)](_0x40ca17,0x0)/_0x382132[_0x2b5e42(0x8e3)]):0x0,'groupSize':_0x29777a&&_0x2def63?_0x2def63['groupSize']:_0x382132[_0x2b5e42(0x8e3)]>=0x2?Math[_0x2b5e42(0xb03)](_0x382132[_0x2b5e42(0x8e3)],_0x8e18bf[_0x2b5e42(0x3b4)]['length']-Math[_0x2b5e42(0x502)](Math[_0x2b5e42(0x642)](_0x40ca17,0x0)/_0x382132[_0x2b5e42(0x8e3)])*_0x382132[_0x2b5e42(0x8e3)]):_0x8e18bf['dataChunks'][_0x2b5e42(0xade)]};try{const _0x247f58=JSON['stringify']([_0x8e18bf[_0x2b5e42(0x9f5)],_0x8e18bf[_0x2b5e42(0x601)],_0x2e3b2b,0x0]);_0x13f926['send'](_0x247f58),_0x13f926['send'](_0x22cb06),_0x382132['stats'][_0x2b5e42(0x1b4)]+=0x1,!_0xab9a9c[_0x2b5e42(0x287)]&&(_0xab9a9c[_0x2b5e42(0x287)]={}),_0xab9a9c[_0x2b5e42(0x287)][_0x2b5e42(0x9cc)]=(_0xab9a9c[_0x2b5e42(0x287)][_0x2b5e42(0x9cc)]||0x0)+0x1,_0xab9a9c[_0x2b5e42(0x287)][_0x2b5e42(0x4ac)]=(_0xab9a9c[_0x2b5e42(0x287)]['chunkedNacksHandled']||0x0)+0x1;}catch(_0x39a14c){errorlog(_0x39a14c);}},_0xab9a9c['chunkedDetails']=_0x4eaa32||![],_0xab9a9c[_0x586347(0xa08)]&&(_0xab9a9c['chunkedRecorder'][_0x586347(0x686)]=_0x5e0b93),_0xab9a9c[_0x586347(0xbc9)][_0x586347(0x1f8)]=async function(_0x1bdada=_0x586347(0x644)){var _0x4e8f3b=_0x586347;if(_0x13a5c5)return;_0x13a5c5=!![];var _0x10b9cf=_0x1bdada;const _0x21e5dd=_0xab9a9c[_0x4e8f3b(0xbc9)][_0x4e8f3b(0x3e7)]=_0xab9a9c[_0x4e8f3b(0xbc9)][_0x4e8f3b(0x3e7)]||{};for(const _0x376000 in _0x21e5dd){(!_0xab9a9c[_0x4e8f3b(0x532)][_0x376000]||!_0xab9a9c[_0x4e8f3b(0x84f)][_0x376000])&&delete _0x21e5dd[_0x376000];}const _0x6005a7={0x0:0x0,0x1:0x0,0x2:0x0};let _0x2cae49=0x0;function _0x484f95(_0xa2095b){var _0x33999c=_0x4e8f3b;if(!_0xa2095b)return 0x2;if(_0xa2095b['scene']!==![]&&_0xa2095b[_0x33999c(0x268)]!==undefined&&_0xa2095b[_0x33999c(0x268)]!==null)return 0x0;if(_0xa2095b[_0x33999c(0x71b)]!==null&&_0xa2095b[_0x33999c(0x71b)]!==undefined&&_0xa2095b['sceneDisplay']!==![])return 0x0;if(_0xa2095b[_0x33999c(0x1ab)]===!![]||_0xa2095b[_0x33999c(0x8d5)]===!![])return 0x2;return 0x1;}function _0x1f9891(_0x4c362d){switch(_0x4c362d){case 0x0:return 0x180000;case 0x1:return 0x100000;default:return 0xc0000;}}function _0x293ace(_0x1cdc25,_0x20adad){var _0x2ffa3c=_0x4e8f3b;return!_0x21e5dd[_0x1cdc25]&&(_0x21e5dd[_0x1cdc25]={'priority':_0x20adad,'skipped':0x0,'sent':0x0,'buffered':0x0,'stalled':![],'consecutiveHigh':0x0}),_0x21e5dd[_0x1cdc25][_0x2ffa3c(0x98b)]=_0x20adad,_0x21e5dd[_0x1cdc25];}function _0x1cf8fd(_0x362f30,_0x3d4173,_0x56c465={}){var _0x4e57d6=_0x4e8f3b,_0x6c98ac=_0xab9a9c[_0x4e57d6(0x532)][_0x362f30],_0x47f213=_0xab9a9c[_0x4e57d6(0x84f)][_0x362f30];if(!_0x6c98ac||!_0x47f213||_0x6c98ac[_0x4e57d6(0x70b)]!==_0x4e57d6(0xa48))return![];var _0x5096fc=_0x56c465['metadata']===!![];!_0x47f213['stats']&&(_0x47f213[_0x4e57d6(0x287)]={});var _0x34cdfd=_0x484f95(_0x47f213),_0x40d68a=_0x293ace(_0x362f30,_0x34cdfd);!_0x6c98ac[_0x4e57d6(0xcbd)]&&(_0x6c98ac[_0x4e57d6(0xcbd)]=![]);!_0x6c98ac['audioHeaderSent']&&(_0x6c98ac[_0x4e57d6(0xc6d)]=![]);!_0x6c98ac['detailsSent']&&(_0x6c98ac[_0x4e57d6(0x7c0)]=![]);if(_0x10b9cf===_0x4e57d6(0x610)&&!_0x6c98ac[_0x4e57d6(0xcbd)])return warnlog('Waiting\x20for\x20keyframe\x20/\x20header\x20before\x20sending\x20delta\x20/\x20raw\x20video\x20data'),_0xab9a9c[_0x4e57d6(0xbc9)]&&(_0xab9a9c['chunkedRecorder'][_0x4e57d6(0x1ff)]=!![]),![];if(!_0x5096fc&&(_0x10b9cf===_0x4e57d6(0xa27)||_0x10b9cf==='delta'||_0x10b9cf===_0x4e57d6(0xa55))&&!_0x6c98ac[_0x4e57d6(0xcbd)])return warnlog(_0x4e57d6(0x8b6)),_0xab9a9c['chunkedRecorder']&&(_0xab9a9c[_0x4e57d6(0xbc9)]['needKeyFrame']=!![]),![];if(!_0x5096fc&&(_0x10b9cf===_0x4e57d6(0x544)||_0x10b9cf===_0x4e57d6(0xcc0))&&!_0x6c98ac[_0x4e57d6(0xc6d)])return warnlog(_0x4e57d6(0xbb6)),![];if(!_0x6c98ac[_0x4e57d6(0x7c0)]){if(_0xab9a9c[_0x4e57d6(0xa08)])try{var _0x5b637b={..._0xab9a9c[_0x4e57d6(0xa08)]};_0x5b637b[_0x4e57d6(0x9f5)]=Date['now'](),_0x6c98ac[_0x4e57d6(0x4c6)](JSON['stringify'](_0x5b637b)),_0x6c98ac[_0x4e57d6(0x7c0)]=!![];}catch(_0x2c600a){return _0x40d68a[_0x4e57d6(0x8c2)]=(_0x40d68a[_0x4e57d6(0x8c2)]||0x0)+0x1,_0x40d68a[_0x4e57d6(0x93a)]=!![],_0xab9a9c[_0x4e57d6(0xbc9)][_0x4e57d6(0x1ff)]=!![],![];}else _0x6c98ac[_0x4e57d6(0x7c0)]=!![];}var _0x1ddaa7=_0x6c98ac[_0x4e57d6(0xb79)]||0x0;_0x1ddaa7>_0x2cae49&&(_0x2cae49=_0x1ddaa7);_0x1ddaa7>_0x6005a7[_0x34cdfd]&&(_0x6005a7[_0x34cdfd]=_0x1ddaa7);_0x47f213[_0x4e57d6(0x287)][_0x4e57d6(0xb79)]=_0x1ddaa7;var _0xe7fa3c=_0x10b9cf===_0x4e57d6(0xa55)||_0x10b9cf===_0x4e57d6(0x610),_0x3b6920=_0x1f9891(_0x34cdfd);_0x34cdfd===0x0&&(_0x1ddaa7>_0x3b6920*1.5?_0x40d68a['highPriorityPressure']=(_0x40d68a[_0x4e57d6(0x59e)]||0x0)+0x1:_0x40d68a[_0x4e57d6(0x59e)]=0x0);if(_0xe7fa3c&&_0x34cdfd>0x0&&_0x1ddaa7>_0x3b6920)return _0x40d68a[_0x4e57d6(0xc77)]=(_0x40d68a['skipped']||0x0)+0x1,_0x40d68a[_0x4e57d6(0x71f)]=(_0x40d68a[_0x4e57d6(0x71f)]||0x0)+0x1,_0x40d68a[_0x4e57d6(0xa75)]=_0x1ddaa7,_0x40d68a[_0x4e57d6(0x71f)]>=0x4&&(_0x40d68a['stalled']=!![],_0xab9a9c[_0x4e57d6(0xbc9)][_0x4e57d6(0x1ff)]=!![]),![];try{_0x6c98ac[_0x4e57d6(0x4c6)](_0x3d4173);}catch(_0x4d143d){return _0x40d68a['sendErrors']=(_0x40d68a[_0x4e57d6(0x8c2)]||0x0)+0x1,_0x40d68a[_0x4e57d6(0x93a)]=!![],_0xab9a9c[_0x4e57d6(0xbc9)][_0x4e57d6(0x1ff)]=!![],![];}if(_0x5096fc){if(_0x10b9cf==_0x4e57d6(0xa27)||_0x10b9cf==_0x4e57d6(0xa55))_0x6c98ac[_0x4e57d6(0xcbd)]=!![];else(_0x10b9cf=='audio'||_0x10b9cf==_0x4e57d6(0xcc0))&&(_0x6c98ac['audioHeaderSent']=!![]);}return _0x1ddaa7=_0x6c98ac[_0x4e57d6(0xb79)]||0x0,_0x1ddaa7>_0x2cae49&&(_0x2cae49=_0x1ddaa7),_0x1ddaa7>_0x6005a7[_0x34cdfd]&&(_0x6005a7[_0x34cdfd]=_0x1ddaa7),_0x47f213[_0x4e57d6(0x287)]['bufferedAmount']=_0x1ddaa7,_0x40d68a[_0x4e57d6(0xa75)]=_0x1ddaa7,_0x40d68a[_0x4e57d6(0xc3e)]=(_0x40d68a[_0x4e57d6(0xc3e)]||0x0)+0x1,_0x40d68a['lastSend']=Date[_0x4e57d6(0x30e)](),_0x40d68a[_0x4e57d6(0x71f)]=Math[_0x4e57d6(0x642)](0x0,(_0x40d68a[_0x4e57d6(0x71f)]||0x0)-0x1),_0x40d68a[_0x4e57d6(0x93a)]=![],!![];}while(_0xab9a9c[_0x4e8f3b(0x2ca)][_0x4e8f3b(0xade)]){if(!Object[_0x4e8f3b(0x7db)](_0xab9a9c['chunkedTransferChannels'])[_0x4e8f3b(0xade)]){_0xab9a9c['chunksQueue']=[],_0x13a5c5=null,_0xab9a9c[_0x4e8f3b(0x287)][_0x4e8f3b(0x1cf)]=0x0;return;}_0xab9a9c['stats'][_0x4e8f3b(0x1cf)]=_0xab9a9c['chunksQueue'][_0x4e8f3b(0xade)],_0x2cae49=0x0,_0x6005a7[0x0]=0x0,_0x6005a7[0x1]=0x0,_0x6005a7[0x2]=0x0;var _0x26bfc4=_0xab9a9c[_0x4e8f3b(0x2ca)][_0x4e8f3b(0xa17)]();if(Array[_0x4e8f3b(0x5b1)](_0x26bfc4)){_0x10b9cf=_0x26bfc4[0x1];const _0x430385=_0x26bfc4['slice']();_0x430385[_0x4e8f3b(0x5e7)](_0xab9a9c[_0x4e8f3b(0x2ca)][_0x4e8f3b(0xade)]);var _0x436062=JSON[_0x4e8f3b(0x6d1)](_0x430385);for(var _0x46ccc5 in _0xab9a9c[_0x4e8f3b(0x532)]){if(!_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5])continue;if((_0x10b9cf==_0x4e8f3b(0xa27)||_0x10b9cf==_0x4e8f3b(0x610)||_0x10b9cf==_0x4e8f3b(0xa55))&&!_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5]['allowVideo'])continue;if((_0x10b9cf==_0x4e8f3b(0x544)||_0x10b9cf==_0x4e8f3b(0xcc0))&&!_0xab9a9c['pcs'][_0x46ccc5]['allowAudio'])continue;if(!_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0xcbd)]&&_0x10b9cf==_0x4e8f3b(0x610)){warnlog(_0x4e8f3b(0x8b6));continue;}try{if(_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5]['readyState']===_0x4e8f3b(0xa48)){if(!_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5]['detailsSent']){if(_0xab9a9c[_0x4e8f3b(0xa08)]){var _0x5632d7={..._0xab9a9c['chunkedDetails']};_0x5632d7[_0x4e8f3b(0x9f5)]=Date[_0x4e8f3b(0x30e)](),_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x4c6)](JSON[_0x4e8f3b(0x6d1)](_0x5632d7)),_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x7c0)]=!![];}else continue;}_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x4c6)](_0x436062);if(_0x10b9cf==_0x4e8f3b(0xa27)||_0x10b9cf==_0x4e8f3b(0xa55))_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5]['keyframeSent']=!![];else(_0x10b9cf==_0x4e8f3b(0x544)||_0x10b9cf==_0x4e8f3b(0xcc0))&&(_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0xc6d)]=!![]);_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x287)][_0x4e8f3b(0xb79)]=_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0xb79)],_0x2cae49<_0xab9a9c['pcs'][_0x46ccc5]['stats'][_0x4e8f3b(0xb79)]&&(_0x2cae49=_0xab9a9c['pcs'][_0x46ccc5]['stats'][_0x4e8f3b(0xb79)]);}}catch(_0x56de93){}}}else{if(_0x26bfc4[_0x4e8f3b(0xc04)]>0x40000){for(var _0x46ccc5 in _0xab9a9c[_0x4e8f3b(0x532)]){if(!_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5])continue;if((_0x10b9cf==_0x4e8f3b(0xa27)||_0x10b9cf==_0x4e8f3b(0x610)||_0x10b9cf=='video')&&!_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x295)])continue;if((_0x10b9cf=='audio'||_0x10b9cf==_0x4e8f3b(0xcc0))&&!_0xab9a9c['pcs'][_0x46ccc5][_0x4e8f3b(0x8c0)])continue;if((_0x10b9cf==_0x4e8f3b(0xa27)||_0x10b9cf==_0x4e8f3b(0x610)||_0x10b9cf==_0x4e8f3b(0xa55))&&!_0xab9a9c['chunkedTransferChannels'][_0x46ccc5][_0x4e8f3b(0xcbd)]){warnlog(_0x4e8f3b(0x8b6));continue;}else{if(!_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0xc6d)]&&(_0x10b9cf==_0x4e8f3b(0x544)||_0x10b9cf==_0x4e8f3b(0xcc0))){warnlog(_0x4e8f3b(0xbb6));continue;}}try{if(_0xab9a9c['chunkedTransferChannels'][_0x46ccc5]['readyState']===_0x4e8f3b(0xa48)){if(!_0xab9a9c['chunkedTransferChannels'][_0x46ccc5]['detailsSent']){if(_0xab9a9c['chunkedDetails']){var _0x5632d7={..._0xab9a9c[_0x4e8f3b(0xa08)]};_0x5632d7[_0x4e8f3b(0x9f5)]=Date[_0x4e8f3b(0x30e)](),_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x4c6)](JSON[_0x4e8f3b(0x6d1)](_0x5632d7)),_0xab9a9c['chunkedTransferChannels'][_0x46ccc5][_0x4e8f3b(0x7c0)]=!![];}else continue;}_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x4c6)](_0x26bfc4[_0x4e8f3b(0x912)](0x0,0x40000)),_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x287)][_0x4e8f3b(0xb79)]=_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0xb79)],_0x2cae49<_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x287)][_0x4e8f3b(0xb79)]&&(_0x2cae49=_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x287)][_0x4e8f3b(0xb79)]);}}catch(_0xfbd1fe){}}_0xab9a9c[_0x4e8f3b(0x2ca)][_0x4e8f3b(0xc50)](_0x26bfc4[_0x4e8f3b(0x912)](0x40000));}else for(var _0x46ccc5 in _0xab9a9c[_0x4e8f3b(0x532)]){if(!_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5])continue;if((_0x10b9cf==_0x4e8f3b(0xa27)||_0x10b9cf==_0x4e8f3b(0x610)||_0x10b9cf==_0x4e8f3b(0xa55))&&!_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x295)])continue;if((_0x10b9cf==_0x4e8f3b(0x544)||_0x10b9cf=='pcm')&&!_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x8c0)])continue;try{if(_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x70b)]===_0x4e8f3b(0xa48)){if(!_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5]['detailsSent']){if(_0xab9a9c[_0x4e8f3b(0xa08)]){var _0x5632d7={..._0xab9a9c[_0x4e8f3b(0xa08)]};_0x5632d7[_0x4e8f3b(0x9f5)]=Date[_0x4e8f3b(0x30e)](),_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x4c6)](JSON['stringify'](_0x5632d7)),_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x7c0)]=!![];}else continue;}_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0x4c6)](_0x26bfc4);}_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x287)][_0x4e8f3b(0xb79)]=_0xab9a9c[_0x4e8f3b(0x532)][_0x46ccc5][_0x4e8f3b(0xb79)],_0x2cae49<_0xab9a9c[_0x4e8f3b(0x84f)][_0x46ccc5][_0x4e8f3b(0x287)][_0x4e8f3b(0xb79)]&&(_0x2cae49=_0xab9a9c['pcs'][_0x46ccc5][_0x4e8f3b(0x287)][_0x4e8f3b(0xb79)]);}catch(_0x24cf66){}}}_0xab9a9c[_0x4e8f3b(0x287)][_0x4e8f3b(0xc12)]=_0x2cae49;}_0x13a5c5=null,_0xab9a9c['stats'][_0x4e8f3b(0x1cf)]=0x0;};}for(var _0x280400 in _0xab9a9c[_0x586347(0x84f)]){if(_0xab9a9c['chunkedTransferChannels'][_0x280400]){if(_0xab9a9c[_0x586347(0xa08)]){var _0x3c30e1={..._0xab9a9c[_0x586347(0xa08)]};_0x3c30e1['timestamp']=Date[_0x586347(0x30e)]();if(_0x4eaa32)try{_0xab9a9c['chunkedTransferChannels'][_0x280400][_0x586347(0x4c6)](JSON[_0x586347(0x6d1)](_0x3c30e1)),_0xab9a9c['chunkedTransferChannels'][_0x280400][_0x586347(0x7c0)]=!![];}catch(_0x2c0786){}else{if(!_0xab9a9c['chunkedTransferChannels'][_0x280400]['detailsSent'])try{_0xab9a9c['chunkedTransferChannels'][_0x280400][_0x586347(0x4c6)](JSON[_0x586347(0x6d1)](_0x3c30e1)),_0xab9a9c[_0x586347(0x532)][_0x280400]['detailsSent']=!![];}catch(_0x1636b9){}}}}else{var _0x55ea29='chunked';_0xab9a9c[_0x586347(0x532)][_0x280400]=_0xab9a9c[_0x586347(0x84f)][_0x280400][_0x586347(0x6e9)](_0x55ea29,{'ordered':!![]}),_0xab9a9c['chunkedTransferChannels'][_0x280400][_0x586347(0x535)]=_0x586347(0x9a2),_0xab9a9c[_0x586347(0x532)][_0x280400][_0x586347(0x752)]=_0x586347(0x7e4),_0xab9a9c[_0x586347(0x532)][_0x280400]['header']=![],_0xab9a9c[_0x586347(0x532)][_0x280400][_0x586347(0x7c0)]=![],_0xab9a9c['chunkedTransferChannels'][_0x280400]['timeOffset']=null,_0xab9a9c['chunkedTransferChannels'][_0x280400][_0x586347(0xcbd)]=![],_0xab9a9c[_0x586347(0x532)][_0x280400]['audioHeaderSent']=![],_0xab9a9c[_0x586347(0x532)][_0x280400]['onopen']=()=>{var _0x56e754=_0x586347;log(_0x56e754(0x8c3));if(_0xab9a9c[_0x56e754(0xa08)]){var _0x1816f0={..._0xab9a9c['chunkedDetails']};_0x1816f0[_0x56e754(0x9f5)]=Date[_0x56e754(0x30e)](),_0xab9a9c['chunkedTransferChannels'][_0x280400][_0x56e754(0x4c6)](JSON[_0x56e754(0x6d1)](_0x1816f0)),_0xab9a9c['chunkedTransferChannels'][_0x280400][_0x56e754(0x7c0)]=!![];}},_0xab9a9c[_0x586347(0x532)][_0x280400][_0x586347(0x38c)]=()=>{var _0x42f4a3=_0x586347;try{var _0x32a377=_0xab9a9c[_0x42f4a3(0x6eb)]['indexOf'](_0xab9a9c[_0x42f4a3(0x532)][_0x280400]);_0x32a377>-0x1&&_0xab9a9c[_0x42f4a3(0x6eb)][_0x42f4a3(0x954)](_0x32a377,0x1);}catch(_0xc8eb52){errorlog(_0xc8eb52);}log('re-Transfer\x20ended'),_0xab9a9c['chunkedTransferChannels'][_0x280400]=null,delete _0xab9a9c[_0x42f4a3(0x532)][_0x280400];var _0x1c00d6=![];for(var _0x55ee32=0x0;_0x55ee32<_0xab9a9c['hostedTransfers']['length'];_0x55ee32++){if(_0x42f4a3(0x535)in _0xab9a9c[_0x42f4a3(0x6eb)][_0x55ee32]&&_0xab9a9c[_0x42f4a3(0x6eb)][_0x55ee32][_0x42f4a3(0x535)]==_0x42f4a3(0x9a2)){_0x1c00d6=!![];break;}}},_0xab9a9c['chunkedTransferChannels'][_0x280400]['onmessage']=_0x1b7f4c=>{var _0x2fcd75=_0x586347;if(_0x1b7f4c[_0x2fcd75(0x67b)])try{var _0x5c8c46=JSON[_0x2fcd75(0xa4b)](_0x1b7f4c[_0x2fcd75(0x67b)]);if(_0x5c8c46['kf'])_0xab9a9c[_0x2fcd75(0xbc9)][_0x2fcd75(0x686)]?(_0xab9a9c['chunkedRecorder'][_0x2fcd75(0x686)][_0x2fcd75(0x4c6)](JSON[_0x2fcd75(0x6d1)]({'kf':!![]})),warnlog(_0x2fcd75(0x6f8))):errorlog(_0x2fcd75(0x724));else _0x5c8c46[_0x2fcd75(0x3c5)]===_0x2fcd75(0x50c)&&_0xab9a9c[_0x2fcd75(0xbc9)]&&typeof _0xab9a9c[_0x2fcd75(0xbc9)][_0x2fcd75(0x5ae)]===_0x2fcd75(0x32c)&&_0xab9a9c[_0x2fcd75(0xbc9)]['handleNack'](_0x280400,_0x5c8c46);}catch(_0x359c0e){}},_0xab9a9c['hostedTransfers'][_0x586347(0x5e7)](_0xab9a9c['chunkedTransferChannels'][_0x280400]);}}await _0xab9a9c['chunkedRecorder'][_0x586347(0x1f8)]();};function _0x1dacdf(_0x5206b4,_0x4f672d=0x2){var _0x1bd653=_0xf92ab0;let _0x19756b=Number[_0x1bd653(0x9e8)](_0x5206b4)?Math[_0x1bd653(0x502)](_0x5206b4):_0x4f672d;_0x19756b<_0x4f672d&&(_0x19756b=_0x4f672d);const _0x153acd=_0x19756b%_0x4f672d;return _0x153acd!==0x0&&(_0x19756b-=_0x153acd),_0x19756b<_0x4f672d&&(_0x19756b=_0x4f672d),_0x19756b;}async function _0x3d17e3(_0x7ea918=0x500,_0x4c1b88=0x2d0,_0x13c271=0x1e){var _0x5e3d8f=_0xf92ab0,_0x1a4d54=[_0x5e3d8f(0x330),_0x5e3d8f(0xc22),_0x5e3d8f(0x8ce),'avc1.42001E'],_0x577ecf=[_0x5e3d8f(0xc33),_0x5e3d8f(0xb1e)],_0x9c80c6=[];if(_0xab9a9c[_0x5e3d8f(0xc85)]){var _0x2854c0=[],_0x5755a6=_0x5e3d8f(0x945);for(var _0x3824d3 of _0x1a4d54){for(var _0x3b2b12 of _0x577ecf){_0x2854c0[_0x5e3d8f(0x5e7)]({'codec':_0x3824d3,'alpha':_0x5755a6,'hardwareAcceleration':_0x3b2b12,'width':_0x7ea918,'height':_0x4c1b88,'bitrate':0x1e8480,'bitrateMode':_0x5e3d8f(0x9cd),'framerate':_0x13c271,'latencyMode':_0x5e3d8f(0x519)});}}for(var _0x307c7c=0x0;_0x307c7c<_0x2854c0[_0x5e3d8f(0xade)];_0x307c7c++){var _0x1717cf=await VideoEncoder['isConfigSupported'](_0x2854c0[_0x307c7c]);_0x1717cf&&_0x1717cf[_0x5e3d8f(0x5d8)]&&_0x9c80c6[_0x5e3d8f(0x5e7)](_0x1717cf);}!_0x9c80c6[_0x5e3d8f(0xade)]&&(!_0xab9a9c[_0x5e3d8f(0xca4)]&&warnUser(_0x5e3d8f(0xa7c),0x1770));}if(!_0x9c80c6[_0x5e3d8f(0xade)]){var _0x2854c0=[],_0x5755a6=_0x5e3d8f(0xa41);for(var _0x3824d3 of _0x1a4d54){for(var _0x3b2b12 of _0x577ecf){_0x2854c0['push']({'codec':_0x3824d3,'alpha':_0x5755a6,'hardwareAcceleration':_0x3b2b12,'width':_0x7ea918,'height':_0x4c1b88,'bitrate':0x1e8480,'bitrateMode':_0x5e3d8f(0x9cd),'framerate':_0x13c271,'latencyMode':'realtime'});}}for(var _0x307c7c=0x0;_0x307c7c<_0x2854c0[_0x5e3d8f(0xade)];_0x307c7c++){var _0x1717cf=await VideoEncoder['isConfigSupported'](_0x2854c0[_0x307c7c]);_0x1717cf&&_0x1717cf['supported']&&_0x9c80c6[_0x5e3d8f(0x5e7)](_0x1717cf);}}return _0x9c80c6;}_0xab9a9c[_0xf92ab0(0x21b)]=async function(_0x1b1a5c=null){var _0x37c0b4=_0xf92ab0;if(_0x1b1a5c&&!_0xab9a9c[_0x37c0b4(0x84f)][_0x1b1a5c]['allowChunked'])return;!_0xab9a9c[_0x37c0b4(0x6b1)]&&_0xab9a9c['chunkedRecorder']&&_0xab9a9c[_0x37c0b4(0xbc9)]['configVideo']&&await _0xab9a9c[_0x37c0b4(0xba0)](_0xab9a9c[_0x37c0b4(0x287)][_0x37c0b4(0x2b0)]);!_0xab9a9c[_0x37c0b4(0xb1a)]&&_0xab9a9c['chunkedRecorder']&&_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0xa91)]&&await _0xab9a9c['webCodecAudio'](_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0xa91)]);if(_0x1b1a5c){if(_0x1b1a5c in _0xab9a9c['chunkedTransferChannels']){warnlog(_0x37c0b4(0x7a0));return;}else _0xab9a9c['chunkedTransferChannels'][_0x1b1a5c]=null;}if(!_0xab9a9c[_0x37c0b4(0xbc9)]){var _0x20e6d9=_0xab9a9c[_0x37c0b4(0x27d)](),_0x30047d=_0xab9a9c[_0x37c0b4(0x86f)],_0x421808=null;_0xab9a9c[_0x37c0b4(0x504)]&&_0xab9a9c[_0x37c0b4(0x504)]<_0x30047d&&(_0x30047d=_0xab9a9c[_0x37c0b4(0x504)]);var _0x8f66b4={'codec':_0x37c0b4(0xc22),'width':0x780,'height':0x438,'bitrate':parseInt(_0x30047d*0x3e8),'frameRate':0x1e,'latencyMode':_0x37c0b4(0x519)},_0x269893=_0x20e6d9['getVideoTracks']();if(_0x269893[_0x37c0b4(0xade)]){var _0x5a1148=_0x269893[0x0][_0x37c0b4(0x498)]();_0x5a1148['width']&&(_0x8f66b4['width']=_0x5a1148[_0x37c0b4(0x28e)]),_0x5a1148[_0x37c0b4(0x43f)]&&(_0x8f66b4[_0x37c0b4(0x43f)]=_0x5a1148['height']),_0x5a1148['frameRate']&&(_0x8f66b4['frameRate']=_0x5a1148[_0x37c0b4(0x453)]);}else _0x8f66b4=![];if(_0x30047d<0x259){var _0x273d65=_0x8f66b4[_0x37c0b4(0x28e)]*_0x8f66b4[_0x37c0b4(0x43f)]/(0x280*0x168);if(_0x273d65>=0x2)_0x8f66b4[_0x37c0b4(0x28e)]=parseInt(_0x8f66b4[_0x37c0b4(0x28e)]/0x2),_0x8f66b4[_0x37c0b4(0x43f)]=parseInt(_0x8f66b4[_0x37c0b4(0x43f)]/0x2);else _0x273d65>=1.5&&(_0x8f66b4[_0x37c0b4(0x28e)]=parseInt(_0x8f66b4[_0x37c0b4(0x28e)]/1.5),_0x8f66b4[_0x37c0b4(0x43f)]=parseInt(_0x8f66b4[_0x37c0b4(0x43f)]/1.5));}_0x8f66b4[_0x37c0b4(0x28e)]=_0x1dacdf(_0x8f66b4['width'],0x2),_0x8f66b4[_0x37c0b4(0x43f)]=_0x1dacdf(_0x8f66b4[_0x37c0b4(0x43f)],0x2),_0x8f66b4[_0x37c0b4(0x453)]=Math[_0x37c0b4(0x642)](0x1,Math['round'](_0x8f66b4['frameRate']||0x1e));try{var _0xce00c4=await _0x3d17e3(_0x8f66b4['width'],_0x8f66b4[_0x37c0b4(0x43f)],_0x8f66b4[_0x37c0b4(0x453)]);_0xce00c4&&_0xce00c4[_0x37c0b4(0xade)]&&(_0x8f66b4[_0x37c0b4(0x44b)]=_0xce00c4[0x0][_0x37c0b4(0x8da)]['codec'],_0x8f66b4['alpha']=_0xce00c4[0x0][_0x37c0b4(0x8da)][_0x37c0b4(0xc85)]),log(_0xce00c4);}catch(_0x1f6489){errorlog(_0x1f6489);}warnlog(_0x8f66b4);_0x8f66b4[_0x37c0b4(0x28e)]==_0x8f66b4[_0x37c0b4(0x43f)]&&(_0x8f66b4[_0x37c0b4(0x28e)]=0x280,_0x8f66b4[_0x37c0b4(0x43f)]=0x280);var _0x253fe3={'codec':'opus','numberOfChannels':0x2,'channels':0x2,'sampleRate':0xbb80,'bitrate':0xfa00,'tuning':{'bitrate':0xfa00}};if(_0x30047d>0xbb8)_0x253fe3={'codec':_0x37c0b4(0xb94),'numberOfChannels':0x2,'channels':0x2,'sampleRate':0xbb80,'tuning':{'bitrate':0x1f400}};else _0x30047d<0x259&&(_0x253fe3={'codec':'opus','numberOfChannels':0x2,'channels':0x2,'sampleRate':0xbb80,'tuning':{'bitrate':0x7d00}});_0xab9a9c[_0x37c0b4(0xcc0)]&&(_0x253fe3={'codec':_0x37c0b4(0xcc0),'numberOfChannels':0x2,'channels':0x2,'sampleRate':0xbb80});!_0x20e6d9[_0x37c0b4(0x568)]()[_0x37c0b4(0xade)]&&(_0x253fe3=![]);if(!_0x253fe3&&!_0x8f66b4){warnlog(_0x37c0b4(0xbf0));return;}warnlog('session.chunkedRecorder\x20set'),_0xab9a9c[_0x37c0b4(0xbc9)]={},_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0x1ff)]=!![],_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0x955)]=_0x8f66b4||![],_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0xa91)]=_0x253fe3||![],_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0xa78)]=[],_0xab9a9c['stats'][_0x37c0b4(0x590)]=_0xab9a9c[_0x37c0b4(0x86f)];function _0x10ca66(){var _0x51e71d=_0x37c0b4;const _0x445f41='room123';let _0x3d044f=![];var _0x587bd0=new WebSocket(_0x51e71d(0x5f8)+_0x445f41+'/publisher');_0x587bd0['timer']=null,_0x587bd0[_0x51e71d(0x752)]=_0x51e71d(0x7e4),_0x587bd0['onopen']=()=>{var _0x556c8a=_0x51e71d;console[_0x556c8a(0x440)](_0x556c8a(0x3e4));if(_0xab9a9c['chunkedAudioEnabled']&&_0xab9a9c[_0x556c8a(0x6b1)]){let _0x28c4fa={'timestamp':Date[_0x556c8a(0x30e)](),'type':_0x556c8a(0xb9b),'realTimeVideo':_0xab9a9c[_0x556c8a(0x287)][_0x556c8a(0x2b0)][_0x556c8a(0xac7)]||0x0,'realTimeAudio':_0xab9a9c[_0x556c8a(0x287)][_0x556c8a(0x77a)][_0x556c8a(0xac7)]||0x0,'size':0x5af3107a3fff,'configVideo':_0xab9a9c[_0x556c8a(0xbc9)][_0x556c8a(0x955)],'configAudio':_0xab9a9c[_0x556c8a(0xbc9)][_0x556c8a(0xa91)],'recordType':_0xab9a9c['chunked'],'filename':_0x17454f+_0x556c8a(0x3f1),'id':_0x17454f};log(_0x28c4fa),_0x587bd0[_0x556c8a(0x865)](_0x28c4fa),_0x3d044f=!![];}else{if(_0xab9a9c['chunkedAudioEnabled']){let _0x368f69={'timestamp':Date[_0x556c8a(0x30e)](),'type':_0x556c8a(0xb9b),'realTimeAudio':_0xab9a9c['stats'][_0x556c8a(0x77a)][_0x556c8a(0xac7)]||0x0,'size':0x5af3107a3fff,'configAudio':_0xab9a9c[_0x556c8a(0xbc9)][_0x556c8a(0xa91)],'recordType':_0xab9a9c[_0x556c8a(0x86f)],'filename':_0x17454f+_0x556c8a(0x3f1),'id':_0x17454f};log(_0x368f69),_0x587bd0[_0x556c8a(0x865)](_0x368f69),_0x3d044f=!![];}else{if(_0xab9a9c[_0x556c8a(0x6b1)]){let _0x4d75e5={'timestamp':Date[_0x556c8a(0x30e)](),'type':_0x556c8a(0xb9b),'realTimeVideo':_0xab9a9c[_0x556c8a(0x287)][_0x556c8a(0x2b0)][_0x556c8a(0xac7)]||0x0,'size':0x5af3107a3fff,'configVideo':_0xab9a9c[_0x556c8a(0xbc9)][_0x556c8a(0x955)],'recordType':_0xab9a9c[_0x556c8a(0x86f)],'filename':_0x17454f+_0x556c8a(0x3f1),'id':_0x17454f};log(_0x4d75e5),_0x587bd0[_0x556c8a(0x865)](_0x4d75e5),_0x3d044f=!![];}}}console[_0x556c8a(0x440)](_0x556c8a(0x90c)),_0xab9a9c['chunkedRecorder']&&_0xab9a9c[_0x556c8a(0xbc9)][_0x556c8a(0x1f8)]&&_0xab9a9c['chunkedRecorder'][_0x556c8a(0x1f8)](),_0x3d044f&&_0xf33db6();},_0x587bd0[_0x51e71d(0x776)]=function(_0xb106f0){var _0x502a67=_0x51e71d;if(!_0x3d044f)return;if(Array[_0x502a67(0x5b1)](_0xb106f0))_0x587bd0[_0x502a67(0x865)](_0xb106f0);else{if(typeof _0xb106f0===_0x502a67(0x537))_0x587bd0[_0x502a67(0x828)](_0xb106f0);else return _0x502a67(0x928);}},_0x587bd0['sendHeader']=function(_0x11389e){var _0x41721e=_0x51e71d;try{const _0x2ccd7e=JSON[_0x41721e(0x6d1)](_0x11389e),_0x285db8=new TextEncoder()[_0x41721e(0x736)](_0x2ccd7e),_0x51357c=new Uint8Array([0x0]),_0x125e97=new Uint8Array(_0x51357c[_0x41721e(0xade)]+_0x285db8[_0x41721e(0xade)]);_0x125e97[_0x41721e(0x43d)](_0x51357c,0x0),_0x125e97[_0x41721e(0x43d)](_0x285db8,_0x51357c['length']),this['send'](_0x125e97);}catch(_0x5ac038){errorlog(_0x5ac038);}},_0x587bd0[_0x51e71d(0x828)]=function(_0x303f53){var _0x516298=_0x51e71d;try{const _0x2f964d=new Uint8Array([0x1]),_0x12c7fd=new Uint8Array(_0x2f964d[_0x516298(0xade)]+_0x303f53[_0x516298(0xc04)]);_0x12c7fd[_0x516298(0x43d)](_0x2f964d,0x0),_0x12c7fd[_0x516298(0x43d)](new Uint8Array(_0x303f53),_0x2f964d['length']),this['send'](_0x12c7fd);}catch(_0x5574ca){errorlog(_0x5574ca);}},_0x587bd0[_0x51e71d(0xb95)]=function(_0x2db825){var _0x393268=_0x51e71d;const _0x1d5a51=new Uint8Array(_0x2db825[_0x393268(0x67b)]),_0x185675=_0x1d5a51[0x0];if(_0x185675===0x3){const _0x33a2b6=new DataView(_0x1d5a51['buffer'])[_0x393268(0x6dc)](0x1,!![]),_0x4a94e8=new DataView(_0x1d5a51['buffer'])[_0x393268(0x6dc)](0x5,!![]),_0x47dd81=_0x1d5a51[0x9]===0x1;console[_0x393268(0x440)]('Total\x20viewers:\x20'+_0x33a2b6),console[_0x393268(0x440)](_0x393268(0xa10)+_0x4a94e8),_0x47dd81&&(console[_0x393268(0x440)](_0x393268(0x3d9)),_0xab9a9c[_0x393268(0xbc9)]['needKeyFrame']=!![]);}},_0x587bd0[_0x51e71d(0xa93)]=function(){const _0x779c94=new Uint8Array([0x2]);this['send'](_0x779c94);};function _0xf33db6(){var _0x2d84cd=_0x51e71d;_0x587bd0[_0x2d84cd(0x70b)]===0x1&&(_0x587bd0[_0x2d84cd(0xa93)](),clearTimeout(_0x587bd0['timer']),_0x587bd0[_0x2d84cd(0xab1)]=setTimeout(_0xf33db6,0x1388));}return _0x587bd0[_0x51e71d(0x38c)]=()=>{var _0x54dcbf=_0x51e71d;console['log'](_0x54dcbf(0x246)),_0x587bd0['timer']&&clearTimeout(_0x587bd0['timer']),_0xab9a9c['chunkedRecorder'][_0x54dcbf(0xad2)]=![];},_0x587bd0[_0x51e71d(0x636)]=_0x1f895b=>{var _0x457ddd=_0x51e71d;console[_0x457ddd(0xca1)](_0x457ddd(0x6c2),_0x1f895b);},_0x587bd0;}_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0xad2)]=![];function _0x5d2b38(){var _0x20b081=_0x37c0b4;let _0x126a1b=-0x1;for(let _0xe6002=_0xab9a9c[_0x20b081(0x2ca)]['length']-0x1;_0xe6002>=0x0;_0xe6002--){const _0x4a0e4f=_0xab9a9c[_0x20b081(0x2ca)][_0xe6002];if(Array[_0x20b081(0x5b1)](_0x4a0e4f)&&_0x4a0e4f[0x1]===_0x20b081(0xa27)){_0x126a1b=_0xe6002;break;}}if(_0x126a1b>0x0){const _0x4b746c=_0x126a1b;_0xab9a9c[_0x20b081(0x2ca)]=_0xab9a9c['chunksQueue']['slice'](_0x126a1b),console['log']('Cleared\x20'+_0x4b746c+_0x20b081(0x9ab));}else _0x126a1b===-0x1&&console[_0x20b081(0x440)]('No\x20keyframe\x20found\x20in\x20queue,\x20keeping\x20all\x20chunks');}_0xab9a9c[_0x37c0b4(0xbc9)]['sendChunks']=async function(_0x2cd3d2=_0x37c0b4(0x644)){var _0x6ea641=_0x37c0b4;if(_0x421808)return;_0x421808=!![];const _0x413c58=0x1f4;if(_0xab9a9c['chunksQueue'][_0x6ea641(0xade)]>_0x413c58){const _0x47b0f6=_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xade)];let _0x21171f=_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0x912)](-_0x413c58),_0x2a5178=_0x21171f[_0x6ea641(0x862)](_0x247c7a=>Array['isArray'](_0x247c7a)&&typeof _0x247c7a[0x1]===_0x6ea641(0x9cb));if(_0x2a5178===-0x1)console['warn'](_0x6ea641(0xc6f)),_0x21171f=[];else _0x2a5178>0x0&&(_0x21171f=_0x21171f[_0x6ea641(0x912)](_0x2a5178));const _0x5d0296=_0x47b0f6-_0x21171f[_0x6ea641(0xade)];console[_0x6ea641(0x2ea)](_0x6ea641(0x6d6)+_0x5d0296+_0x6ea641(0x3e1)),_0xab9a9c[_0x6ea641(0x2ca)]=_0x21171f;}if(_0xab9a9c[_0x6ea641(0x26d)]){!_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xad2)]&&(_0xab9a9c['chunkedRecorder'][_0x6ea641(0xad2)]=_0x10ca66());if(_0xab9a9c[_0x6ea641(0xbc9)]['wss']){if(_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xad2)][_0x6ea641(0x70b)]===0x1)while(_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xade)]){try{_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xad2)][_0x6ea641(0x776)](_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xa17)]());}catch(_0x3e1d78){break;}}else _0xab9a9c['chunksQueue']['length']>0x3e8?(console[_0x6ea641(0x440)](_0x6ea641(0xa0b)),_0x5d2b38()):console[_0x6ea641(0x440)](_0x6ea641(0x578));_0x421808=null;return;}}var _0x43bbe1=_0x2cd3d2;const _0x290eea=_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x3e7)]=_0xab9a9c['chunkedRecorder'][_0x6ea641(0x3e7)]||{};for(const _0x3376f4 in _0x290eea){(!_0xab9a9c[_0x6ea641(0x532)][_0x3376f4]||!_0xab9a9c['pcs'][_0x3376f4])&&delete _0x290eea[_0x3376f4];}const _0x2ffc04={0x0:0x0,0x1:0x0,0x2:0x0};let _0x5daac3=0x0;function _0x2be86f(_0x18f401){var _0x1876f0=_0x6ea641;if(!_0x18f401)return 0x2;if(_0x18f401[_0x1876f0(0x268)]!==![]&&_0x18f401[_0x1876f0(0x268)]!==undefined&&_0x18f401[_0x1876f0(0x268)]!==null)return 0x0;if(_0x18f401['sceneDisplay']!==null&&_0x18f401[_0x1876f0(0x71b)]!==undefined&&_0x18f401[_0x1876f0(0x71b)]!==![])return 0x0;if(_0x18f401[_0x1876f0(0x1ab)]===!![]||_0x18f401[_0x1876f0(0x8d5)]===!![])return 0x2;return 0x1;}function _0x40078c(_0xd4f314){switch(_0xd4f314){case 0x0:return 0x180000;case 0x1:return 0x100000;default:return 0xc0000;}}function _0x5bea5f(_0x384130,_0x3ecdac){return!_0x290eea[_0x384130]&&(_0x290eea[_0x384130]={'priority':_0x3ecdac,'skipped':0x0,'sent':0x0,'buffered':0x0,'stalled':![],'consecutiveHigh':0x0}),_0x290eea[_0x384130]['priority']=_0x3ecdac,_0x290eea[_0x384130];}function _0x113380(_0x153794,_0x4461e0,_0x22fce3={}){var _0xfb32ce=_0x6ea641,_0xca4252=_0xab9a9c[_0xfb32ce(0x532)][_0x153794],_0x554740=_0xab9a9c[_0xfb32ce(0x84f)][_0x153794];if(!_0xca4252||!_0x554740||_0xca4252[_0xfb32ce(0x70b)]!=='open')return![];var _0xb4a229=_0x22fce3[_0xfb32ce(0x2ad)]===!![];!_0x554740['stats']&&(_0x554740['stats']={});var _0x9542f2=_0x2be86f(_0x554740),_0x417936=_0x5bea5f(_0x153794,_0x9542f2);!_0xca4252[_0xfb32ce(0xcbd)]&&(_0xca4252[_0xfb32ce(0xcbd)]=![]);!_0xca4252[_0xfb32ce(0xc6d)]&&(_0xca4252['audioHeaderSent']=![]);!_0xca4252['detailsSent']&&(_0xca4252[_0xfb32ce(0x7c0)]=![]);if(_0x43bbe1==='delta'&&!_0xca4252['keyframeSent'])return warnlog(_0xfb32ce(0x8b6)),_0xab9a9c[_0xfb32ce(0xbc9)]&&(_0xab9a9c[_0xfb32ce(0xbc9)][_0xfb32ce(0x1ff)]=!![]),![];if(!_0xb4a229&&(_0x43bbe1===_0xfb32ce(0xa27)||_0x43bbe1==='delta'||_0x43bbe1==='video')&&!_0xca4252[_0xfb32ce(0xcbd)])return warnlog(_0xfb32ce(0x8b6)),_0xab9a9c[_0xfb32ce(0xbc9)]&&(_0xab9a9c[_0xfb32ce(0xbc9)][_0xfb32ce(0x1ff)]=!![]),![];if(!_0xb4a229&&(_0x43bbe1===_0xfb32ce(0x544)||_0x43bbe1===_0xfb32ce(0xcc0))&&!_0xca4252[_0xfb32ce(0xc6d)])return warnlog('Waiting\x20for\x20audio\x20header\x20before\x20sending\x20raw\x20audio\x20data'),![];if(!_0xca4252['detailsSent']){if(_0xab9a9c[_0xfb32ce(0xa08)])try{var _0x4da69c={..._0xab9a9c[_0xfb32ce(0xa08)]};_0x4da69c[_0xfb32ce(0x9f5)]=Date['now'](),_0xca4252['send'](JSON['stringify'](_0x4da69c)),_0xca4252[_0xfb32ce(0x7c0)]=!![];}catch(_0x442b02){return _0x417936['sendErrors']=(_0x417936['sendErrors']||0x0)+0x1,_0x417936[_0xfb32ce(0x93a)]=!![],_0xab9a9c['chunkedRecorder'][_0xfb32ce(0x1ff)]=!![],![];}else _0xca4252[_0xfb32ce(0x7c0)]=!![];}var _0x3ae9af=_0xca4252[_0xfb32ce(0xb79)]||0x0;_0x3ae9af>_0x5daac3&&(_0x5daac3=_0x3ae9af);_0x3ae9af>_0x2ffc04[_0x9542f2]&&(_0x2ffc04[_0x9542f2]=_0x3ae9af);_0x554740[_0xfb32ce(0x287)][_0xfb32ce(0xb79)]=_0x3ae9af;var _0x4d54d3=_0x43bbe1===_0xfb32ce(0xa55)||_0x43bbe1===_0xfb32ce(0x610),_0x4b4962=_0x40078c(_0x9542f2);_0x9542f2===0x0&&(_0x3ae9af>_0x4b4962*1.5?_0x417936['highPriorityPressure']=(_0x417936[_0xfb32ce(0x59e)]||0x0)+0x1:_0x417936[_0xfb32ce(0x59e)]=0x0);if(_0x4d54d3&&_0x9542f2>0x0&&_0x3ae9af>_0x4b4962)return _0x417936[_0xfb32ce(0xc77)]=(_0x417936[_0xfb32ce(0xc77)]||0x0)+0x1,_0x417936['consecutiveHigh']=(_0x417936['consecutiveHigh']||0x0)+0x1,_0x417936[_0xfb32ce(0xa75)]=_0x3ae9af,_0x417936['consecutiveHigh']>=0x4&&(_0x417936[_0xfb32ce(0x93a)]=!![],_0xab9a9c[_0xfb32ce(0xbc9)]['needKeyFrame']=!![]),![];try{_0xca4252[_0xfb32ce(0x4c6)](_0x4461e0);}catch(_0x5bf8ac){return _0x417936['sendErrors']=(_0x417936[_0xfb32ce(0x8c2)]||0x0)+0x1,_0x417936[_0xfb32ce(0x93a)]=!![],_0xab9a9c[_0xfb32ce(0xbc9)][_0xfb32ce(0x1ff)]=!![],![];}if(_0xb4a229){if(_0x43bbe1==_0xfb32ce(0xa27)||_0x43bbe1==_0xfb32ce(0xa55))_0xca4252[_0xfb32ce(0xcbd)]=!![];else(_0x43bbe1==_0xfb32ce(0x544)||_0x43bbe1==_0xfb32ce(0xcc0))&&(_0xca4252['audioHeaderSent']=!![]);}return _0x3ae9af=_0xca4252[_0xfb32ce(0xb79)]||0x0,_0x3ae9af>_0x5daac3&&(_0x5daac3=_0x3ae9af),_0x3ae9af>_0x2ffc04[_0x9542f2]&&(_0x2ffc04[_0x9542f2]=_0x3ae9af),_0x554740['stats'][_0xfb32ce(0xb79)]=_0x3ae9af,_0x417936['buffered']=_0x3ae9af,_0x417936[_0xfb32ce(0xc3e)]=(_0x417936[_0xfb32ce(0xc3e)]||0x0)+0x1,_0x417936[_0xfb32ce(0x65b)]=Date['now'](),_0x417936[_0xfb32ce(0x71f)]=Math['max'](0x0,(_0x417936['consecutiveHigh']||0x0)-0x1),_0x417936[_0xfb32ce(0x93a)]=![],!![];}while(_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xade)]){if(!Object[_0x6ea641(0x7db)](_0xab9a9c['chunkedTransferChannels'])[_0x6ea641(0xade)]){_0x5d2b38(),_0x421808=null,_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x1cf)]=_0xab9a9c['chunksQueue'][_0x6ea641(0xade)],_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xa78)]=[];return;}_0xab9a9c['stats'][_0x6ea641(0x1cf)]=_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xade)],_0x5daac3=0x0,_0x2ffc04[0x0]=0x0,_0x2ffc04[0x1]=0x0,_0x2ffc04[0x2]=0x0;var _0x12fb5b=_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xa17)]();if(Array['isArray'](_0x12fb5b)){_0x43bbe1=_0x12fb5b[0x1];const _0x1a41f7=_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xade)],_0x3bc840=_0x12fb5b,_0x4812cb=_0x12fb5b['slice']();_0x4812cb[_0x6ea641(0x5e7)](_0x1a41f7);const _0x4ccc0f=JSON[_0x6ea641(0x6d1)](_0x4812cb),_0x57e7b1=_0x43bbe1=='key'||_0x43bbe1==_0x6ea641(0xa55)||_0x43bbe1==_0x6ea641(0x544)||_0x43bbe1==_0x6ea641(0xcc0);let _0x1baa21=![],_0x4f1b4b=![],_0x3c6931=![];for(var _0x10e17b in _0xab9a9c[_0x6ea641(0x532)]){var _0x2f32db=_0xab9a9c[_0x6ea641(0x532)][_0x10e17b];if(!_0x2f32db){_0x3c6931=!![];continue;}var _0x42915f=_0xab9a9c['pcs'][_0x10e17b];if(!_0x42915f)continue;if((_0x43bbe1==_0x6ea641(0xa27)||_0x43bbe1=='delta'||_0x43bbe1==_0x6ea641(0xa55))&&!_0x42915f[_0x6ea641(0x295)])continue;if((_0x43bbe1==_0x6ea641(0x544)||_0x43bbe1==_0x6ea641(0xcc0))&&(!_0x42915f[_0x6ea641(0x8c0)]||_0x42915f['allowChunked']==0x2))continue;_0x1baa21=!![],_0x113380(_0x10e17b,_0x4ccc0f,{'metadata':!![]})&&(_0x4f1b4b=!![]);}if(!_0x4f1b4b&&(_0x1baa21||_0x3c6931)&&_0x57e7b1){_0xab9a9c['chunkedRecorder']&&(_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x1ff)]=!![]);_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xc50)](_0x3bc840),_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x1cf)]=_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xade)],_0x421808=null;return;}}else{if(_0x12fb5b[_0x6ea641(0xc04)]>0x40000){const _0x2a4c5d=_0x12fb5b,_0x59a082=_0x2a4c5d[_0x6ea641(0x912)](0x0,0x40000);let _0x50e0a5=![],_0x59d203=![],_0x52c28e=![];for(var _0x10e17b in _0xab9a9c[_0x6ea641(0x532)]){var _0x2f32db=_0xab9a9c[_0x6ea641(0x532)][_0x10e17b];if(!_0x2f32db){_0x52c28e=!![];continue;}var _0x42915f=_0xab9a9c[_0x6ea641(0x84f)][_0x10e17b];if(!_0x42915f)continue;if((_0x43bbe1==_0x6ea641(0xa27)||_0x43bbe1=='delta'||_0x43bbe1==_0x6ea641(0xa55))&&!_0x42915f[_0x6ea641(0x295)])continue;if((_0x43bbe1==_0x6ea641(0x544)||_0x43bbe1==_0x6ea641(0xcc0))&&(!_0x42915f[_0x6ea641(0x8c0)]||_0x42915f[_0x6ea641(0xae2)]==0x2))continue;_0x50e0a5=!![],_0x113380(_0x10e17b,_0x59a082)&&(_0x59d203=!![]);}const _0x2e5fee=_0x43bbe1==_0x6ea641(0xa27)||_0x43bbe1=='video'||_0x43bbe1=='audio'||_0x43bbe1=='pcm';if(!_0x59d203&&(_0x50e0a5||_0x52c28e)&&_0x2e5fee){_0xab9a9c[_0x6ea641(0xbc9)]&&(_0xab9a9c['chunkedRecorder'][_0x6ea641(0x1ff)]=!![]);_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xc50)](_0x2a4c5d),_0xab9a9c[_0x6ea641(0x287)]['chunkedInQueue']=_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xade)],_0x421808=null;return;}_0x59d203&&(_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xc50)](_0x2a4c5d[_0x6ea641(0x912)](0x40000)),_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xa78)][_0x6ea641(0x5e7)]({'bufferSize':_0x5daac3,'byteLength':0x40000,'timestamp':Date[_0x6ea641(0x30e)]()}));}else{const _0x33e49f=_0x12fb5b;let _0x3dbbad=![],_0xa305a4=![],_0x21b571=![];for(var _0x10e17b in _0xab9a9c['chunkedTransferChannels']){var _0x2f32db=_0xab9a9c['chunkedTransferChannels'][_0x10e17b];if(!_0x2f32db){_0x21b571=!![];continue;}var _0x42915f=_0xab9a9c[_0x6ea641(0x84f)][_0x10e17b];if(!_0x42915f)continue;if((_0x43bbe1==_0x6ea641(0xa27)||_0x43bbe1==_0x6ea641(0x610)||_0x43bbe1=='video')&&!_0x42915f[_0x6ea641(0x295)])continue;if((_0x43bbe1==_0x6ea641(0x544)||_0x43bbe1==_0x6ea641(0xcc0))&&(!_0x42915f['allowAudio']||_0x42915f[_0x6ea641(0xae2)]==0x2))continue;_0x3dbbad=!![],_0x113380(_0x10e17b,_0x12fb5b)&&(_0xa305a4=!![]);}const _0x49e15e=_0x43bbe1==_0x6ea641(0xa27)||_0x43bbe1==_0x6ea641(0xa55)||_0x43bbe1==_0x6ea641(0x544)||_0x43bbe1==_0x6ea641(0xcc0);if(!_0xa305a4&&(_0x3dbbad||_0x21b571)&&_0x49e15e){_0xab9a9c['chunkedRecorder']&&(_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x1ff)]=!![]);_0xab9a9c[_0x6ea641(0x2ca)][_0x6ea641(0xc50)](_0x33e49f),_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x1cf)]=_0xab9a9c['chunksQueue'][_0x6ea641(0xade)],_0x421808=null;return;}_0xa305a4&&_0xab9a9c['chunkedRecorder'][_0x6ea641(0xa78)][_0x6ea641(0x5e7)]({'bufferSize':_0x5daac3,'byteLength':_0x12fb5b[_0x6ea641(0xc04)],'timestamp':Date['now']()});}}_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xa78)]=_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xa78)][_0x6ea641(0x912)](-0x3e8);let _0x1d81e0=0x0,_0x181772=0x0,_0x1c2878=0x0;for(let _0x133d4f=_0xab9a9c['chunkedRecorder'][_0x6ea641(0xa78)]['length']-0x1;_0x133d4f>0x0;_0x133d4f--){if(_0x1c2878>_0xab9a9c['sendingBuffer']*0x2){_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xa78)][_0x6ea641(0x954)](_0x133d4f-0x1,0x1);continue;}const _0x3a6956=_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xa78)][_0x133d4f-0x1],_0x3edb4e=_0xab9a9c[_0x6ea641(0xbc9)]['chunkRates'][_0x133d4f];_0x181772+=_0x3edb4e[_0x6ea641(0x2ae)]-_0x3a6956[_0x6ea641(0x2ae)],_0x1d81e0+=_0x3edb4e[_0x6ea641(0xc04)],_0x1c2878+=_0x3edb4e[_0x6ea641(0x9f5)]-_0x3a6956['timestamp'];}let _0x34cb74=_0x1d81e0-_0x181772,_0x53d374=0x0,_0x39b2bb=0x0;if(_0x1c2878>0x0){const _0x34ad25=_0x1c2878/0x3e8;_0x34ad25>0x0&&(_0x53d374=_0x1d81e0/_0x34ad25*0x8/0x3e8,_0x39b2bb=_0x34cb74/_0x34ad25*0x8/0x3e8);}_0xab9a9c[_0x6ea641(0x287)]['chunkedBuffer']=parseInt(0x8*_0x5daac3/Math[_0x6ea641(0x642)](_0x39b2bb,0x1))+_0x6ea641(0xb31)+_0xab9a9c[_0x6ea641(0x5ce)];let _0x511fc8=_0x39b2bb>0x0?0x8*_0x5daac3/_0x39b2bb/_0xab9a9c['sendingBuffer']:0x0;_0xab9a9c['stats'][_0x6ea641(0x652)]=_0x511fc8,_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x9c6)]=parseInt(_0x53d374)+'\x20:\x20'+parseInt(_0x39b2bb);if(!_0xab9a9c[_0x6ea641(0xbc9)]['adaptation']){const _0x5e624b=Math[_0x6ea641(0x642)](0xc8,_0xab9a9c[_0x6ea641(0x86f)]*0.2),_0x3f2d88=Number[_0x6ea641(0x9e8)](parseFloat(_0xab9a9c[_0x6ea641(0x3de)]))?parseFloat(_0xab9a9c[_0x6ea641(0x3de)]):_0x5e624b,_0x1b76a4=Number[_0x6ea641(0x9e8)](parseFloat(_0xab9a9c[_0x6ea641(0x21a)]))?parseFloat(_0xab9a9c[_0x6ea641(0x21a)]):_0xab9a9c[_0x6ea641(0x86f)];_0xab9a9c['chunkedRecorder'][_0x6ea641(0x79f)]={'target':_0xab9a9c[_0x6ea641(0x86f)],'floor':_0x3f2d88,'ceiling':_0x1b76a4,'lastChange':0x0,'mode':(_0xab9a9c[_0x6ea641(0xc41)]||_0x6ea641(0x42f))[_0x6ea641(0x85b)](),'frameDropBudget':0x0,'maxFrameDrop':Number[_0x6ea641(0x9e8)](parseInt(_0xab9a9c[_0x6ea641(0x5a9)]))?Math[_0x6ea641(0x642)](0x0,parseInt(_0xab9a9c[_0x6ea641(0x5a9)])):0x6,'threshold':Number['isFinite'](parseFloat(_0xab9a9c[_0x6ea641(0x6ac)]))?parseFloat(_0xab9a9c[_0x6ea641(0x6ac)]):0x17c,'interval':Number[_0x6ea641(0x9e8)](parseInt(_0xab9a9c[_0x6ea641(0x2f2)]))?Math[_0x6ea641(0x642)](0xc8,parseInt(_0xab9a9c[_0x6ea641(0x2f2)])):0x320};}const _0x17a741=_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x79f)];_0x17a741['mode']=(_0xab9a9c['chunkadapt']||_0x17a741[_0x6ea641(0x967)]||'bitrate')['toLowerCase']();Number['isFinite'](parseFloat(_0xab9a9c['chunkadaptfloor']))&&(_0x17a741['floor']=parseFloat(_0xab9a9c[_0x6ea641(0x3de)]));Number[_0x6ea641(0x9e8)](parseFloat(_0xab9a9c['chunkadaptceil']))?_0x17a741[_0x6ea641(0x95e)]=parseFloat(_0xab9a9c[_0x6ea641(0x21a)]):_0x17a741[_0x6ea641(0x95e)]=_0xab9a9c[_0x6ea641(0x86f)];Number['isFinite'](parseInt(_0xab9a9c[_0x6ea641(0x5a9)]))&&(_0x17a741[_0x6ea641(0x3f2)]=Math['max'](0x0,parseInt(_0xab9a9c[_0x6ea641(0x5a9)])));Number[_0x6ea641(0x9e8)](parseFloat(_0xab9a9c[_0x6ea641(0x6ac)]))&&(_0x17a741[_0x6ea641(0x976)]=parseFloat(_0xab9a9c[_0x6ea641(0x6ac)]));Number[_0x6ea641(0x9e8)](parseInt(_0xab9a9c[_0x6ea641(0x2f2)]))&&(_0x17a741['interval']=Math[_0x6ea641(0x642)](0xc8,parseInt(_0xab9a9c[_0x6ea641(0x2f2)])));_0x17a741['floor']=Math['max'](0x0,Math['min'](_0x17a741[_0x6ea641(0x502)],_0x17a741[_0x6ea641(0x95e)]));const _0x1fd0dd=Date['now']();let _0x171159=_0x17a741[_0x6ea641(0x357)],_0x517100=![],_0x8824b3=![],_0x378abd=![],_0x2277ce=![],_0x2627c9=0x0;for(const _0xd25521 in _0x290eea){const _0x53041f=_0x290eea[_0xd25521];_0x53041f[_0x6ea641(0x98b)]===0x0&&(_0x53041f[_0x6ea641(0x93a)]||(_0x53041f[_0x6ea641(0x59e)]||0x0)>0x3)&&(_0x517100=!![]),_0x53041f[_0x6ea641(0x98b)]>0x0&&(_0x2627c9+=_0x53041f['skipped']||0x0);}_0x378abd=_0x2ffc04[0x0]>_0x40078c(0x0),_0x8824b3=_0x2ffc04[0x1]>_0x40078c(0x1),_0x2277ce=_0x2ffc04[0x2]>_0x40078c(0x2);const _0x24a477=_0x17a741[_0x6ea641(0x967)]||_0x6ea641(0x42f),_0x4e0a5b=_0x24a477==='bitrate'||_0x24a477===_0x6ea641(0xb3e),_0x358fe7=_0x24a477===_0x6ea641(0x1ef)||_0x24a477===_0x6ea641(0xb3e);_0x358fe7&&(_0x17a741[_0x6ea641(0x4fc)]=Math[_0x6ea641(0x642)](0x0,_0x17a741['frameDropBudget']-0.2));if(_0x517100||_0x378abd||_0x511fc8>1.25){_0x4e0a5b&&(_0x171159=Math[_0x6ea641(0x642)](_0x17a741['floor'],_0x17a741[_0x6ea641(0x357)]*0.82));if(_0x358fe7){const _0x431f5d=_0x24a477==='hybrid'?0x1:0x2;_0x17a741['frameDropBudget']=Math[_0x6ea641(0xb03)](_0x17a741[_0x6ea641(0x3f2)],_0x17a741[_0x6ea641(0x4fc)]+_0x431f5d);}}else{if(_0x8824b3||_0x511fc8>0x1)_0x4e0a5b&&(_0x171159=Math[_0x6ea641(0x642)](_0x17a741[_0x6ea641(0x502)],_0x17a741['target']*0.9)),_0x358fe7&&(_0x17a741[_0x6ea641(0x4fc)]=Math[_0x6ea641(0xb03)](_0x17a741[_0x6ea641(0x3f2)],_0x17a741[_0x6ea641(0x4fc)]+0x1));else!_0x517100&&!_0x378abd&&_0x511fc8<0.35&&_0x39b2bb>0x0&&(_0x4e0a5b&&(_0x171159=Math['min'](_0x17a741[_0x6ea641(0x95e)],_0x17a741[_0x6ea641(0x357)]*1.08)),_0x358fe7&&(_0x17a741[_0x6ea641(0x4fc)]=Math[_0x6ea641(0x642)](0x0,_0x17a741[_0x6ea641(0x4fc)]-0x1)));}_0x4e0a5b&&Math[_0x6ea641(0x76f)](_0x171159-_0x17a741[_0x6ea641(0x357)])>_0x17a741[_0x6ea641(0x357)]*0.05&&_0x1fd0dd-_0x17a741['lastChange']>_0x17a741[_0x6ea641(0xb43)]&&(_0x17a741[_0x6ea641(0x357)]=Math['max'](_0x17a741['floor'],Math[_0x6ea641(0xb03)](_0x171159,_0x17a741[_0x6ea641(0x95e)])),_0x17a741[_0x6ea641(0x82d)]=_0x1fd0dd);_0x517100&&(_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x1ff)]=!![]);_0xab9a9c[_0x6ea641(0x287)]['adjustBitrate']=Math[_0x6ea641(0x642)](_0x17a741['floor'],Math[_0x6ea641(0xb03)](_0x17a741[_0x6ea641(0x357)],_0x17a741['ceiling'])),_0x17a741[_0x6ea641(0x4fc)]=Math['max'](0x0,Math[_0x6ea641(0xb03)](_0x17a741[_0x6ea641(0x3f2)],_0x17a741[_0x6ea641(0x4fc)])),_0xab9a9c[_0x6ea641(0x287)]['chunkedFrameDropBudget']=Math[_0x6ea641(0xc45)](_0x17a741['frameDropBudget']),_0xab9a9c[_0x6ea641(0x287)]['chunkAdaptMode']=_0x17a741['mode'],_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x590)]=Math[_0x6ea641(0xc45)](_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x590)]),_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0xbbe)]=_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x590)];const _0x1ad7be=_0x39b2bb>0x0?_0x39b2bb/0x8:0x0,_0x5ba261={'high':_0x1ad7be?Math[_0x6ea641(0xc45)](_0x2ffc04[0x0]/_0x1ad7be):0x0,'medium':_0x1ad7be?Math[_0x6ea641(0xc45)](_0x2ffc04[0x1]/_0x1ad7be):0x0,'low':_0x1ad7be?Math[_0x6ea641(0xc45)](_0x2ffc04[0x2]/_0x1ad7be):0x0};_0xab9a9c[_0x6ea641(0x287)]['bufferFullnessPriority']=_0x5ba261,_0xab9a9c[_0x6ea641(0x287)]['chunkedViewerHealth']={};for(const _0x7d29fd in _0x290eea){const _0x2d9271=_0x290eea[_0x7d29fd];_0xab9a9c[_0x6ea641(0x287)]['chunkedViewerHealth'][_0x7d29fd]={'priority':_0x2d9271['priority'],'buffered':parseInt(_0x2d9271[_0x6ea641(0xa75)]||0x0),'skipped':_0x2d9271[_0x6ea641(0xc77)]||0x0,'stalled':!!_0x2d9271[_0x6ea641(0x93a)],'pressure':_0x2d9271['highPriorityPressure']||0x0};}_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x950)]=_0x2627c9;try{_0xab9a9c[_0x6ea641(0xbc9)]&&_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)]&&(_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)][_0x6ea641(0x4e0)]=='closed'&&(console[_0x6ea641(0x440)](_0x6ea641(0x9c7)),delete _0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)],_0xab9a9c['chunkedVideoEnabled']=null,await _0xab9a9c[_0x6ea641(0xba0)]()),_0xab9a9c[_0x6ea641(0xbc9)]&&_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)]&&_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)]['configure']&&_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)][_0x6ea641(0x8da)]&&(_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)]['config'][_0x6ea641(0x42f)]&&_0xab9a9c['stats']['adjustBitrate']&&(_0xab9a9c['chunkedRecorder'][_0x6ea641(0x72c)][_0x6ea641(0x8da)][_0x6ea641(0x42f)]=_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x590)]*0x3e8),_0xab9a9c[_0x6ea641(0xbc9)]['videoEncoder'][_0x6ea641(0x8da)][_0x6ea641(0x737)]&&_0xab9a9c['stats']['adjustBitrate']&&(_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)][_0x6ea641(0x8da)][_0x6ea641(0x737)][_0x6ea641(0x42f)]=_0xab9a9c[_0x6ea641(0x287)][_0x6ea641(0x590)]*0x3e8),_0xab9a9c[_0x6ea641(0xbc9)]['videoEncoder'][_0x6ea641(0x8e5)](_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)][_0x6ea641(0x8da)])),_0xab9a9c[_0x6ea641(0x287)]['adjustBitrate']=parseInt(_0xab9a9c['stats']['adjustBitrate'])),_0xab9a9c[_0x6ea641(0xbc9)]&&_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xc97)]&&(_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xc97)][_0x6ea641(0x4e0)]==_0x6ea641(0x5e8)&&(console[_0x6ea641(0x440)](_0x6ea641(0x9c7)),delete _0xab9a9c['chunkedRecorder'][_0x6ea641(0xc97)],_0xab9a9c['chunkedAudioEnabled']=null,await _0xab9a9c[_0x6ea641(0x654)]()),_0xab9a9c['chunkedRecorder']&&_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xc97)]&&_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xc97)][_0x6ea641(0x8e5)]&&_0xab9a9c['chunkedRecorder'][_0x6ea641(0xc97)]['config']&&_0xab9a9c[_0x6ea641(0xbc9)]['audioEncoder'][_0x6ea641(0x8e5)](_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xc97)][_0x6ea641(0x8da)]));}catch(_0x3779ce){errorlog(_0x3779ce);if(_0xab9a9c[_0x6ea641(0x532)])for(var _0x10e17b in _0xab9a9c[_0x6ea641(0x532)]){_0xab9a9c[_0x6ea641(0x532)][_0x10e17b]['close']();_0x10e17b in _0xab9a9c['chunkedTransferChannels']&&delete _0xab9a9c[_0x6ea641(0x532)][_0x10e17b];_0xab9a9c[_0x6ea641(0x6b1)]=null,_0xab9a9c[_0x6ea641(0xb1a)]=null;if(_0xab9a9c[_0x6ea641(0xbc9)]&&_0xab9a9c[_0x6ea641(0xbc9)]['videoEncoder']){try{_0xab9a9c[_0x6ea641(0xbc9)]['videoEncoder'][_0x6ea641(0x3ef)]();}catch(_0x16e0f3){}delete _0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0x72c)],await _0xab9a9c[_0x6ea641(0xba0)]();}if(_0xab9a9c['chunkedRecorder']&&_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xc97)])try{_0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xc97)][_0x6ea641(0x3ef)](),delete _0xab9a9c[_0x6ea641(0xbc9)][_0x6ea641(0xc97)];}catch(_0x31ea71){}setTimeout(function(_0x35ff08){var _0x157483=_0x6ea641;_0xab9a9c[_0x157483(0x21b)](_0x35ff08);},0x3e8,_0x10e17b);}_0x421808=null;return;}}_0x421808=null,_0xab9a9c[_0x6ea641(0x287)]['chunkedInQueue']=0x0;},_0xab9a9c[_0x37c0b4(0xbc9)]['configVideo']&&(_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0x99b)]=_0xab9a9c[_0x37c0b4(0xba0)](_0xab9a9c['chunkedRecorder']['configVideo'])),_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0xa91)]&&(_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0xa91)]['codec']==_0x37c0b4(0xcc0)?_0xab9a9c[_0x37c0b4(0x468)](_0x20e6d9,_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0xa91)]):_0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0x3a6)]=_0xab9a9c[_0x37c0b4(0x654)](_0xab9a9c['chunkedRecorder'][_0x37c0b4(0xa91)])),_0x20e6d9[_0x37c0b4(0x506)]=function(_0x3837a8){var _0x47655f=_0x37c0b4;warnlog(_0x47655f(0x597)),log(_0x3837a8);};}else warnlog(_0x37c0b4(0x4d8));_0xab9a9c['chunkedRecorder']['videoPromise']&&(await _0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0x99b)],delete _0xab9a9c[_0x37c0b4(0xbc9)]['videoPromise']);_0xab9a9c['chunkedRecorder'][_0x37c0b4(0x3a6)]&&(await _0xab9a9c['chunkedRecorder'][_0x37c0b4(0x3a6)],delete _0xab9a9c[_0x37c0b4(0xbc9)][_0x37c0b4(0x3a6)]);if(!_0x1b1a5c)return;var _0x17454f='chunked';if(_0x1b1a5c in _0xab9a9c[_0x37c0b4(0x84f)]){if(!_0xab9a9c['chunkedTransferChannels'][_0x1b1a5c])_0xab9a9c['chunkedTransferChannels'][_0x1b1a5c]=_0xab9a9c[_0x37c0b4(0x84f)][_0x1b1a5c][_0x37c0b4(0x6e9)](_0x17454f,{'ordered':!![]});else{errorlog(_0x37c0b4(0x89d));return;}}else{warnlog('UUID\x20does\x20not\x20exist');return;}_0xab9a9c[_0x37c0b4(0x532)][_0x1b1a5c][_0x37c0b4(0x535)]=_0x37c0b4(0x9a2),_0xab9a9c[_0x37c0b4(0x532)][_0x1b1a5c]['binaryType']='arraybuffer',_0xab9a9c[_0x37c0b4(0x532)][_0x1b1a5c][_0x37c0b4(0xc96)]=![],_0xab9a9c['chunkedTransferChannels'][_0x1b1a5c][_0x37c0b4(0x97d)]=()=>{var _0x149dcc=_0x37c0b4;log(_0x149dcc(0xc7e));if(_0xab9a9c[_0x149dcc(0xb1a)]&&_0xab9a9c[_0x149dcc(0x6b1)]&&_0xab9a9c['pcs'][_0x1b1a5c][_0x149dcc(0x8c0)]&&!(_0xab9a9c[_0x149dcc(0x84f)][_0x1b1a5c][_0x149dcc(0xae2)]==0x2)&&_0xab9a9c['pcs'][_0x1b1a5c][_0x149dcc(0x295)]){let _0x57e7e7={'timestamp':Date['now'](),'type':_0x149dcc(0xb9b),'realTimeVideo':_0xab9a9c[_0x149dcc(0x287)][_0x149dcc(0x2b0)][_0x149dcc(0xac7)]||0x0,'realTimeAudio':_0xab9a9c[_0x149dcc(0x287)][_0x149dcc(0x77a)][_0x149dcc(0xac7)]||0x0,'size':0x5af3107a3fff,'configVideo':_0xab9a9c['chunkedRecorder'][_0x149dcc(0x955)],'configAudio':_0xab9a9c['chunkedRecorder'][_0x149dcc(0xa91)],'recordType':_0xab9a9c[_0x149dcc(0x86f)],'filename':_0x17454f+'.webm','id':_0x17454f};log(_0x57e7e7),_0xab9a9c['chunkedTransferChannels'][_0x1b1a5c]['send'](JSON['stringify'](_0x57e7e7));}else{if(_0xab9a9c[_0x149dcc(0xb1a)]&&_0xab9a9c[_0x149dcc(0x84f)][_0x1b1a5c]['allowAudio']&&!(_0xab9a9c[_0x149dcc(0x84f)][_0x1b1a5c][_0x149dcc(0xae2)]==0x2)){let _0x11b17d={'timestamp':Date[_0x149dcc(0x30e)](),'type':_0x149dcc(0xb9b),'realTimeAudio':_0xab9a9c[_0x149dcc(0x287)][_0x149dcc(0x77a)]['realTime']||0x0,'size':0x5af3107a3fff,'configAudio':_0xab9a9c['chunkedRecorder'][_0x149dcc(0xa91)],'recordType':_0xab9a9c[_0x149dcc(0x86f)],'filename':_0x17454f+'.webm','id':_0x17454f};log(_0x11b17d),_0xab9a9c[_0x149dcc(0x532)][_0x1b1a5c][_0x149dcc(0x4c6)](JSON['stringify'](_0x11b17d));}else{if(_0xab9a9c['chunkedVideoEnabled']&&_0xab9a9c[_0x149dcc(0x84f)][_0x1b1a5c][_0x149dcc(0x295)]){let _0x26b8d9={'timestamp':Date['now'](),'type':_0x149dcc(0xb9b),'realTimeVideo':_0xab9a9c[_0x149dcc(0x287)][_0x149dcc(0x2b0)][_0x149dcc(0xac7)]||0x0,'size':0x5af3107a3fff,'configVideo':_0xab9a9c['chunkedRecorder'][_0x149dcc(0x955)],'recordType':_0xab9a9c[_0x149dcc(0x86f)],'filename':_0x17454f+_0x149dcc(0x3f1),'id':_0x17454f};log(_0x26b8d9),_0xab9a9c['chunkedTransferChannels'][_0x1b1a5c][_0x149dcc(0x4c6)](JSON[_0x149dcc(0x6d1)](_0x26b8d9));}}}},_0xab9a9c[_0x37c0b4(0x532)][_0x1b1a5c][_0x37c0b4(0x38c)]=()=>{var _0x442005=_0x37c0b4;try{var _0x5ac3fe=_0xab9a9c[_0x442005(0x6eb)]['indexOf'](_0xab9a9c['chunkedTransferChannels'][_0x1b1a5c]);_0x5ac3fe>-0x1&&_0xab9a9c[_0x442005(0x6eb)][_0x442005(0x954)](_0x5ac3fe,0x1);}catch(_0x97ecef){errorlog(_0x97ecef);}log(_0x442005(0x439)),_0xab9a9c[_0x442005(0x532)][_0x1b1a5c]=null,delete _0xab9a9c[_0x442005(0x532)][_0x1b1a5c];var _0x23eb57=!![];for(var _0x31047=0x0;_0x31047<_0xab9a9c[_0x442005(0x6eb)][_0x442005(0xade)];_0x31047++){if(_0x442005(0x535)in _0xab9a9c[_0x442005(0x6eb)][_0x31047]&&_0xab9a9c[_0x442005(0x6eb)][_0x31047][_0x442005(0x535)]==_0x442005(0x9a2)){_0x23eb57=![];break;}}if(_0x23eb57){warnlog(_0x442005(0x562));try{_0xab9a9c[_0x442005(0xbc9)][_0x442005(0x638)]();}catch(_0x2f371e){}_0xab9a9c[_0x442005(0xbc9)]=![];}},_0xab9a9c['chunkedTransferChannels'][_0x1b1a5c][_0x37c0b4(0xb95)]=_0x1bf91a=>{var _0x121a68=_0x37c0b4;if(_0x1bf91a['data'])try{var _0x174f0d=JSON[_0x121a68(0xa4b)](_0x1bf91a[_0x121a68(0x67b)]);_0x174f0d['kf']&&(warnlog(_0x121a68(0x968)),_0xab9a9c[_0x121a68(0xbc9)][_0x121a68(0x1ff)]=!![]);}catch(_0x699ee9){}},_0xab9a9c[_0x37c0b4(0x6eb)][_0x37c0b4(0x5e7)](_0xab9a9c[_0x37c0b4(0x532)][_0x1b1a5c]);},_0xab9a9c[_0xf92ab0(0x6a7)]=async function(_0x472d3e,_0x6da7ca,_0x3b55a4){var _0x25cc65=_0xf92ab0;log(_0x25cc65(0x1c6));var _0x126fef=_0x3b55a4;_0x126fef[_0x25cc65(0x752)]='arraybuffer';var _0x5e2c8d='',_0x4a5a3f=0x0,_0x2c0297=![],_0x4216a6=![],_0x1f707d=0x0,_0x50cc2c={};_0x126fef[_0x25cc65(0x97d)]=_0x460b6d=>{var _0x801c20=_0x25cc65;log(_0x801c20(0x38b));},_0x126fef[_0x25cc65(0xb95)]=_0x30a96e=>{var _0x3a9022=_0x25cc65;if(!_0x2c0297)try{_0x2c0297=JSON['parse'](_0x30a96e[_0x3a9022(0x67b)]);if(_0x2c0297['type']==_0x3a9022(0x75a)){var {readable:_0xb8a165,writable:_0x9b6f1}=new TransformStream({'transform':(_0x3d585d,_0x9f8935)=>_0x3d585d[_0x3a9022(0xb39)]()[_0x3a9022(0x998)](_0x5a9c9e=>_0x9f8935[_0x3a9022(0x880)](new Uint8Array(_0x5a9c9e)))});_0x50cc2c[_0x3a9022(0xa64)]=_0x9b6f1['getWriter'](),_0xb8a165[_0x3a9022(0x621)](streamSaver[_0x3a9022(0x3d0)](_0x2c0297[_0x3a9022(0x3d2)]));for(var _0x1339d1=0x0;_0x1339d1{_0x1f707d<=0x0&&(_0x50cc2c['writer']&&setTimeout(function(_0x569f49,_0x31588a){var _0x269043=_0x2b9f;_0x31588a<=0x0?(_0x569f49[_0x269043(0x3ef)](),_0x569f49=null):setTimeout(function(_0x30bd53,_0x97c836){_0x30bd53['close'](),_0x30bd53=null;},0x1388,_0x569f49);},0x3e8,_0x50cc2c['writer'],_0x1f707d));_0x126fef=null;return;};return;};async function _0x379380(_0x167d05,_0x58da87=![]){var _0x1a5dc7=_0xf92ab0;try{_0x167d05[_0x1a5dc7(0xbba)][_0x1a5dc7(0x4ba)](_0x167d05['queue'][_0x1a5dc7(0xa17)]());}catch(_0x318a61){errorlog(_0x318a61);}if(_0x167d05[_0x1a5dc7(0x699)]===null&&!_0x58da87)return;_0x167d05['nextQueue']=setTimeout(function(_0x45146a){_0x379380(_0x45146a);},0x21,_0x167d05);}return _0xab9a9c['recieveChunkedStream']=async function(_0xedeebf,_0x5a35a3){var _0x266373=_0xf92ab0;log(_0x266373(0x1c6));if(!_0xab9a9c['rpcs'][_0xedeebf]){errorlog(_0x266373(0x4b7));return;}!_0xab9a9c[_0x266373(0x404)][_0xedeebf]['chunkedChannels']?_0xab9a9c['rpcs'][_0xedeebf][_0x266373(0xa6a)]=[]:_0xab9a9c[_0x266373(0x404)][_0xedeebf]['chunkedChannels'][_0x266373(0x306)](_0x247a04=>{var _0x34af56=_0x266373;_0x247a04['channel']&&_0x247a04['channel'][_0x34af56(0x3ef)]();});var _0x2be207='',_0x592a18=0x0,_0x157435=![],_0x6af049=![],_0x2fcb80={};_0x2fcb80['channel']=_0x5a35a3,_0xab9a9c[_0x266373(0x404)][_0xedeebf][_0x266373(0xa6a)]['push'](_0x2fcb80),_0x2fcb80[_0x266373(0xc8d)][_0x266373(0x752)]=_0x266373(0x7e4),_0x2fcb80[_0x266373(0xc8d)]['onopen']=_0x2e3ea0=>{var _0x2996df=_0x266373;log(_0x2996df(0x38b));},_0x2fcb80['channel'][_0x266373(0x38c)]=async function(_0xc04736){var _0xa9fa88=_0x266373;if(_0x2fcb80&&_0x2fcb80[_0xa9fa88(0x61c)]){if(_0x2fcb80&&_0x2fcb80[_0xa9fa88(0x7eb)][_0xa9fa88(0x423)]){await delay(0x3e8);try{await _0x2fcb80[_0xa9fa88(0x7eb)][_0xa9fa88(0x423)]();}catch(_0x44addc){}}}_0xab9a9c[_0xa9fa88(0x404)][_0xedeebf]&&(delete _0xab9a9c['rpcs'][_0xedeebf][_0xa9fa88(0x287)][_0xa9fa88(0x3bb)],delete _0xab9a9c['rpcs'][_0xedeebf][_0xa9fa88(0x287)][_0xa9fa88(0x340)]);return;};async function _0xfc385d(){var _0x15d38b=_0x266373,_0x10eabc=await window['showSaveFilePicker']({'startIn':_0x15d38b(0x785),'suggestedName':_0x15d38b(0xb7d),'types':[{'description':_0x15d38b(0x3a1),'accept':{'video/webm':[_0x15d38b(0x3f1)]}}]}),_0x53c913=await _0x10eabc['createWritable']();return _0x2fcb80[_0x15d38b(0x7bc)][_0x15d38b(0x787)]=_0x53c913,_0x2fcb80[_0x15d38b(0x61c)]=new WebMWriter(_0x2fcb80[_0x15d38b(0x7bc)]),_0x2fcb80[_0x15d38b(0x7eb)][_0x15d38b(0x423)]=async function(_0xb9edfb=![]){var _0xf6059b=_0x15d38b;_0xb9edfb?(_0x2fcb80['writer_config'][_0xf6059b(0x787)][_0xf6059b(0x3ef)](),_0x2fcb80[_0xf6059b(0x7eb)][_0xf6059b(0x423)]=![],clearInterval(_0x2fcb80[_0xf6059b(0x26b)]),_0x2fcb80[_0xf6059b(0x26b)]=null,await _0x2fcb80[_0xf6059b(0x61c)][_0xf6059b(0x5fa)]()):(_0x2fcb80[_0xf6059b(0x7eb)][_0xf6059b(0x423)]=![],clearInterval(_0x2fcb80['updateTime']),_0x2fcb80[_0xf6059b(0x26b)]=null,await _0x2fcb80['videoWriter'][_0xf6059b(0x5fa)](),_0x2fcb80[_0xf6059b(0x7bc)][_0xf6059b(0x787)][_0xf6059b(0x3ef)]());},_0x2fcb80[_0x15d38b(0x61c)];}const _0x5d7731=0x3,_0x3a3f26=0xb4;function _0x2fc170(){var _0x3f0253=_0x266373;return!_0x2fcb80[_0x3f0253(0x5f3)]&&(_0x2fcb80[_0x3f0253(0x5f3)]={'frames':new Map(),'order':[],'current':null,'pendingResend':null,'legacyFrameId':0x0,'legacyMode':![],'stats':{'fecRepairs':0x0,'nacksSent':0x0,'framesDropped':0x0}}),_0x2fcb80['chunkReliability'];}function _0xf8d94(_0x5386e9){var _0x194178=_0x266373;const _0x26f4e3={'timestamp':_0x5386e9[0x0],'type':_0x5386e9[0x1],'extra':null,'queueDepth':null};for(let _0x1f19c6=0x2;_0x1f19c6<_0x5386e9['length'];_0x1f19c6++){const _0x73b763=_0x5386e9[_0x1f19c6];if(_0x73b763&&typeof _0x73b763==='object'&&!Array[_0x194178(0x5b1)](_0x73b763))_0x26f4e3['extra']=_0x73b763;else typeof _0x73b763===_0x194178(0xab6)&&_0x26f4e3[_0x194178(0xacd)]===null&&(_0x26f4e3[_0x194178(0xacd)]=_0x73b763);}return _0x26f4e3;}function _0x7b525b(_0xb1f0d6){var _0x3e02d9=_0x266373;const _0x2b015b=_0x2fc170();return _0x2b015b['frames'][_0x3e02d9(0x666)](_0xb1f0d6>>>0x0)||null;}function _0x452a7e(_0x2ce35a){var _0xe9441a=_0x266373;const _0x1c4502=_0x2fc170(),_0x22bfab=_0x2ce35a['extra']||{};let _0x8fe6ec;typeof _0x22bfab[_0xe9441a(0x9d7)]===_0xe9441a(0xab6)&&Number[_0xe9441a(0x9e8)](_0x22bfab['frameId'])?_0x8fe6ec=_0x22bfab[_0xe9441a(0x9d7)]>>>0x0:(_0x1c4502[_0xe9441a(0x259)]+=0x1,_0x8fe6ec=_0x1c4502[_0xe9441a(0x259)]>>>0x0);const _0x3f2350=Array[_0xe9441a(0x5b1)](_0x22bfab['descriptors'])&&_0x22bfab[_0xe9441a(0xbcd)][_0xe9441a(0xade)]?_0x22bfab[_0xe9441a(0xbcd)]:[{'index':0x0,'size':_0x22bfab['dataBytes']||0x0,'group':0x0}],_0xf0396=Array[_0xe9441a(0x5b1)](_0x22bfab[_0xe9441a(0x53d)])?_0x22bfab[_0xe9441a(0x53d)]:[],_0x3c7502={'frameId':_0x8fe6ec,'timestamp':_0x2ce35a[_0xe9441a(0x9f5)],'type':_0x2ce35a[_0xe9441a(0x3c5)],'media':_0x22bfab[_0xe9441a(0x60b)]||(_0x2ce35a['type']==='audio'||_0x2ce35a['type']===_0xe9441a(0xcc0)?_0xe9441a(0x544):_0xe9441a(0xa55)),'descriptors':_0x3f2350,'parityDescriptors':_0xf0396,'data':new Array(_0x3f2350[_0xe9441a(0xade)])['fill'](null),'parity':new Array(_0xf0396['length'])[_0xe9441a(0xa80)](null),'nextDataIndex':0x0,'nextParityIndex':0x0,'queueDepth':_0x2ce35a[_0xe9441a(0xacd)],'nackAttempts':{},'complete':![],'dropped':![],'assembled':null,'createdAt':Date['now'](),'nackEnabled':!!_0x22bfab[_0xe9441a(0x50c)]};return _0x1c4502[_0xe9441a(0x720)][_0xe9441a(0x43d)](_0x8fe6ec,_0x3c7502),_0x1c4502[_0xe9441a(0x9ae)][_0xe9441a(0x5e7)](_0x8fe6ec),_0x1c4502[_0xe9441a(0x68e)]=_0x3c7502,_0x1c4502[_0xe9441a(0x8db)]=null,_0x1c4502[_0xe9441a(0x598)]=![],_0x3c7502;}function _0x4b36bb(_0x145f02,_0x365f73,_0x36ebcf=![]){var _0x5c6ef3=_0x266373;if(!_0x145f02||!_0x145f02[_0x5c6ef3(0x983)])return;if(!_0x145f02)return;const _0x3d4f4a=(_0x36ebcf?'p':'d')+':'+_0x365f73,_0x3b751a=_0x145f02[_0x5c6ef3(0xcc5)][_0x3d4f4a]||0x0;if(_0x3b751a>=_0x5d7731)return;_0x145f02[_0x5c6ef3(0xcc5)][_0x3d4f4a]=_0x3b751a+0x1,setTimeout(()=>{var _0x39b628=_0x5c6ef3;try{_0x2fcb80['channel']['send'](JSON[_0x39b628(0x6d1)]({'type':_0x39b628(0x50c),'frameId':_0x145f02[_0x39b628(0x9d7)],'chunkIndex':_0x365f73,'parity':_0x36ebcf}));const _0x1d09c2=_0x2fc170();_0x1d09c2[_0x39b628(0x287)][_0x39b628(0x6a5)]+=0x1;if(_0xab9a9c[_0x39b628(0x404)][_0x2fcb80[_0x39b628(0xab9)]]&&_0xab9a9c['rpcs'][_0x2fcb80[_0x39b628(0xab9)]]['stats']){const _0x544ad0=_0xab9a9c[_0x39b628(0x404)][_0x2fcb80[_0x39b628(0xab9)]]['stats'][_0x39b628(0x3bb)]=_0xab9a9c[_0x39b628(0x404)][_0x2fcb80[_0x39b628(0xab9)]][_0x39b628(0x287)][_0x39b628(0x3bb)]||{};_0x544ad0[_0x39b628(0x3e6)]=(_0x544ad0[_0x39b628(0x3e6)]||0x0)+0x1;}}catch(_0x3761c4){errorlog(_0x3761c4);}},_0x3a3f26);}function _0x58caad(_0x48216a){var _0x30c837=_0x266373;const _0x4f9de8=_0x48216a[_0x30c837(0x24b)]((_0x244e92,_0x44b0f4)=>_0x244e92+(_0x44b0f4?_0x44b0f4[_0x30c837(0xc04)]:0x0),0x0),_0x57805b=new Uint8Array(_0x4f9de8);let _0x424ccb=0x0;for(const _0x1f98f9 of _0x48216a){if(!_0x1f98f9)continue;_0x57805b[_0x30c837(0x43d)](_0x1f98f9,_0x424ccb),_0x424ccb+=_0x1f98f9[_0x30c837(0xc04)];}return _0x57805b;}function _0x59efc5(_0xf38e3b,_0xde187e){var _0xc07c2c=_0x266373;const _0x3e1b17=_0xf38e3b[_0xc07c2c(0x53d)][_0xde187e];if(!_0x3e1b17)return;const _0x59e5f0=_0x3e1b17[_0xc07c2c(0x51d)]||0x0,_0x392078=_0x3e1b17['groupSize']||_0xf38e3b['descriptors'][_0xc07c2c(0xade)];let _0x4836f9=-0x1;for(let _0x49df49=0x0;_0x49df49<_0x392078;_0x49df49++){const _0x21a14b=_0x59e5f0+_0x49df49;if(!_0xf38e3b[_0xc07c2c(0x67b)][_0x21a14b]){if(_0x4836f9!==-0x1)return;_0x4836f9=_0x21a14b;}}if(_0x4836f9===-0x1)return;const _0x2b20c3=_0xf38e3b['parity'][_0xde187e];if(!_0x2b20c3)return;const _0x276da3=_0xf38e3b[_0xc07c2c(0xbcd)][_0x4836f9]&&_0xf38e3b[_0xc07c2c(0xbcd)][_0x4836f9][_0xc07c2c(0x53c)]?_0xf38e3b[_0xc07c2c(0xbcd)][_0x4836f9]['size']:_0x2b20c3['byteLength'],_0x420829=new Uint8Array(_0x2b20c3['byteLength']);_0x420829[_0xc07c2c(0x43d)](_0x2b20c3);for(let _0xd1f09a=0x0;_0xd1f09a<_0x392078;_0xd1f09a++){const _0x592781=_0x59e5f0+_0xd1f09a;if(_0x592781===_0x4836f9)continue;const _0x32a5eb=_0xf38e3b[_0xc07c2c(0x67b)][_0x592781];if(!_0x32a5eb)return;for(let _0x13d610=0x0;_0x13d610<_0x420829[_0xc07c2c(0xc04)];_0x13d610++){_0x420829[_0x13d610]^=_0x13d610<_0x32a5eb[_0xc07c2c(0xc04)]?_0x32a5eb[_0x13d610]:0x0;}}_0xf38e3b[_0xc07c2c(0x67b)][_0x4836f9]=_0x420829['slice'](0x0,_0x276da3);const _0x33b3c0=_0x2fc170();_0x33b3c0[_0xc07c2c(0x287)][_0xc07c2c(0x944)]+=0x1;if(_0xab9a9c[_0xc07c2c(0x404)][_0x2fcb80['UUID']]&&_0xab9a9c[_0xc07c2c(0x404)][_0x2fcb80[_0xc07c2c(0xab9)]][_0xc07c2c(0x287)]){const _0xf88086=_0xab9a9c[_0xc07c2c(0x404)][_0x2fcb80[_0xc07c2c(0xab9)]]['stats'][_0xc07c2c(0x3bb)]=_0xab9a9c[_0xc07c2c(0x404)][_0x2fcb80[_0xc07c2c(0xab9)]][_0xc07c2c(0x287)][_0xc07c2c(0x3bb)]||{};_0xf88086['fec_repairs']=(_0xf88086[_0xc07c2c(0x52f)]||0x0)+0x1;}}function _0x256bd0(_0x4c1b62,_0x59ed5c){var _0x5dd75d=_0x266373;_0x4c1b62[_0x5dd75d(0x5fa)]=!![],_0x4c1b62[_0x5dd75d(0x9c9)]=_0x59ed5c||null;}function _0x1e0f22(_0x2ec15a){var _0x325e51=_0x266373;const _0x264dec=_0x2fc170();_0x264dec['stats'][_0x325e51(0x7e7)]=(_0x264dec[_0x325e51(0x287)][_0x325e51(0x7e7)]||0x0)+0x1;if(_0xab9a9c['rpcs'][_0x2fcb80[_0x325e51(0xab9)]]&&_0xab9a9c[_0x325e51(0x404)][_0x2fcb80[_0x325e51(0xab9)]][_0x325e51(0x287)]){const _0x5aac16=_0xab9a9c[_0x325e51(0x404)][_0x2fcb80['UUID']]['stats'][_0x325e51(0x3bb)]=_0xab9a9c[_0x325e51(0x404)][_0x2fcb80[_0x325e51(0xab9)]][_0x325e51(0x287)][_0x325e51(0x3bb)]||{};_0x5aac16[_0x325e51(0x34b)]=(_0x5aac16[_0x325e51(0x34b)]||0x0)+0x1;}_0x2ec15a['dropped']=!![];}async function _0x2b7440(_0x2106a5){var _0x1b4212=_0x266373;if(!_0x2106a5||_0x2106a5['complete'])return;const _0x3d32de=[];for(let _0x3a8936=0x0;_0x3a8936<_0x2106a5[_0x1b4212(0x67b)][_0x1b4212(0xade)];_0x3a8936++){!_0x2106a5[_0x1b4212(0x67b)][_0x3a8936]&&_0x3d32de['push'](_0x3a8936);}if(!_0x3d32de[_0x1b4212(0xade)]){_0x256bd0(_0x2106a5,_0x58caad(_0x2106a5[_0x1b4212(0x67b)]));return;}if(!_0x2106a5[_0x1b4212(0x983)]){_0x1e0f22(_0x2106a5),_0x256bd0(_0x2106a5,null);return;}_0x3d32de[_0x1b4212(0x306)](_0x3d491f=>_0x4b36bb(_0x2106a5,_0x3d491f));const _0x1d1509=_0x3d32de['every'](_0x2cb686=>(_0x2106a5[_0x1b4212(0xcc5)]['d:'+_0x2cb686]||0x0)>=_0x5d7731);_0x1d1509&&(_0x1e0f22(_0x2106a5),_0x256bd0(_0x2106a5,null));}async function _0xbdb42f(){var _0x2bdcec=_0x266373;const _0x2b5f8f=_0x2fc170();while(_0x2b5f8f[_0x2bdcec(0x9ae)]['length']){const _0x36e845=_0x2b5f8f[_0x2bdcec(0x9ae)][0x0],_0x5d2f59=_0x2b5f8f[_0x2bdcec(0x720)][_0x2bdcec(0x666)](_0x36e845);if(!_0x5d2f59||!_0x5d2f59[_0x2bdcec(0x5fa)])break;_0x2b5f8f['order'][_0x2bdcec(0xa17)](),_0x2b5f8f[_0x2bdcec(0x720)][_0x2bdcec(0x6ea)](_0x36e845);if(_0x5d2f59['dropped']||!_0x5d2f59[_0x2bdcec(0x9c9)])continue;await _0x2fcb80[_0x2bdcec(0x85f)]({'data':_0x5d2f59['assembled'],'timestamp':_0x5d2f59[_0x2bdcec(0x9f5)],'type':_0x5d2f59[_0x2bdcec(0x3c5)]});}}function _0x366b05(_0x53be93){var _0x4422ba=_0x266373;const _0xa4a270=_0x2fc170(),_0x5f58cf=_0xf8d94(_0x53be93);if(!_0x5f58cf[_0x4422ba(0x70a)]||typeof _0x5f58cf['extra']!==_0x4422ba(0x537)){_0xa4a270[_0x4422ba(0x598)]=!![],_0xa4a270[_0x4422ba(0x68e)]=null,_0xa4a270[_0x4422ba(0x8db)]=null,_0x2fcb80[_0x4422ba(0xa4a)]=_0x53be93;return;}if(_0x5f58cf[_0x4422ba(0x70a)][_0x4422ba(0x813)]){const _0x2cfafb=_0x7b525b(_0x5f58cf[_0x4422ba(0x70a)][_0x4422ba(0x9d7)]);if(!_0x2cfafb)return;_0xa4a270[_0x4422ba(0x8db)]={'frame':_0x2cfafb,'parity':!!_0x5f58cf[_0x4422ba(0x70a)][_0x4422ba(0x73f)],'chunkIndex':Number[_0x4422ba(0x9e8)](_0x5f58cf[_0x4422ba(0x70a)][_0x4422ba(0x8bd)])?parseInt(_0x5f58cf[_0x4422ba(0x70a)]['chunkIndex']):0x0,'parityIndex':Number[_0x4422ba(0x9e8)](_0x5f58cf[_0x4422ba(0x70a)][_0x4422ba(0x4cb)])?parseInt(_0x5f58cf[_0x4422ba(0x70a)]['parityIndex']):Number[_0x4422ba(0x9e8)](_0x5f58cf['extra'][_0x4422ba(0x8bd)])?parseInt(_0x5f58cf[_0x4422ba(0x70a)][_0x4422ba(0x8bd)]):0x0};return;}const _0x3ead13=_0x452a7e(_0x5f58cf);_0x3ead13[_0x4422ba(0x67b)][_0x4422ba(0xade)]===0x0&&(_0x3ead13[_0x4422ba(0x67b)]=[null]);}async function _0x4d5027(_0x14e7e5){var _0x44846c=_0x266373;const _0x2421d6=_0x2fc170(),_0x27f6bb=_0x14e7e5 instanceof Uint8Array?_0x14e7e5:new Uint8Array(_0x14e7e5);if(_0x2421d6['legacyMode']){await _0xd3b955(_0x27f6bb);return;}if(_0x2421d6['pendingResend']){const _0x265a27=_0x2421d6[_0x44846c(0x8db)]['frame'];_0x265a27&&(_0x2421d6['pendingResend'][_0x44846c(0x73f)]?(_0x265a27['parity'][_0x2421d6[_0x44846c(0x8db)][_0x44846c(0x4cb)]]=_0x27f6bb,_0x59efc5(_0x265a27,_0x2421d6['pendingResend'][_0x44846c(0x4cb)])):_0x265a27['data'][_0x2421d6[_0x44846c(0x8db)][_0x44846c(0x8bd)]]=_0x27f6bb,await _0x2b7440(_0x265a27),await _0xbdb42f());_0x2421d6[_0x44846c(0x8db)]=null;return;}const _0x2b9bc8=_0x2421d6[_0x44846c(0x68e)];if(!_0x2b9bc8){await _0xd3b955(_0x27f6bb);return;}if(_0x2b9bc8[_0x44846c(0x7ec)]<_0x2b9bc8['data'][_0x44846c(0xade)]){_0x2b9bc8[_0x44846c(0x67b)][_0x2b9bc8[_0x44846c(0x7ec)]]=_0x27f6bb,_0x2b9bc8[_0x44846c(0x7ec)]+=0x1;_0x2b9bc8[_0x44846c(0x7ec)]===_0x2b9bc8[_0x44846c(0x67b)]['length']&&_0x2b9bc8[_0x44846c(0x73f)][_0x44846c(0xade)]===0x0&&(await _0x2b7440(_0x2b9bc8),_0x2421d6[_0x44846c(0x68e)]=null,await _0xbdb42f());return;}if(_0x2b9bc8[_0x44846c(0x1e4)]<_0x2b9bc8['parity']['length']){_0x2b9bc8[_0x44846c(0x73f)][_0x2b9bc8[_0x44846c(0x1e4)]]=_0x27f6bb,_0x59efc5(_0x2b9bc8,_0x2b9bc8[_0x44846c(0x1e4)]),_0x2b9bc8[_0x44846c(0x1e4)]+=0x1;_0x2b9bc8['nextParityIndex']===_0x2b9bc8[_0x44846c(0x73f)][_0x44846c(0xade)]&&(await _0x2b7440(_0x2b9bc8),_0x2421d6[_0x44846c(0x68e)]=null,await _0xbdb42f());return;}await _0xd3b955(_0x27f6bb);}async function _0xd3b955(_0x52412e){var _0x381272=_0x266373;if(_0x52412e[_0x381272(0xc04)]>=0x40000){if(_0x2fcb80[_0x381272(0x657)]){const _0x2c2689=new Uint8Array(_0x2fcb80[_0x381272(0x657)][_0x381272(0xade)]+_0x52412e[_0x381272(0xc04)]);_0x2c2689['set'](_0x2fcb80[_0x381272(0x657)]),_0x2c2689['set'](_0x52412e,_0x2fcb80[_0x381272(0x657)][_0x381272(0xade)]),_0x2fcb80[_0x381272(0x657)]=_0x2c2689;}else _0x2fcb80[_0x381272(0x657)]=new Uint8Array(_0x52412e);return;}if(_0x2fcb80[_0x381272(0x657)]){const _0x119b62=new Uint8Array(_0x2fcb80['buffer']['length']+_0x52412e[_0x381272(0xc04)]);_0x119b62[_0x381272(0x43d)](_0x2fcb80['buffer']),_0x119b62['set'](_0x52412e,_0x2fcb80[_0x381272(0x657)][_0x381272(0xade)]),_0x2fcb80['buffer']=null,await _0x2fcb80[_0x381272(0x85f)]({'data':_0x119b62,'timestamp':_0x2fcb80['frameMeta'][0x0],'type':_0x2fcb80[_0x381272(0xa4a)][0x1]});}else await _0x2fcb80[_0x381272(0x85f)]({'data':_0x52412e,'timestamp':_0x2fcb80[_0x381272(0xa4a)][0x0],'type':_0x2fcb80['frameMeta'][0x1]});}_0x2fcb80[_0x266373(0xc8d)][_0x266373(0xb95)]=async function(_0x20b60a){var _0x595383=_0x266373;if(!_0x157435)try{let _0x349730=JSON[_0x595383(0xa4b)](_0x20b60a[_0x595383(0x67b)]);if(_0x349730[_0x595383(0x3c5)]==_0x595383(0xb9b)){log(_0x595383(0xbca)),_0x157435=_0x349730;_0xab9a9c['retransmit']&&_0xab9a9c[_0x595383(0x526)](_0x157435,_0x2fcb80[_0x595383(0xc8d)]);log(_0x595383(0x732)),log(_0x157435),_0x2fcb80[_0x595383(0x83f)]=_0x157435,_0x2fcb80[_0x595383(0xab9)]=_0xedeebf,_0x2fcb80[_0x595383(0x5ea)]=0x0,_0x2fcb80['status']=0x2,_0x2fcb80[_0x595383(0x56c)]=Date[_0x595383(0x30e)](),_0x2fcb80[_0x595383(0x566)]=_0x157435[_0x595383(0x9f5)],_0x2fcb80['remoteTimestampBase']=_0x157435['timestamp']||_0x2fcb80[_0x595383(0x56c)],_0x2fcb80['timedelta']=_0x2fcb80[_0x595383(0x56c)]-_0x2fcb80[_0x595383(0x841)];typeof performance!==_0x595383(0x2cf)&&performance[_0x595383(0x30e)]?(_0x2fcb80[_0x595383(0x1da)]=performance['now'](),_0x2fcb80[_0x595383(0x99c)]=function(){var _0x349cc8=_0x595383;return _0x2fcb80[_0x349cc8(0x841)]+(performance['now']()-_0x2fcb80[_0x349cc8(0x1da)]);}):_0x2fcb80[_0x595383(0x99c)]=function(){var _0x3c5b0d=_0x595383;return Date[_0x3c5b0d(0x30e)]()-_0x2fcb80[_0x3c5b0d(0x4de)];};_0x2fcb80['dc']=_0x2fcb80['channel'],_0x2fcb80['id']=_0x157435['id'],_0x2fcb80[_0x595383(0x26b)]=null,_0x2fcb80[_0x595383(0x657)]=![];!_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x7eb)]&&(_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x7eb)]=createVideoElement());_0x2fcb80[_0x595383(0x7eb)]=_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x7eb)];!_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x7eb)][_0x595383(0x5f6)]&&(_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x7eb)][_0x595383(0x5f6)]=createMediaStream());!_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0xca7)]&&(_0xab9a9c['rpcs'][_0xedeebf]['streamSrc']=createMediaStream());_0x2fcb80[_0x595383(0xca7)]=_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0xca7)],_0x2fcb80[_0x595383(0x7eb)]['autoplay']=!![],_0x2fcb80[_0x595383(0x7eb)][_0x595383(0x8b2)]=![],_0x2fcb80[_0x595383(0x7eb)][_0x595383(0xc24)](_0x595383(0x515),''),_0x2fcb80[_0x595383(0x7eb)][_0x595383(0xa18)][_0x595383(0x2e8)]=_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0xa96)],_0x2fcb80['videoElement']['id']=_0x595383(0x628)+_0xedeebf,_0x2fcb80['videoElement'][_0x595383(0xa18)]['UUID']=_0xedeebf,_0x2fcb80[_0x595383(0x7eb)][_0x595383(0xb9b)]=!![];_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x4d6)]&&applyMirrorGuest(_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x4d6)],_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x7eb)],_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0xb30)]);_0xab9a9c['rpcs'][_0xedeebf][_0x595383(0x359)]!==![]&&(_0xab9a9c['rpcs'][_0xedeebf][_0x595383(0x7eb)]['rotated']=_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x359)],_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x7eb)]['dataset'][_0x595383(0x278)]=_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x359)]);_0x2fcb80[_0x595383(0x7eb)][_0x595383(0xcba)]('playing',_0x306322=>{var _0x21d73e=_0x595383;try{var _0x58af5f=document[_0x21d73e(0x2b2)](_0x21d73e(0x2ef));_0x58af5f&&_0x58af5f[_0x21d73e(0x3d1)][_0x21d73e(0x5be)](_0x58af5f);}catch(_0x17c96e){}_0x2fcb80['playing']=!![];if(_0x2fcb80[_0x21d73e(0x795)])_0x2fcb80[_0x21d73e(0x795)][_0x21d73e(0xc27)]();else _0xab9a9c[_0x21d73e(0x44a)]&&_0xab9a9c[_0x21d73e(0x44a)][_0x21d73e(0xc27)]();try{_0xab9a9c[_0x21d73e(0x999)]&&(v[_0x21d73e(0x70b)]>=0x3&&(!v['pip']&&(v[_0x21d73e(0x999)]=!![],toggleSystemPip(v,!![]))));}catch(_0x1692d0){}},{'once':!![]}),_0x2fcb80[_0x595383(0x7eb)][_0x595383(0xcba)]('error',function(_0x1ab4d8){errorlog(_0x1ab4d8);}),_0x2fcb80[_0x595383(0x7eb)][_0x595383(0x755)]=_0xfc385d,_0x2fcb80['videoElement']['oncanplay']=function(){updateMixer();},_0x2fcb80['videoWriter']=![],_0x2fcb80['frameMeta']=![],_0x2fcb80['writer_config']={},_0x2fcb80['writer_config'][_0x595383(0xa55)]=![],_0x2fcb80[_0x595383(0x7bc)]['audio']=![],_0x2fcb80[_0x595383(0xb45)]=Number[_0x595383(0x9e8)](parseFloat(_0xab9a9c['chunkjitterslack']))?parseFloat(_0xab9a9c[_0x595383(0x5ac)]):0x0,_0x2fcb80[_0x595383(0xa22)]=![],_0x2fcb80[_0x595383(0x896)]=![],_0x2fcb80[_0x595383(0x74b)]=![],_0x2fcb80[_0x595383(0x759)]=![],_0x2fcb80[_0x595383(0xa55)]=![],_0x2fcb80['audio']=![],_0x2fcb80[_0x595383(0x75e)]=![],_0x2fcb80[_0x595383(0xa68)]=![],_0x2fcb80[_0x595383(0x9d2)]=0x1;if(_0x157435['configVideo']){_0xab9a9c[_0x595383(0x404)][_0xedeebf]['stats'][_0x595383(0x3bb)]=_0x157435['configVideo'],_0x2fcb80[_0x595383(0xa22)]={};const _0x5cc1fe=_0x1dacdf(parseInt(_0x157435['configVideo'][_0x595383(0x28e)])||0x500,0x2),_0x29a00f=_0x1dacdf(parseInt(_0x157435['configVideo']['height'])||0x2d0,0x2);_0x2fcb80[_0x595383(0xa22)][_0x595383(0x28e)]=_0x5cc1fe,_0x2fcb80['stream_configVideo'][_0x595383(0x43f)]=_0x29a00f,_0x2fcb80[_0x595383(0xa22)][_0x595383(0x44b)]=_0x157435['configVideo']['codec']||_0x595383(0xc22),_0x2fcb80['writer_config'][_0x595383(0xa55)]=!![],_0x2fcb80[_0x595383(0x7bc)][_0x595383(0x28e)]=_0x5cc1fe,_0x2fcb80['writer_config'][_0x595383(0x43f)]=_0x29a00f;if(_0x157435['configVideo'][_0x595383(0x44b)]=='vp09.00.10.08')_0x2fcb80[_0x595383(0x7bc)][_0x595383(0x44b)]='VP9';else{if(_0x157435['configVideo'][_0x595383(0x44b)]==_0x595383(0x330))_0x2fcb80[_0x595383(0x7bc)][_0x595383(0x44b)]=_0x595383(0xb8c);else{if(_0x157435['configVideo'][_0x595383(0x44b)]=='av1')_0x2fcb80[_0x595383(0x7bc)]['codec']=_0x595383(0xb8c);else{if(_0x157435[_0x595383(0x955)][_0x595383(0x44b)]==_0x595383(0x906))_0x2fcb80[_0x595383(0x7bc)][_0x595383(0x44b)]=_0x595383(0xc8a);else _0x157435[_0x595383(0x955)][_0x595383(0x44b)]==_0x595383(0xaa9)?_0x2fcb80[_0x595383(0x7bc)][_0x595383(0x44b)]='H264':_0x2fcb80['writer_config']['codec']='VP9';}}}_0x2fcb80[_0x595383(0x74b)]={'output':_0x4981bf=>{var _0x139812=_0x595383;try{_0x2fcb80['video']['frameWriter']['write'](_0x4981bf)[_0x139812(0xacc)](_0x55d4d5=>{});}catch(_0x5b8c19){}},'error':_0x2980c8=>{var _0xd2141e=_0x595383;_0x2fcb80[_0xd2141e(0xa55)][_0xd2141e(0xbba)]['state']=='closed'?(errorlog(_0x2980c8[_0xd2141e(0x3a3)]),warnlog(_0xd2141e(0x21e))):errorlog(_0x2980c8['message']);}},_0x2fcb80['video']={},_0x2fcb80[_0x595383(0xa55)][_0x595383(0x59b)]=new MediaStreamTrackGenerator({'kind':'video'}),_0x2fcb80[_0x595383(0xa55)][_0x595383(0x452)]=new MediaStream([_0x2fcb80[_0x595383(0xa55)][_0x595383(0x59b)]]),_0x2fcb80[_0x595383(0xa55)][_0x595383(0x6bc)]=_0x2fcb80[_0x595383(0xa55)]['generator'][_0x595383(0x483)][_0x595383(0x8ef)](),_0x2fcb80[_0x595383(0xa55)][_0x595383(0xbba)]=new VideoDecoder(_0x2fcb80[_0x595383(0x74b)]),_0x2fcb80[_0x595383(0xa55)]['decoder'][_0x595383(0x8e5)](_0x2fcb80[_0x595383(0xa22)]),_0x2fcb80['video'][_0x595383(0x9fc)]=[],_0x2fcb80[_0x595383(0xa55)][_0x595383(0x699)]=null,_0x2fcb80[_0x595383(0xa55)]['playbackheader']=![],_0x2fcb80[_0x595383(0xa55)][_0x595383(0xc96)]=![],_0x595383(0x52e)in _0x157435&&(_0x2fcb80[_0x595383(0xa55)][_0x595383(0xac7)]=_0x157435['realTimeVideo']),_0x2fcb80[_0x595383(0xca7)][_0x595383(0x311)](_0x2fcb80[_0x595383(0xa55)][_0x595383(0x452)]['getVideoTracks']()[0x0]);}const _0x5179de=function(_0x3fbb50=_0x595383(0xa55),_0x12b0b8=0xc8){var _0x5d75d6=_0x595383;const _0x2b156=_0xab9a9c[_0x5d75d6(0x404)][_0x2fcb80[_0x5d75d6(0xab9)]],_0x531540=_0x1880a9=>{if(_0x1880a9===![]||_0x1880a9===null||_0x1880a9===undefined)return null;const _0x3f35b7=parseFloat(_0x1880a9);return Number['isFinite'](_0x3f35b7)?_0x3f35b7:null;},_0x2b72cd=_0x531540(_0x3fbb50===_0x5d75d6(0xa55)?_0xab9a9c[_0x5d75d6(0xc6e)]:_0xab9a9c[_0x5d75d6(0x98f)]);let _0x37ae97=_0x2b72cd!==null?_0x2b72cd:_0x12b0b8;if(_0x2b156){if(_0x3fbb50===_0x5d75d6(0x544)){const _0xe34d55=_0x531540(_0xab9a9c['audioBuffer']);_0xe34d55!==null&&(_0x37ae97=_0xe34d55);}const _0x4c073c=_0x531540(_0x2b156[_0x5d75d6(0x657)]);if(_0x4c073c!==null)_0x37ae97=_0x4c073c;else{const _0x7eb9de=_0x531540(_0xab9a9c['buffer']);_0x7eb9de!==null?_0x37ae97=_0x7eb9de:_0x2b156[_0x5d75d6(0x657)]=_0x37ae97;}}const _0x45830f=_0x531540(_0xab9a9c['chunkbufferfloor']),_0x33fac1=_0x531540(_0xab9a9c[_0x5d75d6(0x43b)]),_0x2680ee=_0x45830f!==null?_0x45830f:0x0,_0x49c9f=_0x3fbb50===_0x5d75d6(0x544)?0x1388:0x7530,_0x2b792a=_0x33fac1!==null?_0x33fac1:_0x49c9f;if(typeof _0xab9a9c['calculateOptimalBufferSize']===_0x5d75d6(0x32c))try{const _0x521172=_0xab9a9c[_0x5d75d6(0x1cc)](_0x2fcb80[_0x5d75d6(0xab9)],{'media':_0x3fbb50,'base':_0x37ae97,'clamp':{'min':_0x2680ee,'max':_0x2b792a}});typeof _0x521172===_0x5d75d6(0xab6)&&!Number['isNaN'](_0x521172)&&(_0x37ae97=_0x521172);}catch(_0x479c18){errorlog(_0x479c18);}return!Number[_0x5d75d6(0x9e8)](_0x37ae97)&&(_0x37ae97=_0x12b0b8),_0x37ae97=Math[_0x5d75d6(0x642)](_0x2680ee,Math[_0x5d75d6(0xb03)](_0x37ae97,_0x2b792a)),_0x37ae97;};_0x157435[_0x595383(0xa91)]&&(_0xab9a9c['rpcs'][_0xedeebf]['stats'][_0x595383(0x340)]=_0x157435['configAudio'],_0x2fcb80[_0x595383(0x896)]=_0x157435[_0x595383(0xa91)],_0x2fcb80[_0x595383(0x7bc)][_0x595383(0x544)]=!![],_0x2fcb80[_0x595383(0x7bc)][_0x595383(0x2df)]=_0x157435[_0x595383(0xa91)][_0x595383(0xa5c)]||0xbb80,_0x2fcb80[_0x595383(0x7bc)][_0x595383(0x867)]=_0x157435[_0x595383(0xa91)][_0x595383(0x51c)]||0x1,_0x2fcb80[_0x595383(0x896)]['codec']&&_0x2fcb80[_0x595383(0x896)]['codec']==_0x595383(0xcc0)?(!_0x2fcb80[_0x595383(0x5bb)]?_0x2fcb80[_0x595383(0x5bb)]=_0xab9a9c[_0x595383(0x44a)][_0x595383(0x627)]():_0x2fcb80[_0x595383(0xca7)][_0x595383(0x568)]()['forEach'](_0x5bf4f3=>{var _0x1c1739=_0x595383;_0x2fcb80[_0x1c1739(0xca7)]['removeTrack'](_0x5bf4f3);}),_0x2fcb80['destination']['stream']['getAudioTracks']()[_0x595383(0x306)](_0x1aeaef=>{var _0x390485=_0x595383;_0x2fcb80['streamSrc'][_0x390485(0x311)](_0x1aeaef);}),_0x2fcb80[_0x595383(0x8de)]=!![]):(!_0x2fcb80[_0x595383(0x544)]&&(_0x2fcb80['audio']={}),_0x2fcb80[_0x595383(0x544)]['queue']=[],_0x2fcb80['audio'][_0x595383(0x699)]=null,_0x595383(0xc14)in _0x157435?_0x2fcb80[_0x595383(0x544)]['realTime']=_0x157435['realTimeAudio']:errorlog(_0x595383(0x5c4)),_0x2fcb80['init_audio']={'output':_0xeb0c9b=>{var _0x560349=_0x595383;_0x2fcb80['audio'][_0x560349(0x6bc)][_0x560349(0x76b)](_0xeb0c9b);const _0x49d7fd=_0xab9a9c['rpcs'][_0x2fcb80['UUID']];if(!_0x49d7fd||!_0x49d7fd['stats']||!_0x49d7fd[_0x560349(0x287)][_0x560349(0x340)])return;if(_0x2fcb80[_0x560349(0x37b)])return;const _0x2d50c5=_0x49d7fd['stats'][_0x560349(0x340)],_0x342e64=typeof _0x2fcb80[_0x560349(0x99c)]===_0x560349(0x32c)?_0x2fcb80[_0x560349(0x99c)]():Date[_0x560349(0x30e)]()-(_0x2fcb80['timedelta']||0x0),_0x161802=typeof _0xeb0c9b[_0x560349(0x9f5)]==='number'?_0xeb0c9b[_0x560349(0x9f5)]:0x0,_0x5c1fae=_0x2fcb80[_0x560349(0x544)][_0x560349(0xac7)]||0x0,_0x1e105f=_0x161802/0x3e8-(_0x342e64-_0x5c1fae),_0x362a3f=((_0xab9a9c[_0x560349(0x44a)][_0x560349(0xc0c)]||0x0)+(_0xab9a9c[_0x560349(0x44a)][_0x560349(0xc67)]||0x0))*0x3e8;!_0x2fcb80[_0x560349(0x544)][_0x560349(0xb2a)]&&(_0x2fcb80[_0x560349(0x544)][_0x560349(0xb2a)]={'targetMs':0xc8,'minLead':0x18,'rejoinMargin':0x78,'rebuffering':![]});const _0xe9da07=_0x2fcb80[_0x560349(0x544)][_0x560349(0xb2a)],_0x25ff46=_0x5179de(_0x560349(0x544),_0xe9da07[_0x560349(0x860)]??0xc8);_0xe9da07[_0x560349(0x860)]=_0x25ff46;if(_0x25ff46===0x0)_0xe9da07[_0x560349(0xcb3)]=0x0,_0xe9da07['rejoinMargin']=0x0;else{_0xe9da07['minLead']=Math[_0x560349(0x642)](0x14,Math[_0x560349(0xb03)](0x64,_0x25ff46*0.12));let _0x275244=Math['max'](_0xe9da07[_0x560349(0xcb3)],_0x25ff46*0.25);const _0x1a86c8=Number[_0x560349(0x9e8)](_0x2fcb80[_0x560349(0xb45)])?_0x2fcb80[_0x560349(0xb45)]:0x0;_0x1a86c8>0x0&&(_0x275244+=_0x1a86c8),_0xe9da07[_0x560349(0x1bf)]=_0x275244;}const _0x4af1d1=_0x1e105f+_0x25ff46;!_0xe9da07[_0x560349(0xb49)]&&_0x25ff46>0x0&&_0x4af1d1<_0xe9da07[_0x560349(0xcb3)]&&(_0xe9da07['rebuffering']=!![],_0xe9da07[_0x560349(0x57a)]=Date[_0x560349(0x30e)](),_0x2d50c5[_0x560349(0x2c3)]=_0xe9da07['lastUnderflow']);_0xe9da07[_0x560349(0xb49)]&&(_0x25ff46===0x0||_0x4af1d1>=_0x25ff46-_0xe9da07['rejoinMargin'])&&(_0xe9da07[_0x560349(0xb49)]=![]);let _0x21d28e;if(_0x25ff46===0x0)_0x21d28e=Math[_0x560349(0x642)](0x0,_0x4af1d1);else _0xe9da07[_0x560349(0xb49)]?_0x21d28e=Math['max'](_0x25ff46,_0xe9da07[_0x560349(0xcb3)]):_0x21d28e=Math[_0x560349(0x642)](_0xe9da07['minLead'],_0x4af1d1);let _0x159d70=_0x21d28e-_0x362a3f;(_0x159d70<0x0||!Number[_0x560349(0x9e8)](_0x159d70))&&(_0x159d70=0x0);_0x2d50c5['buffer_dateNow']=Date[_0x560349(0x30e)](),_0x2d50c5[_0x560349(0x62a)]=_0x2fcb80[_0x560349(0x4de)],_0x2d50c5['buffer_realTime']=_0x2fcb80[_0x560349(0x544)][_0x560349(0xac7)],_0x2d50c5[_0x560349(0x4cf)]=_0x161802,_0x2d50c5[_0x560349(0x62f)]=_0x159d70,_0x2d50c5['buffer_buffer']=_0x25ff46,_0x2d50c5['buffer_baseLatency']=(_0xab9a9c[_0x560349(0x44a)][_0x560349(0xc0c)]||0x0)*0x3e8,_0x2d50c5[_0x560349(0x1d8)]=(_0xab9a9c[_0x560349(0x44a)][_0x560349(0xc67)]||0x0)*0x3e8;const _0x435f89=Math['max'](0x0,_0x21d28e);_0x2d50c5[_0x560349(0x474)]=Math['round'](_0x1e105f),_0x2d50c5['buffer_level']=Math[_0x560349(0xc45)](_0x435f89),_0x2d50c5[_0x560349(0xb49)]=_0xe9da07['rebuffering'];try{_0x2fcb80[_0x560349(0xb99)][_0x560349(0x2d1)][_0x560349(0x60d)]?_0x2fcb80[_0x560349(0xb99)][_0x560349(0x2d1)][_0x560349(0x60d)](_0x159d70/0x3e8,_0xab9a9c['audioCtx'][_0x560349(0x674)],0.05):_0x2fcb80[_0x560349(0xb99)][_0x560349(0x2d1)][_0x560349(0x226)](_0x159d70/0x3e8,_0xab9a9c[_0x560349(0x44a)][_0x560349(0x674)]);}catch(_0xd8bfce){errorlog(_0xd8bfce);}const _0x13b9c2=Math[_0x560349(0x642)](0xa,_0x159d70);_0x2fcb80[_0x560349(0x37b)]=setTimeout(function(){_0x2fcb80['audioTime']=null;},_0x13b9c2);},'error':_0x2e1248=>{var _0xdfe7f7=_0x595383;_0x2fcb80[_0xdfe7f7(0x544)]['decoder'][_0xdfe7f7(0x4e0)]=='closed'?(errorlog(_0x2e1248[_0xdfe7f7(0x3a3)]),warnlog(_0xdfe7f7(0x21e))):errorlog(_0x2e1248[_0xdfe7f7(0x3a3)]);}},_0x2fcb80[_0x595383(0x544)]['decoder']=new AudioDecoder(_0x2fcb80[_0x595383(0x759)]),_0x2fcb80[_0x595383(0x544)][_0x595383(0xbba)][_0x595383(0x8e5)](_0x2fcb80[_0x595383(0x896)]),_0x2fcb80['audio'][_0x595383(0x59b)]=new MediaStreamTrackGenerator({'kind':_0x595383(0x544)}),_0x2fcb80['audio'][_0x595383(0x6bc)]=_0x2fcb80[_0x595383(0x544)]['generator'][_0x595383(0x483)][_0x595383(0x8ef)](),_0x2fcb80['audio'][_0x595383(0x452)]=new MediaStream([_0x2fcb80[_0x595383(0x544)]['generator']]),_0x2fcb80['audio'][_0x595383(0x738)]=_0xab9a9c[_0x595383(0x44a)][_0x595383(0x8ac)](_0x2fcb80[_0x595383(0x544)][_0x595383(0x452)]),_0x2fcb80[_0x595383(0xb99)]=_0xab9a9c[_0x595383(0x44a)][_0x595383(0x366)](0x1e),_0x2fcb80[_0x595383(0xb99)][_0x595383(0x2d1)][_0x595383(0xb58)]=0x0,_0x2fcb80[_0x595383(0x544)]['audioNode'][_0x595383(0x3ea)](_0x2fcb80[_0x595383(0xb99)]),_0x2fcb80['destination']=_0xab9a9c[_0x595383(0x44a)][_0x595383(0x627)](),_0x2fcb80[_0x595383(0xb99)][_0x595383(0x3ea)](_0x2fcb80[_0x595383(0x5bb)]),_0x2fcb80[_0x595383(0x5bb)]['stream'][_0x595383(0x568)]()[_0x595383(0x306)](_0x478afc=>{var _0x3acfe4=_0x595383;_0x2fcb80['streamSrc'][_0x3acfe4(0x311)](_0x478afc);})));warnlog(_0x157435),setupIncomingVideoTracking(_0xab9a9c[_0x595383(0x404)][_0xedeebf][_0x595383(0x7eb)],_0xedeebf);if(_0x2fcb80[_0x595383(0x544)]&&_0x2fcb80[_0x595383(0xa55)])updateIncomingVideoElement(_0xedeebf);else{if(_0x2fcb80['video'])updateIncomingVideoElement(_0xedeebf,!![],![]);else _0x2fcb80[_0x595383(0x544)]&&updateIncomingVideoElement(_0xedeebf,![],!![]);}_0x2fcb80[_0x595383(0x85f)]=async function(_0x2b0efc){var _0x4b8381=_0x595383;_0xab9a9c[_0x4b8381(0xc30)]&&_0x4b8381(0x9f5)in _0x2b0efc&&_0xab9a9c[_0x4b8381(0x404)][_0xedeebf]&&pokeIframeAPI(_0x4b8381(0x26a),{'UUID':_0xedeebf,'streamID':_0xab9a9c[_0x4b8381(0x404)][_0xedeebf][_0x4b8381(0xa96)],'type':_0x2b0efc['type'],'ts':_0x2b0efc['timestamp']});if(_0x2b0efc[_0x4b8381(0x3c5)]=='audio'){try{_0xab9a9c[_0x4b8381(0x404)][_0x2fcb80['UUID']][_0x4b8381(0x287)][_0x4b8381(0x340)][_0x4b8381(0x371)]=parseInt(_0x2b0efc[_0x4b8381(0x9f5)]/0x2710)/0x64;}catch(_0x240687){console[_0x4b8381(0xca1)](_0x4b8381(0xc99),_0x240687);return;}_0x2fcb80['processFrameAudio'](_0x2b0efc);}else{if(_0x2b0efc[_0x4b8381(0x3c5)]==_0x4b8381(0xcc0)){var _0x2fa7cb=_0xab9a9c[_0x4b8381(0x44a)][_0x4b8381(0x30d)]();_0x2fa7cb[_0x4b8381(0x3ea)](_0x2fcb80['destination']),_0x2fa7cb[_0x4b8381(0x5c2)]=function(){var _0x1e66e2=_0x4b8381;this[_0x1e66e2(0xa63)]();};var _0x38fe88=_0xab9a9c[_0x4b8381(0x44a)][_0x4b8381(0x689)](0x2,_0x2b0efc[_0x4b8381(0x67b)][_0x4b8381(0xade)],_0xab9a9c['audioCtx'][_0x4b8381(0xa5c)]/0x2);_0x2fa7cb[_0x4b8381(0x657)]=_0x38fe88;var _0xe9e480=_0x38fe88[_0x4b8381(0xbef)](0x0)[_0x4b8381(0x43d)](_0x2b0efc['data']);_0x2fa7cb[_0x4b8381(0x59f)](0x0);}else _0xab9a9c[_0x4b8381(0x404)][_0x2fcb80[_0x4b8381(0xab9)]][_0x4b8381(0x287)][_0x4b8381(0x3bb)][_0x4b8381(0x371)]=parseInt(_0x2b0efc[_0x4b8381(0x9f5)]/0x2710)/0x64,_0x2fcb80['processFrameVideo'](_0x2b0efc);}},_0x2fcb80[_0x595383(0x9dd)]=async function(_0x12af27){var _0x4ebc02=_0x595383;try{_0x12af27[_0x4ebc02(0x3c5)]?_0x12af27=new EncodedVideoChunk(_0x12af27):errorlog('dataframe\x20has\x20no\x20type');}catch(_0x137a0b){errorlog(_0x137a0b),errorlog(_0x12af27);return;}!_0x2fcb80[_0x4ebc02(0x505)]&&(_0x2fcb80[_0x4ebc02(0x505)]=function(_0x445622){const _0x49b598=[],_0x295ae4={'inFlight':![],'rebuffering':![],'rebufferTimer':null,'bufferState':{'targetMs':0xc8,'minLead':0x20,'rejoinMargin':0x78,'enterThreshold':0x8}};_0x2fcb80['video']['nextQueue']=null;function _0x1b8234(){var _0x3db606=_0x2b9f;if(!_0xab9a9c[_0x3db606(0x404)][_0x445622])return![];return!_0xab9a9c[_0x3db606(0x404)][_0x445622][_0x3db606(0x287)][_0x3db606(0x3bb)]&&(_0xab9a9c['rpcs'][_0x445622]['stats'][_0x3db606(0x3bb)]={}),!![];}function _0x3470ab(){var _0x4de260=_0x2b9f;if(typeof _0x2fcb80[_0x4de260(0x99c)]===_0x4de260(0x32c))return _0x2fcb80['getRemoteNow']();return Date[_0x4de260(0x30e)]()-(_0x2fcb80[_0x4de260(0x4de)]||0x0);}function _0x482ad0(_0x9e21bc){var _0x4021c5=_0x2b9f;const _0x5bc8b6=_0x295ae4[_0x4021c5(0xb2a)];_0x5bc8b6[_0x4021c5(0x860)]=_0x9e21bc;if(_0x9e21bc===0x0)_0x5bc8b6['minLead']=0x0,_0x5bc8b6[_0x4021c5(0x1bf)]=0x0,_0x5bc8b6['enterThreshold']=0x0;else{_0x5bc8b6[_0x4021c5(0xcb3)]=Math[_0x4021c5(0x642)](0x10,Math[_0x4021c5(0xb03)](0xa0,_0x9e21bc*0.12));let _0x155c9d=Math[_0x4021c5(0x642)](_0x5bc8b6[_0x4021c5(0xcb3)]*0x2,_0x9e21bc*0.3);const _0x776106=Number[_0x4021c5(0x9e8)](_0x2fcb80[_0x4021c5(0xb45)])?_0x2fcb80[_0x4021c5(0xb45)]:0x0;_0x776106>0x0&&(_0x155c9d+=_0x776106),_0x5bc8b6[_0x4021c5(0x1bf)]=_0x155c9d,_0x5bc8b6['enterThreshold']=Math[_0x4021c5(0xb03)](_0x5bc8b6['minLead']*0.5,_0x9e21bc*0.1);}return _0x5bc8b6;}function _0x3d1174(_0x1232c5){var _0x5588f3=_0x2b9f;if(_0x295ae4[_0x5588f3(0xcbf)])return;_0x295ae4['rebufferTimer']=setTimeout(function(){var _0x3986a6=_0x5588f3;_0x295ae4[_0x3986a6(0xcbf)]=null,_0x4fb45e();},_0x1232c5);}function _0x2e67e7(_0xbee324){var _0x40a096=_0x2b9f;if(!_0x2fcb80[_0x40a096(0xa55)]||!_0x2fcb80[_0x40a096(0xa55)][_0x40a096(0xbba)]||_0x2fcb80[_0x40a096(0xa55)][_0x40a096(0xbba)]['state']==='closed')return;try{_0x2fcb80[_0x40a096(0xa55)][_0x40a096(0xbba)][_0x40a096(0x4ba)](_0xbee324);}catch(_0x4a252f){errorlog(_0x4a252f),!_0x2fcb80[_0x40a096(0x822)]&&(_0x2fcb80['dc']['send'](JSON[_0x40a096(0x6d1)]({'kf':!![]})),_0x2fcb80[_0x40a096(0x822)]=setTimeout(function(){var _0x3a3a1a=_0x40a096;clearTimeout(_0x2fcb80['requestKeyframe']),_0x2fcb80[_0x3a3a1a(0x822)]=null;},0x3e8));}}function _0x4fb45e(){var _0x238f28=_0x2b9f;if(_0x295ae4[_0x238f28(0xb08)])return;if(!_0x49b598['length'])return;if(!_0x1b8234()){_0x49b598[_0x238f28(0xade)]=0x0;return;}if(!_0x2fcb80[_0x238f28(0xa55)]||!_0x2fcb80['video'][_0x238f28(0xbba)]||_0x2fcb80[_0x238f28(0xa55)]['decoder']['state']===_0x238f28(0x5e8)){_0x49b598[_0x238f28(0xade)]=0x0;return;}const _0x89d91a=_0x49b598[0x0],_0x3fec9e=typeof _0x89d91a[_0x238f28(0x9f5)]===_0x238f28(0xab6)?_0x89d91a['timestamp']:0x0,_0x957e42=_0x3470ab(),_0x106eeb=_0x2fcb80[_0x238f28(0xa55)]['realTime']||0x0,_0x8d52d4=_0x5179de('video',_0x295ae4[_0x238f28(0xb2a)]['targetMs']??0xc8),_0xc9eb3d=_0x482ad0(_0x8d52d4),_0x1b1adb=_0x3fec9e/0x3e8-(_0x957e42-_0x106eeb),_0x13cb0e=_0x1b1adb+_0x8d52d4,_0x45a42c=Math[_0x238f28(0x642)](0x0,_0x13cb0e);let _0x501144=Math[_0x238f28(0x642)](0x0,_0x13cb0e);const _0x4c3e12=_0xab9a9c['rpcs'][_0x445622]['stats'][_0x238f28(0x3bb)],_0x37cc29=function(_0x2c0a4f){var _0x25c235=_0x238f28;let _0xef1c5a=_0x2c0a4f;!(_0xef1c5a>=0x0&&Number[_0x25c235(0x9e8)](_0xef1c5a))&&(_0xef1c5a=0x0),_0x4c3e12[_0x25c235(0x474)]=Math[_0x25c235(0xc45)](_0x1b1adb),_0x4c3e12[_0x25c235(0x62f)]=Math[_0x25c235(0xc45)](_0xef1c5a),_0x4c3e12[_0x25c235(0x54e)]=Math[_0x25c235(0xc45)](_0x8d52d4),_0x4c3e12[_0x25c235(0x5b0)]=Math[_0x25c235(0xc45)](_0x45a42c),_0x4c3e12['rebuffering']=!!_0x295ae4[_0x25c235(0xb49)];},_0x4886de=_0xab9a9c[_0x238f28(0x44a)]&&_0xab9a9c[_0x238f28(0x44a)][_0x238f28(0x4e0)];if(!_0xab9a9c[_0x238f28(0x44a)]||_0x4886de!==_0x238f28(0x35d))return _0x37cc29(0x0),_0x49b598[_0x238f28(0xa17)](),_0x2e67e7(_0x89d91a),_0x4fb45e();const _0x585656=_0xc9eb3d[_0x238f28(0xca6)],_0xd1573e=_0x8d52d4>0x0?Math['max'](_0xc9eb3d['minLead'],_0x8d52d4*0.5):0x0;_0x8d52d4>0x0&&_0x45a42c<=_0x585656&&(_0x295ae4[_0x238f28(0xb49)]=!![],_0x4c3e12[_0x238f28(0x2c3)]=Date[_0x238f28(0x30e)]());if(_0x295ae4['rebuffering']&&_0x8d52d4>0x0&&_0x45a42c0x0&&_0x501144<_0xc9eb3d[_0x238f28(0xcb3)]&&(_0x501144=_0xc9eb3d[_0x238f28(0xcb3)]);_0x37cc29(_0x501144),_0x49b598['shift'](),_0x295ae4[_0x238f28(0xb08)]=!![];!_0xab9a9c[_0x238f28(0x482)]&&(_0xab9a9c[_0x238f28(0x482)]=_0xab9a9c[_0x238f28(0x44a)][_0x238f28(0xbec)](),_0xab9a9c[_0x238f28(0x482)]['gain']['value']=0x0,_0xab9a9c['silence']['connect'](_0xab9a9c[_0x238f28(0x44a)][_0x238f28(0x5bb)]));const _0x489518=_0xab9a9c[_0x238f28(0x44a)][_0x238f28(0x9aa)]();_0x489518[_0x238f28(0x3ea)](_0xab9a9c[_0x238f28(0x482)]),_0x489518[_0x238f28(0x59f)](0x0),_0x489518['onended']=function(){var _0x4bc929=_0x238f28;_0x489518[_0x4bc929(0xa63)](),_0x2e67e7(_0x89d91a),_0x295ae4[_0x4bc929(0xb08)]=![],_0x49b598['length']&&_0x4fb45e();},_0x489518[_0x238f28(0x638)](_0xab9a9c['audioCtx'][_0x238f28(0x674)]+_0x501144/0x3e8);}return{'enqueue'(_0x4d46cc){var _0x23bca1=_0x2b9f;_0x49b598['push'](_0x4d46cc),_0x2fcb80[_0x23bca1(0xa55)][_0x23bca1(0x9fc)]!==_0x49b598&&(_0x2fcb80[_0x23bca1(0xa55)][_0x23bca1(0x9fc)]=_0x49b598),_0x4fb45e();}};});if(_0x2fcb80[_0x4ebc02(0x61c)]&&_0x2fcb80[_0x4ebc02(0x7eb)]['stopWriter']){if(!_0x2fcb80['video'][_0x4ebc02(0xc96)]&&_0x12af27[_0x4ebc02(0x3c5)]!==_0x4ebc02(0xa27))log(_0x4ebc02(0xc9f)),log(_0x12af27),!_0x2fcb80[_0x4ebc02(0x822)]&&(_0x2fcb80['dc'][_0x4ebc02(0x4c6)](JSON['stringify']({'kf':!![]})),_0x2fcb80[_0x4ebc02(0x822)]=setTimeout(function(){var _0x19d4c6=_0x4ebc02;clearTimeout(_0x2fcb80[_0x19d4c6(0x822)]),_0x2fcb80[_0x19d4c6(0x822)]=null;},0x3e8));else!_0x2fcb80['video']['header']?(_0x2fcb80['video']['header']=Date[_0x4ebc02(0x30e)](),_0x2fcb80[_0x4ebc02(0x61c)]['addFrame'](_0x12af27),log('start\x20writing\x20frames'),_0xab9a9c[_0x4ebc02(0x827)]&&!_0x2fcb80[_0x4ebc02(0x26b)]&&(_0x2fcb80['updateTime']=setInterval(function(_0x21fc70){var _0x4a9c70=_0x4ebc02,_0x4d3592=(Date[_0x4a9c70(0x30e)]()-_0x2fcb80[_0x4a9c70(0xa55)]['header'])/0x3e8,_0x2d1332=Math['floor'](_0x4d3592/0x3c),_0x3b3062=Math['floor'](_0x4d3592-_0x2d1332*0x3c);try{document[_0x4a9c70(0xac5)](_0x4a9c70(0x848)+_0x21fc70+'\x27]')[_0x4a9c70(0x247)]='\x20'+_0x2d1332+_0x4a9c70(0x930)+zpadTime(_0x3b3062)+'s';}catch(_0xc74199){log(_0x4a9c70(0x507));}},0x3e8,_0x2fcb80[_0x4ebc02(0xab9)]))):_0x2fcb80[_0x4ebc02(0x61c)]['addFrame'](_0x12af27);}_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x87b)]&&_0x2fcb80[_0x4ebc02(0xa55)]&&_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0xbba)][_0x4ebc02(0x4e0)]===_0x4ebc02(0x5e8)&&(_0x2fcb80[_0x4ebc02(0x9d2)]+=0x1,warnlog(_0x4ebc02(0x358)),_0x2fcb80[_0x4ebc02(0xa55)]['playbackheader']=![],_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0xbba)]=new VideoDecoder(_0x2fcb80['init_video']),await _0x2fcb80['video'][_0x4ebc02(0xbba)][_0x4ebc02(0x8e5)](_0x2fcb80[_0x4ebc02(0xa22)]),_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x87b)]=![]);if(_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x87b)]||_0x12af27[_0x4ebc02(0x3c5)]===_0x4ebc02(0xa27)){_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x87b)]=!![];if(_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0xac7)]){try{!_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x80f)]&&(_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x80f)]=_0x2fcb80[_0x4ebc02(0x505)](_0xedeebf)),_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x80f)]['enqueue'](_0x12af27);}catch(_0x192ece){errorlog(_0x192ece),_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x80f)]=null;}return;}try{if(_0x2fcb80['video']['nextQueue'])_0x2fcb80['video'][_0x4ebc02(0x9fc)][_0x4ebc02(0x5e7)](_0x12af27);else{if(_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x9fc)][_0x4ebc02(0xade)])_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x9fc)][_0x4ebc02(0x5e7)](_0x12af27);else{if(_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0xac7)]){_0x2fcb80['video'][_0x4ebc02(0x699)]=!![];function _0x3d451c(_0x3086ae){var _0x4c37e6=_0x4ebc02,_0x2ec6a9=_0x2fcb80['video_session'],_0x5c0631=_0x3086ae[_0x4c37e6(0x9f5)]/0x3e8-(Date['now']()-_0x2fcb80[_0x4c37e6(0x4de)]-_0x2fcb80[_0x4c37e6(0xa55)][_0x4c37e6(0xac7)]),_0x3a4eb6=0xc8;if(!_0xab9a9c['rpcs'][_0x2fcb80[_0x4c37e6(0xab9)]]){clearTimeout(_0x2fcb80[_0x4c37e6(0xa55)][_0x4c37e6(0x699)]),_0x2fcb80[_0x4c37e6(0xa55)][_0x4c37e6(0x699)]=null,_0x2fcb80[_0x4c37e6(0xa55)]['queue']=[];return;}else{if(_0xab9a9c[_0x4c37e6(0x404)][_0x2fcb80[_0x4c37e6(0xab9)]][_0x4c37e6(0x657)]!==![])_0x3a4eb6=_0xab9a9c['rpcs'][_0x2fcb80[_0x4c37e6(0xab9)]]['buffer'];else _0xab9a9c[_0x4c37e6(0x657)]!==![]?_0x3a4eb6=_0xab9a9c[_0x4c37e6(0x657)]:_0xab9a9c['rpcs'][_0x2fcb80['UUID']]['buffer']=_0x3a4eb6;}_0x5c0631+=_0x3a4eb6,!_0xab9a9c[_0x4c37e6(0x404)][_0x2fcb80[_0x4c37e6(0xab9)]][_0x4c37e6(0x287)]['chunked_mode_video']&&(_0xab9a9c[_0x4c37e6(0x404)][_0x2fcb80[_0x4c37e6(0xab9)]][_0x4c37e6(0x287)][_0x4c37e6(0x3bb)]={}),_0xab9a9c[_0x4c37e6(0x404)][_0x2fcb80[_0x4c37e6(0xab9)]][_0x4c37e6(0x287)]['chunked_mode_video']['buffer_delta']=parseInt(_0x5c0631),_0xab9a9c[_0x4c37e6(0x404)][_0x2fcb80['UUID']][_0x4c37e6(0x287)][_0x4c37e6(0x3bb)][_0x4c37e6(0x54e)]=parseInt(_0x3a4eb6),_0xab9a9c[_0x4c37e6(0x404)][_0x2fcb80[_0x4c37e6(0xab9)]][_0x4c37e6(0x287)][_0x4c37e6(0x3bb)][_0x4c37e6(0xb83)]=_0x3086ae[_0x4c37e6(0x9f5)]+':'+(Date[_0x4c37e6(0x30e)]()-_0x2fcb80['timedelta']-_0x2fcb80['video']['realTime'])+':'+Date[_0x4c37e6(0x30e)]()+':'+_0x2fcb80['timedelta']+':'+_0x2fcb80['video']['realTime'],!_0xab9a9c[_0x4c37e6(0x482)]&&(_0xab9a9c[_0x4c37e6(0x482)]=_0xab9a9c[_0x4c37e6(0x44a)]['createGain'](),_0xab9a9c[_0x4c37e6(0x482)][_0x4c37e6(0x93f)][_0x4c37e6(0xb58)]=0x0,_0xab9a9c['silence']['connect'](_0xab9a9c[_0x4c37e6(0x44a)][_0x4c37e6(0x5bb)])),!_0x2fcb80[_0x4c37e6(0xaeb)]&&(_0x5c0631<=0x0&&(_0x5c0631=0x0),_0x2fcb80[_0x4c37e6(0xaeb)]=_0xab9a9c[_0x4c37e6(0x44a)][_0x4c37e6(0x9aa)](),_0x2fcb80[_0x4c37e6(0xaeb)]['connect'](_0xab9a9c[_0x4c37e6(0x482)]),_0x2fcb80[_0x4c37e6(0xaeb)][_0x4c37e6(0x59f)](0x0),_0x2fcb80[_0x4c37e6(0xaeb)][_0x4c37e6(0x5c2)]=_0x5eba27=>{var _0x1ec977=_0x4c37e6;_0x2fcb80[_0x1ec977(0xaeb)][_0x1ec977(0xa63)]();if(_0x2ec6a9===_0x2fcb80[_0x1ec977(0x9d2)])try{_0x2fcb80['video'][_0x1ec977(0xbba)][_0x1ec977(0x4ba)](_0x3086ae);}catch(_0x450047){errorlog(_0x450047);}else console[_0x1ec977(0x440)](_0x2ec6a9,_0x2fcb80[_0x1ec977(0x9d2)]);_0x2fcb80[_0x1ec977(0xaeb)]=![],_0x2fcb80['video'][_0x1ec977(0x9fc)]['length']?_0x3d451c(_0x2fcb80['video'][_0x1ec977(0x9fc)][_0x1ec977(0xa17)]()):_0x2fcb80[_0x1ec977(0xa55)][_0x1ec977(0x699)]=null;},_0x2fcb80[_0x4c37e6(0xaeb)][_0x4c37e6(0x638)](_0xab9a9c[_0x4c37e6(0x44a)][_0x4c37e6(0x674)]+_0x5c0631/0x3e8));}try{_0x3d451c(_0x12af27);}catch(_0xddcf31){errorlog(_0xddcf31),_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x699)]=null,!_0x2fcb80[_0x4ebc02(0x822)]&&(_0x2fcb80['dc'][_0x4ebc02(0x4c6)](JSON[_0x4ebc02(0x6d1)]({'kf':!![]})),_0x2fcb80[_0x4ebc02(0x822)]=setTimeout(function(){var _0x4c5aa7=_0x4ebc02;clearTimeout(_0x2fcb80['requestKeyframe']),_0x2fcb80[_0x4c5aa7(0x822)]=null;},0x3e8));}}else try{_0x2fcb80['video'][_0x4ebc02(0xbba)][_0x4ebc02(0x4ba)](_0x12af27);}catch(_0x1695b1){errorlog(_0x1695b1);}}}}catch(_0x3ce7a8){errorlog(_0x3ce7a8),_0x2fcb80[_0x4ebc02(0xa55)][_0x4ebc02(0x87b)]=![];}}_0x2fcb80[_0x4ebc02(0xa55)]['decoder'][_0x4ebc02(0x527)]&&console['log'](_0x4ebc02(0x45f)+_0x2fcb80[_0x4ebc02(0xa55)]['decoder'][_0x4ebc02(0x527)]),!_0x2fcb80[_0x4ebc02(0xa55)]['playbackheader']&&(!_0x2fcb80[_0x4ebc02(0x822)]&&(_0x2fcb80['dc']['send'](JSON['stringify']({'kf':!![]})),_0x2fcb80[_0x4ebc02(0x822)]=setTimeout(function(){var _0x471842=_0x4ebc02;clearTimeout(_0x2fcb80[_0x471842(0x822)]),_0x2fcb80['requestKeyframe']=null;},0x3e8)));},_0x2fcb80['processFrameAudio']=async function(_0x4b464f){var _0x8383d8=_0x595383;if(!_0x2fcb80[_0x8383d8(0x544)]){errorlog('Audio\x20isn\x27t\x20setup\x20yet.');return;}try{_0x4b464f['type']='key',_0x4b464f=new EncodedAudioChunk(_0x4b464f);}catch(_0x6dcd0f){return;}_0x2fcb80[_0x8383d8(0x61c)]&&_0x2fcb80['video'][_0x8383d8(0xc96)]&&_0x2fcb80[_0x8383d8(0x7eb)][_0x8383d8(0x423)]&&_0x2fcb80['videoWriter'][_0x8383d8(0x7d8)](_0x4b464f);_0x2fcb80[_0x8383d8(0x544)][_0x8383d8(0xbba)][_0x8383d8(0x4e0)]==='closed'&&(_0x2fcb80['audio']['decoder']=new AudioDecoder(_0x2fcb80[_0x8383d8(0x759)]),_0x2fcb80['audio'][_0x8383d8(0xbba)][_0x8383d8(0x8e5)](_0x2fcb80[_0x8383d8(0x896)]));try{_0x2fcb80[_0x8383d8(0x544)][_0x8383d8(0xbba)][_0x8383d8(0x4ba)](_0x4b464f);}catch(_0x54b2c7){errorlog(_0x54b2c7);}};}else{if(_0x2fcb80['audio']&&_0x157435[_0x595383(0xc14)])_0x2fcb80[_0x595383(0x544)]['realTime']=_0x157435[_0x595383(0xc14)];else _0x2fcb80[_0x595383(0xa55)]&&_0x157435['realTimeVideo']?_0x2fcb80[_0x595383(0xa55)][_0x595383(0xac7)]=_0x157435[_0x595383(0x52e)]:errorlog(_0x349730);}return;}catch(_0x32f239){errorlog(_0x32f239);}else _0xab9a9c[_0x595383(0x218)]&&(_0xab9a9c[_0x595383(0x2ca)][_0x595383(0x5e7)](_0x20b60a[_0x595383(0x67b)]),_0xab9a9c[_0x595383(0x218)]&&_0xab9a9c['retransmitChunkedStream']());try{const _0x1784ea=_0x20b60a[_0x595383(0x67b)];if(typeof _0x1784ea===_0x595383(0x9cb))_0x366b05(JSON[_0x595383(0xa4b)](_0x1784ea));else _0x1784ea&&await _0x4d5027(_0x1784ea);}catch(_0x25c561){errorlog(_0x25c561);}};return;},_0xab9a9c[_0xf92ab0(0x7f2)]=function(){var _0x4f42c8=_0xf92ab0;const _0x20e818=new Set([...Object[_0x4f42c8(0x7db)](_0xab9a9c[_0x4f42c8(0x84f)]),...Object[_0x4f42c8(0x7db)](_0xab9a9c[_0x4f42c8(0x404)])]),_0x4982ae=_0xab9a9c['directorList'][_0x4f42c8(0xade)];_0xab9a9c['directorList']=_0xab9a9c['directorList']['filter'](_0x49ab16=>_0x20e818[_0x4f42c8(0x2f5)](_0x49ab16));const _0x1d60ae=_0x4982ae-_0xab9a9c[_0x4f42c8(0x8a3)][_0x4f42c8(0xade)];_0x1d60ae&&log(_0x4f42c8(0x57c)+_0x1d60ae+_0x4f42c8(0xcdb));},_0xab9a9c['setupIncoming']=async function(_0x39b72e){var _0x27ae7a=_0xf92ab0;log('SETUP\x20INCOMING');var _0x1b5982=_0x39b72e[_0x27ae7a(0xab9)];if(_0x1b5982 in _0xab9a9c[_0x27ae7a(0x404)]){if(_0x27ae7a(0x94a)in _0x39b72e&&_0x39b72e[_0x27ae7a(0x94a)]){if(_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['session']==_0x39b72e[_0x27ae7a(0x94a)]){log(_0x27ae7a(0x309));return;}warnlog('already\x20connected\x201'),_0xab9a9c[_0x27ae7a(0x35f)](_0x1b5982,![],!![])||![];}}else log(_0x27ae7a(0xa9d));try{for(var _0x3b8813 in _0xab9a9c[_0x27ae7a(0x404)]){_0xab9a9c[_0x27ae7a(0x404)][_0x3b8813][_0x27ae7a(0xa96)]==_0x39b72e[_0x27ae7a(0xa96)]&&(_0xab9a9c[_0x27ae7a(0x404)][_0x3b8813][_0x27ae7a(0x625)]&&errorlog(_0x27ae7a(0xb69)),_0xab9a9c[_0x27ae7a(0x404)][_0x3b8813][_0x27ae7a(0x7eb)]&&(_0xab9a9c[_0x27ae7a(0x404)][_0x3b8813][_0x27ae7a(0x7eb)][_0x27ae7a(0x1b0)][_0x27ae7a(0x375)]=_0x27ae7a(0xcd8)),warnlog(_0x27ae7a(0x248)),_0xab9a9c[_0x27ae7a(0x35f)](_0x3b8813),_0x3b8813!==_0x1b5982&&(_0x3b8813 in _0xab9a9c[_0x27ae7a(0x84f)]&&(_0x39b72e[_0x27ae7a(0x94a)]&&_0x39b72e['session'][_0x27ae7a(0x7a4)](0x0,0x6)!==_0xab9a9c[_0x27ae7a(0xc19)]?(warnlog('CLOSING\x20SECONDARY\x20CONNECTION;\x20matched\x20stream\x20ID\x20has\x20re-connected'),log('closing\x2020'),_0xab9a9c[_0x27ae7a(0x219)](_0x3b8813,![])):warnlog(_0x27ae7a(0x7e6)))));}document['getElementById'](_0x27ae7a(0x350))&&(document[_0x27ae7a(0x2b2)]('mainmenu')[_0x27ae7a(0x3d1)][_0x27ae7a(0x5be)](document[_0x27ae7a(0x2b2)](_0x27ae7a(0x350))),document['querySelectorAll'](_0x27ae7a(0x44d))[_0x27ae7a(0x306)](_0x38bfe8=>{var _0x458e90=_0x27ae7a;_0x38bfe8['classList'][_0x458e90(0x50f)](_0x458e90(0x852));}));}catch(_0x5bee09){errorlog(_0x5bee09);}if(_0xab9a9c['shadowBanned']){log('Shadow\x20mode:\x20ignoring\x20incoming\x20connection\x20from\x20'+_0x1b5982);return;}if(_0xab9a9c[_0x27ae7a(0x777)]!==![]){if(Object[_0x27ae7a(0x7db)](_0xab9a9c['rpcs'])['length']>=_0xab9a9c[_0x27ae7a(0x777)]){warnlog(_0x27ae7a(0xb55));return;}}else{if(_0xab9a9c[_0x27ae7a(0xb6c)]!==![]){if(Object['keys'](_0xab9a9c[_0x27ae7a(0x404)])[_0x27ae7a(0xade)]+Object[_0x27ae7a(0x7db)](_0xab9a9c[_0x27ae7a(0x84f)])[_0x27ae7a(0xade)]>=_0xab9a9c[_0x27ae7a(0xb6c)]){warnlog(_0x27ae7a(0xb55));return;}}}if(_0xab9a9c['queue']){if(_0xab9a9c[_0x27ae7a(0x827)])!(_0x1b5982 in _0xab9a9c['pcs'])&&_0xab9a9c[_0x27ae7a(0x2cb)](_0x1b5982);else{if(_0xab9a9c['directorList'][_0x27ae7a(0x565)](_0x1b5982)==-0x1){if(!(_0x39b72e['streamID']&&_0xab9a9c['view_set']&&_0xab9a9c[_0x27ae7a(0x4f1)]['includes'](_0x39b72e[_0x27ae7a(0xa96)])))return;}}}!_0xab9a9c[_0x27ae7a(0x7d3)]&&await chooseBestTURN();_0xab9a9c['encodedInsertableStreams']&&(_0xab9a9c['configuration'][_0x27ae7a(0x288)]=!![]);_0xab9a9c[_0x27ae7a(0x8a9)]&&(_0xab9a9c[_0x27ae7a(0x7d3)][_0x27ae7a(0x8a9)]=_0xab9a9c[_0x27ae7a(0x8a9)]);try{if(_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]&&_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['stashed']){let _0xb1f1d=new RTCPeerConnection(_0xab9a9c['configuration']);try{_0xb1f1d[_0x27ae7a(0xb14)]=_0x53c9bd=>{warnlog(_0x53c9bd);};}catch(_0x39d9fd){warnlog(_0x39d9fd);}var _0x1c98ab=Object['keys'](_0xab9a9c['rpcs'][_0x1b5982]);for(var _0x3b8813=0x0;_0x3b8813<_0x1c98ab[_0x27ae7a(0xade)];_0x3b8813++){var _0x3c40bb=_0x1c98ab[_0x3b8813];if(_0xb1f1d['hasOwnProperty'](_0x3c40bb))continue;_0xb1f1d[_0x3c40bb]=_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x3c40bb],log(_0x27ae7a(0x7e0)+_0x3c40bb);}_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]=_0xb1f1d;}else{_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]=new RTCPeerConnection(_0xab9a9c['configuration']);try{_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xb14)]=_0x27a68b=>{warnlog(_0x27a68b);};}catch(_0x5e605c){warnlog(_0x5e605c);}}if(_0xab9a9c['requireencryption']&&!_0x39b72e[_0x27ae7a(0xa85)]){errorlog(_0x27ae7a(0x4e7)),errorlog(_0x39b72e);return;}else{if(!_0x39b72e[_0x27ae7a(0xa85)]&&!_0xab9a9c[_0x27ae7a(0xa73)]&&_0xab9a9c[_0x27ae7a(0x54c)]&&!_0xab9a9c[_0x27ae7a(0x4c3)]){errorlog(_0x27ae7a(0x807)),errorlog(_0x39b72e);return;}}}catch(_0xa3035c){!_0xab9a9c[_0x27ae7a(0xca4)]&&warnUser(_0x27ae7a(0x3e8));errorlog(_0xa3035c);return;}!_0x39b72e[_0x27ae7a(0xa85)]?(_0xab9a9c['password']&&_0xab9a9c['defaultPassword']&&(warnlog(_0x27ae7a(0x925)),warnlog(_0x39b72e)),_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa85)]=![]):(!_0xab9a9c[_0x27ae7a(0x54c)]&&(errorlog(_0x27ae7a(0x921)),errorlog(_0x39b72e)),_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa85)]=!![]);if(_0xab9a9c['security']){if(Object['keys'](_0xab9a9c[_0x27ae7a(0x404)])['length']>0x1){warnlog(_0x27ae7a(0x4df)),log(_0xab9a9c[_0x27ae7a(0x404)]),delete _0xab9a9c[_0x27ae7a(0x404)][_0x1b5982],updateUserList();return;}else warnlog(_0x27ae7a(0x2a1));}_0x39b72e[_0x27ae7a(0xa96)]in _0xab9a9c['waitingWatchList']&&(log(_0x27ae7a(0x438)),delete _0xab9a9c[_0x27ae7a(0x2f4)][_0x39b72e[_0x27ae7a(0xa96)]]);try{_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa96)]=_0x39b72e[_0x27ae7a(0xa96)];const _0x514ead=_0x1b5982+_0x27ae7a(0x6f3);if(_0x514ead in _0xab9a9c[_0x27ae7a(0x404)]){const _0x10686b=_0xab9a9c[_0x27ae7a(0x404)][_0x514ead],_0x5b94ce=_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa96)];if(_0x5b94ce){const _0x31a066=_0x5b94ce+':s';_0x10686b[_0x27ae7a(0xa96)]!==_0x31a066&&(_0x10686b[_0x27ae7a(0xa96)]=_0x31a066),_0x10686b['videoElement']&&(_0x10686b[_0x27ae7a(0x7eb)][_0x27ae7a(0xa18)][_0x27ae7a(0x2e8)]=_0x31a066),_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa3b)]&&_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['screenElement']!==_0x10686b['videoElement']&&(_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa3b)]['dataset'][_0x27ae7a(0x2e8)]=_0x31a066);}}await checkDirectorStreamID();}catch(_0x193470){errorlog(_0x193470);return;}_0x39b72e['session']?_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x94a)]=_0x39b72e[_0x27ae7a(0x94a)]:_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x94a)]=null;_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x80e)]=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x971)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x23c)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x888)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0xc38)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['allowGraphs']=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x9e1)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x287)]={},_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x287)][_0x27ae7a(0x8c1)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x990)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x9f4)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xc05)]=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x3ce)]=-0x1,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x3ee)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x657)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x4a7)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x3b8)]=![],_0xab9a9c['rpcs'][_0x1b5982]['targetBandwidth']=-0x1,_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x9b6)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x7eb)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['imageElement']=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xb1c)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x34e)]=[],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x6cc)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x33c)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x458)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xcd7)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['virtualHangup']=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['remoteMuteState']=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x5bc)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x808)]=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x97e)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['mutedState']=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x25e)]=null,_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0xaa5)]=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x4d6)]=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['flipState']=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xb96)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['rotate']=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x3d6)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x447)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['scaleWidth']=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x1b6)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x4c7)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x7c4)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xca7)]=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa65)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xc15)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa88)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x8d5)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x827)]=null,_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x8b4)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x9eb)]=0x64,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x45d)]=0x0,_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x87e)]=0x0,_0xab9a9c['rpcs'][_0x1b5982]['settings']=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x1d1)]='1',_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['opacityMuted']='1',_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x1e6)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['pliCount']=0x0,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x5e0)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['meta']=![],_0xab9a9c['rpcs'][_0x1b5982]['order']=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x6ed)]=null,_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x6f9)]=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xc53)]={},_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x7dc)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x7ca)]=![],_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0x334)]=Date['now'](),_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xcb0)]=![],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x649)]=_0xab9a9c[_0x27ae7a(0x649)];(_0xab9a9c['activeSpeaker']==0x2||_0xab9a9c['activeSpeaker']==0x4)&&(_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x888)]=!![]);if(_0xab9a9c[_0x27ae7a(0xb21)]){var _0x1d6142=createRichVideoElement(_0x1b5982);_0x1d6142[_0x27ae7a(0x1b0)][_0x27ae7a(0x375)]=_0x27ae7a(0x2ed);}if(_0xab9a9c[_0x27ae7a(0x827)]){if(_0xab9a9c['customWSS']&&'isScene'in _0x39b72e&&_0x39b72e['isScene']!==![]){}else{var _0x2c6e8d=soloLinkGenerator(_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['streamID']);_0x27ae7a(0x96d)in _0x39b72e?createControlBox(_0x1b5982,_0x2c6e8d,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa96)],_0x39b72e[_0x27ae7a(0x96d)]):createControlBox(_0x1b5982,_0x2c6e8d,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['streamID']);_0xab9a9c[_0x27ae7a(0x6cf)]&&_0xab9a9c[_0x27ae7a(0x6cf)][_0x27ae7a(0xade)]&&setTimeout(function(){autoAssignAudioChannel(_0x1b5982);},0x64);if(_0xab9a9c[_0x27ae7a(0x442)]&&_0xab9a9c[_0x27ae7a(0x1dd)][_0x27ae7a(0xa4f)](_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa96)])){var _0x30cc59=_0xab9a9c[_0x27ae7a(0x1dd)]['indexOf'](_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa96)]);_0x30cc59>-0x1&&_0xab9a9c[_0x27ae7a(0x1dd)][_0x27ae7a(0x954)](_0x30cc59,0x1),setTimeout(function(){var _0x34e5fe=_0x27ae7a;_0x3447a7(_0x1b5982),_0xab9a9c[_0x34e5fe(0x55c)](_0x1b5982);},0x64);}}}_0xab9a9c['rpcs'][_0x1b5982][_0x27ae7a(0xab9)]=_0x1b5982;try{if(_0xab9a9c[_0x27ae7a(0x4f1)]){if(_0xab9a9c[_0x27ae7a(0x4f1)][_0x27ae7a(0xa4f)](_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa96)])){if(_0xab9a9c[_0x27ae7a(0x4b2)]!==![]){let _0x3fa87c=_0xab9a9c['view_set']['indexOf'](_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xa96)]);_0xab9a9c[_0x27ae7a(0x4b2)]['length']>_0x3fa87c&&(_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['manualBandwidth']=parseInt(_0xab9a9c[_0x27ae7a(0x4b2)][_0x3fa87c]),_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x9b6)]<=0x0&&(_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x9b6)]=![]));}}}}catch(_0x3031fa){errorlog(_0x3031fa);}_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x38c)]=function(_0x43e85e){var _0x1a18f6=_0x27ae7a;log(_0x1a18f6(0x34c)),_0xab9a9c[_0x1a18f6(0x35f)](_0x1b5982);},_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0xbaa)]=null,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['iceBundle']=[],_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x9f8)]=0xa,_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x7c1)]=function(_0x3c988f){var _0x44366a=_0x27ae7a;if(_0x3c988f[_0x44366a(0x4f8)]==null){log(_0x44366a(0x262));if(_0xab9a9c[_0x44366a(0x404)][_0x1b5982]&&_0xab9a9c['rpcs'][_0x1b5982]['whipCallback2']){var _0x1ff6f1=[..._0xab9a9c[_0x44366a(0x404)][_0x1b5982]['iceBundle']];try{if(_0xab9a9c[_0x44366a(0x64b)]){var _0xa8e4a2=filterIpv6FromCandidates(_0x1ff6f1);_0x1ff6f1=_0xa8e4a2[_0x44366a(0x2a4)];}else _0xab9a9c[_0x44366a(0x823)]!==![]&&(_0x1ff6f1=reorderCandidatesIpv4First(_0x1ff6f1));}catch(_0x4889e8){warnlog(_0x44366a(0x71e),_0x4889e8);}_0xab9a9c[_0x44366a(0x404)][_0x1b5982][_0x44366a(0x92a)](_0x1ff6f1),clearTimeout(_0xab9a9c['rpcs'][_0x1b5982][_0x44366a(0xbaa)]),_0xab9a9c[_0x44366a(0x404)][_0x1b5982]['iceTimer']=null,_0xab9a9c[_0x44366a(0x404)][_0x1b5982]['iceBundle']=[],_0xab9a9c[_0x44366a(0x404)][_0x1b5982]['whipCallback2']=null,console[_0x44366a(0x440)](_0x44366a(0xa6d));}return;}try{if(_0xab9a9c[_0x44366a(0x2e3)]){if(_0x3c988f[_0x44366a(0x4f8)][_0x44366a(0x4f8)][_0x44366a(0x565)](_0xab9a9c[_0x44366a(0x2e3)])===-0x1){log(_0x44366a(0x8b7));return;}else log(_0x3c988f[_0x44366a(0x4f8)]);}}catch(_0x3db4f8){errorlog(_0x3db4f8);}try{if(_0xab9a9c['localNetworkOnly']){if(!filterIceLAN(_0x3c988f[_0x44366a(0x4f8)]))return;}if(_0xab9a9c['stunOnly']){if(!filterStunOnly(_0x3c988f[_0x44366a(0x4f8)]))return;}}catch(_0x4e4f6c){errorlog(_0x4e4f6c);}_0xab9a9c['rpcs'][_0x1b5982][_0x44366a(0x397)][_0x44366a(0x5e7)](_0x3c988f[_0x44366a(0x4f8)]);if(_0xab9a9c[_0x44366a(0x404)][_0x1b5982]&&(_0xab9a9c[_0x44366a(0x404)][_0x1b5982][_0x44366a(0x92a)]||_0xab9a9c[_0x44366a(0x404)][_0x1b5982]['iceTimer']!==null))return;_0xab9a9c[_0x44366a(0x404)][_0x1b5982]['iceTimer']=setTimeout(function(_0x5a6325){var _0x516623=_0x44366a;if(!(_0x5a6325 in _0xab9a9c['rpcs']))return;if(_0xab9a9c[_0x516623(0x404)][_0x5a6325][_0x516623(0x92a)])return;_0xab9a9c['rpcs'][_0x5a6325][_0x516623(0xbaa)]=null;if(!_0xab9a9c[_0x516623(0x404)][_0x5a6325][_0x516623(0x397)]||!_0xab9a9c['rpcs'][_0x5a6325][_0x516623(0x397)][_0x516623(0xade)]){errorlog(_0x516623(0x277));return;}var _0x1e8f6a={};_0x1e8f6a[_0x516623(0xab9)]=_0x5a6325,_0x1e8f6a[_0x516623(0x3c5)]='remote';var _0x424853=_0xab9a9c['rpcs'][_0x5a6325][_0x516623(0x397)];try{if(_0xab9a9c['disableIpv6']){var _0xbb4b84=filterIpv6FromCandidates(_0x424853);_0x424853=_0xbb4b84[_0x516623(0x2a4)];}else _0xab9a9c[_0x516623(0x823)]!==![]&&(_0x424853=reorderCandidatesIpv4First(_0x424853));}catch(_0x4bd497){warnlog(_0x516623(0x587),_0x4bd497);}_0x1e8f6a[_0x516623(0x460)]=_0x424853,_0x1e8f6a[_0x516623(0x94a)]=_0xab9a9c[_0x516623(0x404)][_0x5a6325]['session'],_0xab9a9c[_0x516623(0x404)][_0x5a6325][_0x516623(0x397)]=[],_0xab9a9c['rpcs'][_0x5a6325][_0x516623(0x9f8)]=0x3e8;if(_0xab9a9c[_0x516623(0x404)][_0x5a6325][_0x516623(0x625)])return;_0xab9a9c[_0x516623(0x54c)]&&_0xab9a9c[_0x516623(0x404)][_0x5a6325][_0x516623(0xa85)]?_0xab9a9c[_0x516623(0xab5)](JSON[_0x516623(0x6d1)](_0x1e8f6a[_0x516623(0x460)]))['then'](function(_0x1c138c){var _0x5714eb=_0x516623;_0x1e8f6a['candidates']=_0x1c138c[0x0],_0x1e8f6a[_0x5714eb(0xa85)]=_0x1c138c[0x1],_0xab9a9c[_0x5714eb(0x3fb)](_0x1e8f6a);})[_0x516623(0xacc)](errorlog):_0xab9a9c[_0x516623(0x3fb)](_0x1e8f6a);},_0xab9a9c[_0x44366a(0x404)][_0x1b5982][_0x44366a(0x9f8)],_0x1b5982);},_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x99e)]=function(_0x39f071){var _0x5068f7=_0x27ae7a;switch(this[_0x5068f7(0xa83)]){case'new':log('new'),log(_0x5068f7(0x5d9)),clearInterval(_0xab9a9c[_0x5068f7(0x404)][this['UUID']][_0x5068f7(0x808)]);case _0x5068f7(0x937):log(_0x5068f7(0x937)),log(_0x5068f7(0xbbf)),clearInterval(_0xab9a9c['rpcs'][this['UUID']][_0x5068f7(0x808)]);case _0x5068f7(0x497):log(_0x5068f7(0x49c)),log(_0x5068f7(0x425)),clearInterval(_0xab9a9c['rpcs'][this[_0x5068f7(0xab9)]]['closeTimeout']);if(_0xab9a9c[_0x5068f7(0x915)]){if(_0xab9a9c['ws'][_0x5068f7(0x70b)]!==0x1){_0xab9a9c['ws'][_0x5068f7(0x3ef)]();break;}_0xab9a9c['ws'][_0x5068f7(0x3ef)](),setTimeout(function(){var _0x4bb77d=_0x5068f7;_0xab9a9c[_0x4bb77d(0xca4)]!=!![]&&warnUser(getTranslation(_0x4bb77d(0x692)));},0x1);}break;case _0x5068f7(0x922):log(_0x5068f7(0x8cd)),warnlog(_0x5068f7(0x282));if(this[_0x5068f7(0xab9)]in _0xab9a9c['rpcs']){clearInterval(_0xab9a9c[_0x5068f7(0x404)][this['UUID']][_0x5068f7(0x808)]),_0xab9a9c[_0x5068f7(0x404)][this['UUID']][_0x5068f7(0x9f8)]=0x0;if(_0xab9a9c[_0x5068f7(0x404)][this[_0x5068f7(0xab9)]][_0x5068f7(0xcb0)])return;try{const _0x122cd8=Date['now'](),_0x2646d3=this[_0x5068f7(0xab9)];_0xab9a9c['rpcs'][_0x2646d3]&&(_0xab9a9c['rpcs'][_0x2646d3]['lastPongToken']=undefined,_0xab9a9c['rpcs'][_0x2646d3]['lastPingToken']=_0x122cd8);try{_0xab9a9c[_0x5068f7(0x936)]({'ping':_0x122cd8},_0x2646d3);}catch(_0x59eb29){warnlog(_0x59eb29);}setTimeout(function(_0x641af1,_0x4e468f){var _0x5aa873=_0x5068f7;try{if(!(_0x641af1 in _0xab9a9c[_0x5aa873(0x404)]))return;if(_0xab9a9c[_0x5aa873(0x404)][_0x641af1][_0x5aa873(0xa83)]!==_0x5aa873(0x922))return;if(_0xab9a9c[_0x5aa873(0x404)][_0x641af1][_0x5aa873(0xa86)]!==_0x4e468f)try{_0xab9a9c['anyrequest']({'iceRestartRequest':!![],'UUID':_0x641af1});}catch(_0x3061cd){warnlog(_0x3061cd),errorlog(_0x3061cd);}}catch(_0x41e0c3){errorlog(_0x41e0c3);}},0xbb8,_0x2646d3,_0x122cd8);}catch(_0x518910){errorlog(_0x518910);}_0xab9a9c[_0x5068f7(0x404)][this[_0x5068f7(0xab9)]]['closeTimeout']=setTimeout(function(_0x5675f3){var _0x41ab3a=_0x5068f7;log(_0x41ab3a(0x693)),_0xab9a9c[_0x41ab3a(0x35f)](_0x5675f3);},0x1388,this[_0x5068f7(0xab9)]);}else log(_0x5068f7(0x3e9));break;case'failed':warnlog(_0x5068f7(0x3bd));if(this['UUID']in _0xab9a9c['rpcs']){clearInterval(_0xab9a9c[_0x5068f7(0x404)][this[_0x5068f7(0xab9)]][_0x5068f7(0x808)]),_0xab9a9c[_0x5068f7(0x404)][this[_0x5068f7(0xab9)]][_0x5068f7(0x9f8)]=0x0;try{var _0x275c29={'iceRestartRequest':!![],'UUID':this['UUID']};_0xab9a9c[_0x5068f7(0x3fb)](_0x275c29),log('Sent\x20ICE\x20restart\x20request\x20to\x20publisher\x20via\x20anyrequest');}catch(_0x2a96a1){errorlog(_0x2a96a1);}_0xab9a9c[_0x5068f7(0x404)][this[_0x5068f7(0xab9)]][_0x5068f7(0x808)]=setTimeout(function(_0x4888df){var _0x365feb=_0x5068f7;log(_0x365feb(0xa07)),_0xab9a9c[_0x365feb(0x35f)](_0x4888df);},0x7530,this[_0x5068f7(0xab9)]);}else log('UUID\x20not\x20found;\x20can\x27t\x20close.');break;case'closed':warnlog(_0x5068f7(0x6a8)),_0xab9a9c['closeRPC'](this[_0x5068f7(0xab9)]);break;default:log(_0x5068f7(0x8c9)),log(_0x5068f7(0x8d8)+this[_0x5068f7(0xa83)]),clearInterval(_0xab9a9c['rpcs'][this['UUID']][_0x5068f7(0x808)]);break;}},_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['onicegatheringstatechange']=function(_0x43ffeb){var _0x3761aa=_0x27ae7a;let _0x12541d=_0x43ffeb[_0x3761aa(0x357)];switch(_0x12541d[_0x3761aa(0x626)]){case _0x3761aa(0x6f1):log(_0x3761aa(0x40e));break;case'complete':log(_0x3761aa(0xc7f));if(_0xab9a9c[_0x3761aa(0x404)][_0x1b5982][_0x3761aa(0x92a)]){var _0x1369e8=[..._0xab9a9c[_0x3761aa(0x404)][_0x1b5982][_0x3761aa(0x397)]];try{if(_0xab9a9c[_0x3761aa(0x64b)]){var _0x1257c0=filterIpv6FromCandidates(_0x1369e8);_0x1369e8=_0x1257c0[_0x3761aa(0x2a4)];}else _0xab9a9c[_0x3761aa(0x823)]!==![]&&(_0x1369e8=reorderCandidatesIpv4First(_0x1369e8));}catch(_0x5b856a){warnlog(_0x3761aa(0x1e5),_0x5b856a);}_0xab9a9c[_0x3761aa(0x404)][_0x1b5982][_0x3761aa(0x92a)](_0x1369e8),clearTimeout(_0xab9a9c[_0x3761aa(0x404)][_0x1b5982][_0x3761aa(0xbaa)]),_0xab9a9c[_0x3761aa(0x404)][_0x1b5982][_0x3761aa(0xbaa)]=null,_0xab9a9c['rpcs'][_0x1b5982]['iceBundle']=[],_0xab9a9c[_0x3761aa(0x404)][_0x1b5982]['whipCallback2']=null;}break;}},_0xab9a9c['rpcs'][_0x1b5982]['oniceconnectionstatechange']=function(){var _0x327037=_0x27ae7a;try{if(this['iceConnectionState']==_0x327037(0x5e8))errorlog(_0x327037(0x21e));else{if(this[_0x327037(0x413)]==_0x327037(0x922)){if(_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0xcb0)])return;warnlog(_0x327037(0x492)),_0xab9a9c[_0x327037(0x404)][_0x1b5982]['opacityDisconnect']='0',_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0x7eb)][_0x327037(0x1b0)][_0x327037(0x477)]='0',_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0xb23)]=setTimeout(function(_0x3c6f7e){updateMixer();},0x1f4,_0x1b5982);}else this[_0x327037(0x413)]==_0x327037(0xa40)?errorlog(_0x327037(0x778)):(log(_0x327037(0x407)+this[_0x327037(0x413)]),_0xab9a9c[_0x327037(0x404)][_0x1b5982]['disconnectedTimeout']&&clearTimeout(_0xab9a9c['rpcs'][_0x1b5982][_0x327037(0xb23)]),_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0x7eb)]&&_0x327037(0x477)in _0xab9a9c[_0x327037(0x404)][_0x1b5982]['videoElement']['style']?_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0x1d1)]=='0'&&_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0x3e5)]=='1'?(_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0x7eb)][_0x327037(0x1b0)]['opacity']='1',_0xab9a9c['rpcs'][_0x1b5982][_0x327037(0x1d1)]='1',updateMixer()):_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0x1d1)]='1':_0xab9a9c[_0x327037(0x404)][_0x1b5982][_0x327037(0x1d1)]='1');}}catch(_0x436523){}},_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982]['ondatachannel']=function(_0xafe056){var _0x117013=_0x27ae7a;log(_0xafe056);if(_0xafe056[_0x117013(0xc8d)][_0x117013(0x5e0)]&&_0xafe056[_0x117013(0xc8d)]['label']!==_0x117013(0x5cc)){if(_0xab9a9c[_0x117013(0x30c)][_0x117013(0xa4f)](_0xab9a9c[_0x117013(0x404)][_0x1b5982][_0x117013(0xa96)]))return;if(_0xafe056['channel'][_0x117013(0x5e0)]===_0x117013(0x86f))_0xab9a9c[_0x117013(0x727)](_0x1b5982,_0xafe056[_0x117013(0xc8d)]);else _0xafe056['channel']['label']===_0x117013(0x35a)?_0xab9a9c[_0x117013(0x900)](_0x1b5982,_0xafe056[_0x117013(0xc8d)]):_0xab9a9c[_0x117013(0x6a7)](_0xab9a9c['rpcs'],_0x1b5982,_0xafe056[_0x117013(0xc8d)]);return;}_0xab9a9c['rpcs'][_0x1b5982][_0x117013(0x7d4)]=_0xafe056['channel'],_0xab9a9c[_0x117013(0x404)][_0x1b5982][_0x117013(0x7d4)][_0x117013(0xab9)]=_0x1b5982,_0xab9a9c[_0x117013(0x404)][_0x1b5982][_0x117013(0x7d4)][_0x117013(0x636)]=_0x5108b2=>{var _0x15e33f=_0x117013;_0x5108b2[_0x15e33f(0xca1)]&&_0x5108b2[_0x15e33f(0xca1)]['sctpCauseCode']&&_0x5108b2['error'][_0x15e33f(0x95d)]!==0xc&&warnlog(_0x5108b2),log(_0x15e33f(0x9b0)+_0x1b5982);},_0xab9a9c[_0x117013(0x404)][_0x1b5982][_0x117013(0x7d4)][_0x117013(0x97d)]=_0x126b0=>{var _0x14e5c7=_0x117013;_0xab9a9c[_0x14e5c7(0x404)][_0x1b5982]['delayIceSend']=0x0;var _0x2007bf={};_0x2007bf['downloads']=![],_0x2007bf[_0x14e5c7(0x873)]=![],_0x2007bf[_0x14e5c7(0xb51)]=![],_0x2007bf[_0x14e5c7(0x275)]=![],_0x2007bf[_0x14e5c7(0x8cf)]=![],_0x2007bf[_0x14e5c7(0x544)]=![],_0x2007bf[_0x14e5c7(0xa55)]=![],_0x2007bf[_0x14e5c7(0x4e2)]=![],_0x2007bf['allowwebp']=![],_0x2007bf[_0x14e5c7(0x8f1)]=![],_0x2007bf['allowscreenvideo']=![],_0x2007bf[_0x14e5c7(0xa99)]=![],_0x2007bf[_0x14e5c7(0xbae)]=![];_0xab9a9c[_0x14e5c7(0xa25)]&&(_0xab9a9c['audioCodec']===_0x14e5c7(0xa0c)||_0xab9a9c[_0x14e5c7(0xa25)]===_0x14e5c7(0x46e))&&(_0x2007bf[_0x14e5c7(0x3c8)]=_0xab9a9c['audioCodec']);const _0x3b268d=_0xab9a9c[_0x14e5c7(0x404)][_0x1b5982]['streamID']||'';let _0x21ff94=_0x3b268d;_0x21ff94&&_0x21ff94['endsWith'](':s')&&(_0x21ff94=_0x21ff94['slice'](0x0,-0x2));const _0x5e0ef3=!!_0x3b268d&&_0x3b268d[_0x14e5c7(0x361)](':s'),_0x3478bf=_0x21ff94?_0x21ff94+':s':'',_0x43944d=_0x3478bf||(_0x5e0ef3?_0x3b268d:''),_0x203e40=_0xab9a9c[_0x14e5c7(0xb3a)]!==![]&&_0xab9a9c['exclude']?_0xab9a9c[_0x14e5c7(0xb3a)]:null,_0x4c8d9f=!!(_0x203e40&&_0x21ff94&&_0x203e40[_0x14e5c7(0xa4f)](_0x21ff94)),_0x13126d=!!(_0x203e40&&_0x3478bf&&_0x203e40[_0x14e5c7(0xa4f)](_0x3478bf)),_0x1fcdd5=!!(_0x203e40&&_0x3b268d&&_0x203e40[_0x14e5c7(0xa4f)](_0x3b268d));try{let _0x18a2d1=!_0xab9a9c[_0x14e5c7(0x54a)],_0x5bbaa9=!_0xab9a9c[_0x14e5c7(0x54a)];const _0x15f21d=_0x304ade=>{var _0x1ffc93=_0x14e5c7;if(!_0x304ade||_0xab9a9c[_0x1ffc93(0xa45)]===![]||_0xab9a9c[_0x1ffc93(0xa45)]===!![])return![];if(_0xab9a9c['allowScreen']&&typeof _0xab9a9c[_0x1ffc93(0xa45)]['includes']===_0x1ffc93(0x32c))return _0xab9a9c[_0x1ffc93(0xa45)][_0x1ffc93(0xa4f)](_0x304ade);return![];};if(!_0xab9a9c[_0x14e5c7(0x54a)]&&_0xab9a9c[_0x14e5c7(0xa45)]!==![]){if(_0xab9a9c[_0x14e5c7(0xa45)]===!![])_0x5bbaa9=!![],_0x18a2d1=!![];else _0x15f21d(_0x3b268d)||_0x15f21d(_0x3478bf)||_0x15f21d(_0x21ff94)?(_0x5bbaa9=!![],_0x18a2d1=!![]):(_0x5bbaa9=![],_0x18a2d1=![]);}if(!_0xab9a9c['noScreenShare']){if(_0xab9a9c[_0x14e5c7(0x74f)]===!![])_0x18a2d1=!![];else _0xab9a9c[_0x14e5c7(0x74f)]===![]&&(_0x18a2d1=![]);if(_0xab9a9c[_0x14e5c7(0xb4f)]===!![])_0x5bbaa9=!![];else _0xab9a9c['screenAudioOverride']===![]&&(_0x5bbaa9=![]);}_0x2007bf['allowscreenvideo']=_0x18a2d1,_0x2007bf['allowscreenaudio']=_0x5bbaa9;if(_0x2007bf[_0x14e5c7(0x25a)]){if(_0xab9a9c['novideo']!==![])(!_0x43944d||!_0xab9a9c[_0x14e5c7(0x4fe)][_0x14e5c7(0xa4f)](_0x43944d))&&(_0x2007bf['allowscreenvideo']=![]);else{if(_0xab9a9c['broadcast']!==![]){if(_0xab9a9c[_0x14e5c7(0x4e2)]!==null)_0x3478bf&&_0x3478bf===_0xab9a9c[_0x14e5c7(0x4e2)]?_0x2007bf['broadcast']=!![]:_0x2007bf[_0x14e5c7(0x25a)]=![];else _0xab9a9c['directorUUID']&&(_0x1b5982===_0xab9a9c[_0x14e5c7(0xb38)]?_0x2007bf['broadcast']=!![]:_0x2007bf[_0x14e5c7(0x25a)]=![]);}else _0x13126d&&(_0x2007bf[_0x14e5c7(0x25a)]=![],_0x2007bf[_0x14e5c7(0x8f1)]=![]);}}if(_0x2007bf[_0x14e5c7(0x8f1)]){if(_0xab9a9c[_0x14e5c7(0x5aa)]!==![])(!_0x43944d||!_0xab9a9c[_0x14e5c7(0x5aa)][_0x14e5c7(0xa4f)](_0x43944d))&&(_0x2007bf[_0x14e5c7(0x8f1)]=![]);else _0xab9a9c['excludeaudio']&&(_0x43944d&&_0xab9a9c[_0x14e5c7(0xaa4)][_0x14e5c7(0xa4f)](_0x43944d)&&(_0x2007bf['allowscreenaudio']=![]));}}catch(_0x3d4a23){errorlog(_0x3d4a23);}try{if(_0xab9a9c['novideo']!==![]){if(_0x3b268d&&_0xab9a9c[_0x14e5c7(0x4fe)][_0x14e5c7(0xa4f)](_0x3b268d))_0x2007bf['video']=!![];else _0x3478bf&&_0xab9a9c[_0x14e5c7(0x4fe)][_0x14e5c7(0xa4f)](_0x3478bf)?_0x2007bf[_0x14e5c7(0xa55)]=_0x5e0ef3?!![]:0x2:_0x2007bf[_0x14e5c7(0xa55)]=![];}else{if(_0xab9a9c[_0x14e5c7(0x4e2)]!==![]){if(_0xab9a9c[_0x14e5c7(0x4e2)]!==null)_0x3b268d&&_0x3b268d===_0xab9a9c[_0x14e5c7(0x4e2)]?(_0x2007bf['broadcast']=!![],_0x2007bf['video']=!![]):_0x2007bf[_0x14e5c7(0xa55)]=![];else _0xab9a9c[_0x14e5c7(0xb38)]&&(_0x1b5982===_0xab9a9c[_0x14e5c7(0xb38)]?(_0x2007bf[_0x14e5c7(0x4e2)]=!![],_0x2007bf[_0x14e5c7(0xa55)]=!![]):_0x2007bf['video']=![]);}else _0x203e40?_0x4c8d9f||_0x1fcdd5?_0x2007bf[_0x14e5c7(0xa55)]=![]:(_0x2007bf['video']=!![],_0x13126d&&(_0x2007bf[_0x14e5c7(0x25a)]=![],_0x2007bf[_0x14e5c7(0x8f1)]=![])):_0x2007bf[_0x14e5c7(0xa55)]=!![];}if(_0xab9a9c[_0x14e5c7(0x5aa)]!==![]){if(_0x3b268d&&_0xab9a9c['noaudio'][_0x14e5c7(0xa4f)](_0x3b268d))_0x2007bf[_0x14e5c7(0x544)]=!![];else _0x3478bf&&_0xab9a9c[_0x14e5c7(0x5aa)][_0x14e5c7(0xa4f)](_0x3478bf)?_0x2007bf[_0x14e5c7(0x544)]=0x2:_0x2007bf[_0x14e5c7(0x544)]=![];}else _0xab9a9c[_0x14e5c7(0xaa4)]&&_0xab9a9c['excludeaudio'][_0x14e5c7(0xa4f)](_0x3b268d)?_0x2007bf['audio']=![]:_0x2007bf['audio']=!![];_0xab9a9c[_0x14e5c7(0x856)]&&_0xab9a9c[_0x14e5c7(0x8a3)][_0x14e5c7(0x565)](_0x1b5982)>=0x0&&(_0x2007bf[_0x14e5c7(0x544)]=![]);_0xab9a9c[_0x14e5c7(0x3a7)]&&_0xab9a9c[_0x14e5c7(0x8a3)][_0x14e5c7(0x565)](_0x1b5982)>=0x0&&(_0x2007bf[_0x14e5c7(0xa55)]=![]);_0xab9a9c[_0x14e5c7(0xb5f)]!==![]?_0xab9a9c['noiframe'][_0x14e5c7(0xa4f)](_0xab9a9c[_0x14e5c7(0x404)][_0x1b5982]['streamID'])?_0x2007bf[_0x14e5c7(0x275)]=!![]:_0x2007bf[_0x14e5c7(0x275)]=![]:_0x2007bf[_0x14e5c7(0x275)]=!![];if(_0xab9a9c[_0x14e5c7(0x892)]!==![])_0xab9a9c['noWidget'][_0x14e5c7(0xa4f)](_0xab9a9c[_0x14e5c7(0x404)][_0x1b5982][_0x14e5c7(0xa96)])?_0x2007bf[_0x14e5c7(0x8cf)]=!![]:_0x2007bf[_0x14e5c7(0x8cf)]=![];else{if(_0xab9a9c[_0x14e5c7(0x268)]!==![])_0x2007bf[_0x14e5c7(0x8cf)]=![];else _0xab9a9c[_0x14e5c7(0x7c7)]&&!_0xab9a9c[_0x14e5c7(0x827)]&&_0xab9a9c[_0x14e5c7(0x6d8)]===![]?_0x2007bf[_0x14e5c7(0x8cf)]=![]:_0x2007bf['widget']=!![];}_0xab9a9c['noMeshcast']&&(_0x2007bf[_0x14e5c7(0x46d)]=![]);if(_0xab9a9c['screenWhepPreference']===_0x14e5c7(0x305))_0x2007bf[_0x14e5c7(0x7ae)]=![],_0x2007bf[_0x14e5c7(0xaa3)]=![];else _0xab9a9c[_0x14e5c7(0x66e)]==='whep'&&(_0x2007bf[_0x14e5c7(0x25a)]=![],_0x2007bf[_0x14e5c7(0x8f1)]=![]);_0xab9a9c[_0x14e5c7(0x54b)]&&(_0x2007bf[_0x14e5c7(0x902)]=_0xab9a9c['hideDirector']),_0xab9a9c[_0x14e5c7(0x3ed)]!==![]&&(!_0xab9a9c[_0x14e5c7(0x3ed)]['includes'](_0xab9a9c['rpcs'][_0x1b5982][_0x14e5c7(0xa96)])&&(_0x2007bf[_0x14e5c7(0xa55)]=![],_0x2007bf[_0x14e5c7(0x544)]=![])),(_0xab9a9c[_0x14e5c7(0xcd0)]||_0xab9a9c[_0x14e5c7(0xa7a)]||_0xab9a9c[_0x14e5c7(0x23a)]||_0xab9a9c[_0x14e5c7(0x758)])&&(_0x2007bf[_0x14e5c7(0x873)]=_0xab9a9c[_0x14e5c7(0xcd0)]||_0xab9a9c['midiRemote']||_0xab9a9c[_0x14e5c7(0x23a)]||_0xab9a9c[_0x14e5c7(0x758)]),_0x2007bf[_0x14e5c7(0x214)]=!![],_0xab9a9c[_0x14e5c7(0xcd5)]&&(_0x2007bf[_0x14e5c7(0x214)]=![]),_0xab9a9c[_0x14e5c7(0x2f3)]?_0x2007bf[_0x14e5c7(0xa99)]=![]:_0x2007bf[_0x14e5c7(0xa99)]=_0xab9a9c[_0x14e5c7(0x9bc)]?0x2:0x1,_0xab9a9c[_0x14e5c7(0x61e)]&&(_0x2007bf[_0x14e5c7(0xbae)]=_0xab9a9c['allowResources']),_0xab9a9c[_0x14e5c7(0x9e1)]&&(_0x2007bf[_0x14e5c7(0xb51)]=!![]),_0xab9a9c[_0x14e5c7(0x44b)]&&(_0xab9a9c[_0x14e5c7(0x44b)]=='webp'||_0xab9a9c['codec']=='images'||_0xab9a9c[_0x14e5c7(0x44b)]=='jpeg')&&(_0x2007bf['allowwebp']=!![]),_0xab9a9c[_0x14e5c7(0x2c1)]&&(_0x2007bf[_0x14e5c7(0x624)]=!![]),_0xab9a9c['badStreamList'][_0x14e5c7(0xa4f)](_0xab9a9c['rpcs'][_0x1b5982]['streamID'])&&(warnlog(_0x14e5c7(0x868)),_0x2007bf[_0x14e5c7(0x46d)]=![],_0x2007bf[_0x14e5c7(0xa99)]=![],_0x2007bf['allowdrawing']=![],_0x2007bf[_0x14e5c7(0xbae)]=![],_0x2007bf[_0x14e5c7(0x624)]=![],_0x2007bf[_0x14e5c7(0x214)]=![],_0x2007bf[_0x14e5c7(0x873)]=![],_0x2007bf[_0x14e5c7(0x275)]=![],_0x2007bf[_0x14e5c7(0x8cf)]=![],_0x2007bf[_0x14e5c7(0x544)]=![],_0x2007bf['video']=![],_0x2007bf[_0x14e5c7(0x4e2)]=![],_0x2007bf[_0x14e5c7(0xbbb)]=![],_0x2007bf[_0x14e5c7(0x8f1)]=![],_0x2007bf[_0x14e5c7(0x25a)]=![]);}catch(_0xc99766){errorlog(_0xc99766);}try{_0x2007bf[_0x14e5c7(0xa82)]={},_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x5e0)]=_0xab9a9c['label'],_0x2007bf[_0x14e5c7(0xa82)]['meta']=_0xab9a9c[_0x14e5c7(0xab7)];_0xab9a9c[_0x14e5c7(0x54a)]&&(_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x8b1)]=![]);_0x2007bf[_0x14e5c7(0xa82)]['order']=_0xab9a9c[_0x14e5c7(0x9ae)];_0xab9a9c[_0x14e5c7(0xa4c)]&&(_0x2007bf[_0x14e5c7(0xa82)]['preferChannel']=_0xab9a9c[_0x14e5c7(0xa4c)]);_0x2007bf[_0x14e5c7(0xa82)]['stereo_url']=_0xab9a9c[_0x14e5c7(0x839)],_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x5ba)]=_0xab9a9c[_0x14e5c7(0x42f)],_0x2007bf['info'][_0x14e5c7(0x5fb)]=_0xab9a9c['audiobitrate'],_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0xac9)]=_0xab9a9c[_0x14e5c7(0x44b)];_0xab9a9c['audioCodec']&&(_0x2007bf[_0x14e5c7(0xa82)]['audio_codec_url']=_0xab9a9c[_0x14e5c7(0xa25)]);_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0xaf2)]=_0xab9a9c['version'],_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0xcb7)]=_0xab9a9c[_0x14e5c7(0xcb7)],_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x499)]=_0xab9a9c[_0x14e5c7(0x713)],_0x2007bf['info'][_0x14e5c7(0x336)]=_0xab9a9c[_0x14e5c7(0x336)],_0x2007bf['info'][_0x14e5c7(0x36d)]=_0xab9a9c[_0x14e5c7(0x36d)],_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x2fa)]=_0xab9a9c[_0x14e5c7(0x2fa)];Firefox&&(_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x69e)]=Firefox);ChromiumVersion&&(_0x2007bf[_0x14e5c7(0xa82)]['chromium']=ChromiumVersion);SafariVersion&&(_0x2007bf[_0x14e5c7(0xa82)]['safari']=SafariVersion);navigator&&navigator[_0x14e5c7(0xb01)]&&(_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x90f)]=navigator['userAgent']);navigator&&navigator[_0x14e5c7(0xb85)]&&(_0x2007bf[_0x14e5c7(0xa82)]['platform']=navigator[_0x14e5c7(0xb85)]);gpgpuSupport&&(_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0xaee)]=gpgpuSupport);cpuSupport&&(_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x763)]=cpuSupport);if(_0xab9a9c[_0x14e5c7(0x583)]===![]){if(window['obsstudio']){_0x2007bf['info'][_0x14e5c7(0x51f)]=window[_0x14e5c7(0x704)]['pluginVersion'];try{_0x2007bf=_0xab9a9c[_0x14e5c7(0xc70)](_0x2007bf,_0x1b5982);}catch(_0x1e5fa7){errorlog(_0x1e5fa7),warnUser(_0x1e5fa7[_0x14e5c7(0x3a3)]);}}else _0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x51f)]=![];}else _0x2007bf['info'][_0x14e5c7(0x51f)]=![];}catch(_0x2b02ad){}_0xab9a9c[_0x14e5c7(0x824)]&&(_0x2007bf[_0x14e5c7(0xa82)][_0x14e5c7(0x52b)]=!![]);_0x2007bf['guest']=![],_0x2007bf[_0x14e5c7(0x268)]=![],_0x2007bf[_0x14e5c7(0x827)]=![],_0x2007bf[_0x14e5c7(0xc87)]=![],_0x2007bf[_0x14e5c7(0xcb7)]=![];_0xab9a9c[_0x14e5c7(0x1d9)]&&(_0x2007bf[_0x14e5c7(0x1d9)]=!![]);_0xab9a9c[_0x14e5c7(0x713)]&&(_0x2007bf['enhanceaudio']=!![]);_0xab9a9c[_0x14e5c7(0x2e1)]&&(_0x2007bf[_0x14e5c7(0x2e1)]=_0xab9a9c['degrade']);_0xab9a9c[_0x14e5c7(0x585)]&&(_0x2007bf['solo']=_0xab9a9c[_0x14e5c7(0x585)]);_0xab9a9c[_0x14e5c7(0x85e)]!==![]&&(_0x2007bf[_0x14e5c7(0x85e)]=_0xab9a9c[_0x14e5c7(0x85e)]);if(_0xab9a9c['director']){_0x2007bf['director']=!![],_0x2007bf[_0x14e5c7(0xcb7)]=_0xab9a9c[_0x14e5c7(0xcb7)];if(_0xab9a9c['directorUUID']&&_0xab9a9c[_0x14e5c7(0xb38)]===_0x1b5982)_0xab9a9c[_0x14e5c7(0xa79)]();else{var _0x30f84e={};_0x30f84e['addCoDirector']=[];for(var _0x5a7e8 in _0xab9a9c['pcs']){_0xab9a9c[_0x14e5c7(0x84f)][_0x5a7e8][_0x14e5c7(0x352)]===!![]&&_0x30f84e[_0x14e5c7(0x770)]['push'](_0x5a7e8);}_0x30f84e[_0x14e5c7(0x770)][_0x14e5c7(0xade)]&&(_0x2007bf[_0x14e5c7(0xc90)]=_0x30f84e);}if(_0xab9a9c[_0x14e5c7(0x249)]&&_0xab9a9c[_0x14e5c7(0x249)]>0x0)_0x2007bf[_0x14e5c7(0x2d8)]=_0xab9a9c[_0x14e5c7(0x249)]-Date[_0x14e5c7(0x30e)]()/0x3e8,_0x2007bf[_0x14e5c7(0x586)]=!![],_0x2007bf[_0x14e5c7(0xab8)]=!![];else _0xab9a9c['roomTimer']&&_0xab9a9c[_0x14e5c7(0x249)]<0x0&&(_0x2007bf[_0x14e5c7(0x2d8)]=_0xab9a9c[_0x14e5c7(0x249)]*-0x1,_0x2007bf[_0x14e5c7(0x586)]=!![],_0x2007bf[_0x14e5c7(0xab8)]=!![],_0x2007bf[_0x14e5c7(0xa29)]=!![]);_0xab9a9c['showRoomTime']&&(_0x2007bf['showTime']=!![]);}else{if(_0xab9a9c[_0x14e5c7(0x268)]!==![])_0x2007bf[_0x14e5c7(0x268)]=_0xab9a9c[_0x14e5c7(0x268)],(_0xab9a9c[_0x14e5c7(0x990)]||_0xab9a9c['solo'])&&(_0x2007bf['showDirector']=_0xab9a9c[_0x14e5c7(0x990)]||_0xab9a9c[_0x14e5c7(0x585)]);else _0xab9a9c[_0x14e5c7(0x70d)]!==![]&&_0xab9a9c[_0x14e5c7(0x70d)]!==''&&(_0x2007bf[_0x14e5c7(0xcb7)]=_0xab9a9c['forceios'],_0x2007bf[_0x14e5c7(0x1ab)]=!![]);}if(_0xab9a9c[_0x14e5c7(0xa53)])_0x2007bf['scale']=parseFloat(_0xab9a9c['scale']);else(_0xab9a9c[_0x14e5c7(0x7a8)]||_0xab9a9c[_0x14e5c7(0x2a2)])&&(_0x2007bf[_0x14e5c7(0x8eb)]={},_0x2007bf[_0x14e5c7(0x8eb)]['h']=null,_0x2007bf['requestResolution']['w']=null,_0xab9a9c['viewheight']&&(_0x2007bf[_0x14e5c7(0x8eb)]['h']=_0xab9a9c['viewheight'],_0xab9a9c['rpcs'][_0x1b5982][_0x14e5c7(0x447)]=_0xab9a9c['viewheight']),_0xab9a9c['viewwidth']&&(_0x2007bf[_0x14e5c7(0x8eb)]['w']=_0xab9a9c[_0x14e5c7(0x2a2)],_0xab9a9c[_0x14e5c7(0x404)][_0x1b5982][_0x14e5c7(0x39a)]=_0xab9a9c['viewwidth']));!_0xab9a9c['roomid']&&(_0xab9a9c[_0x14e5c7(0xb2c)]&&(playtone(![],_0x14e5c7(0x837)),showNotification('There\x27s\x20a\x20new\x20incoming\x20connection.')));_0xab9a9c['rpcs'][_0x1b5982][_0x14e5c7(0x4f2)]=_0x2007bf;_0xab9a9c['sendRequest'](_0x2007bf,_0x1b5982)?log(_0x14e5c7(0x398)):errorlog(_0x14e5c7(0x771));if(_0xab9a9c[_0x14e5c7(0x41e)])try{_0xab9a9c[_0x14e5c7(0x41e)](![],_0x1b5982);}catch(_0xc245ab){errorlog(_0xc245ab);}pokeIframeAPI(_0x14e5c7(0x2b9),!![],_0x1b5982),pokeIframeAPI('view-connection',!![],_0x1b5982),pokeAPI(_0x14e5c7(0x74d),_0xab9a9c[_0x14e5c7(0x404)][_0x1b5982][_0x14e5c7(0xa96)]),_0xab9a9c[_0x14e5c7(0x470)]&&(_0xab9a9c[_0x14e5c7(0x1a5)]&&(_0xab9a9c['layout']=combinedLayout(_0xab9a9c['layout_array'])),updateMixer()),clearTimeout(_0xab9a9c[_0x14e5c7(0x404)][_0x1b5982][_0x14e5c7(0x80e)]),_0xab9a9c[_0x14e5c7(0x404)][_0x1b5982][_0x14e5c7(0x80e)]=setTimeout(processStats,0x0,_0x1b5982);},_0xab9a9c[_0x117013(0x404)][_0x1b5982][_0x117013(0x7d4)][_0x117013(0xb95)]=async _0x301ec9=>{var _0x324492=_0x117013;if(typeof _0x301ec9[_0x324492(0x67b)]==_0x324492(0x537)){if(!_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)]){_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)]=document['createElement']('img'),_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)]['width']=0x10,_0xab9a9c['rpcs'][_0x1b5982][_0x324492(0xc9a)]['height']=0x9,_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)][_0x324492(0x1b0)][_0x324492(0xb48)]=_0x324492(0xbb4),_0xab9a9c['rpcs'][_0x1b5982][_0x324492(0xc9a)][_0x324492(0xa18)]['UUID']=_0x1b5982;try{_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)]['dataset']['sid']=_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xa96)];}catch(_0x41c2e1){}_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)][_0x324492(0x63d)]=![],_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)]['addEventListener']('click',function(_0x1f22ef){var _0xbab71b=_0x324492;log('clicked');try{if(_0x1f22ef[_0xbab71b(0x37a)]||_0x1f22ef[_0xbab71b(0x87d)]){_0x1f22ef[_0xbab71b(0x8f3)]();if(_0xab9a9c[_0xbab71b(0x6a6)]!==![]){var _0x3c8445=_0x1f22ef[_0xbab71b(0x4be)][_0xbab71b(0xa18)]['UUID'];if('stats'in _0xab9a9c[_0xbab71b(0x404)][_0x3c8445]){var [_0x8d9871,_0x3261d9]=statsMenuCreator();printViewStats(_0x3261d9,_0x3c8445),_0x8d9871[_0xbab71b(0xb43)]=setInterval(printViewStats,_0xab9a9c[_0xbab71b(0x41a)],_0x3261d9,_0x3c8445);}}return _0x1f22ef[_0xbab71b(0xbd8)](),![];}}catch(_0x577bb6){errorlog(_0x577bb6);}}),updateMixer();}else _0xab9a9c[_0x324492(0x404)][_0x1b5982]['imageElement'][_0x324492(0x63d)]&&(_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)]['hidden']=![],_0xab9a9c[_0x324492(0x404)][_0x1b5982][_0x324492(0xc9a)][_0x324492(0x1b0)][_0x324492(0x569)]=_0x324492(0x33e));_0xab9a9c['rpcs'][_0x1b5982][_0x324492(0xc9a)][_0x324492(0x5e6)]=window[_0x324492(0x8a1)]['createObjectURL'](new Blob([new Uint8Array(_0x301ec9[_0x324492(0x67b)])],{'type':_0x324492(0x1f7)}));return;}try{var _0x5a86d2=JSON[_0x324492(0xa4b)](_0x301ec9['data']);_0x5a86d2[_0x324492(0xab9)]=_0x1b5982;if(_0x5a86d2[_0x324492(0x5ee)]||_0x5a86d2[_0x324492(0x1ac)]){let _0x391661=_0x5a86d2[_0x324492(0x5ee)]||_0x5a86d2[_0x324492(0x1ac)];if(_0xab9a9c[_0x324492(0xc76)][_0x1b5982]){if(_0xab9a9c['mids'][_0x1b5982]['includes'](_0x391661))return;else _0xab9a9c[_0x324492(0xc76)][_0x1b5982][_0x324492(0x5e7)](_0x391661);}else _0xab9a9c[_0x324492(0xc76)][_0x1b5982]=[_0x391661];}_0x324492(0xaf4)in _0x5a86d2?await _0xab9a9c['processRPCSOnMessage'](_0x5a86d2,_0x1b5982+_0x324492(0x6f3)):await _0xab9a9c[_0x324492(0xad6)](_0x5a86d2,_0x1b5982);}catch(_0xea8940){warnlog(_0x324492(0x4c1)),warnlog(_0xea8940),warnlog(_0x301ec9['data']);}},_0xab9a9c[_0x117013(0xad6)]=async function(_0x1dcf19,_0x4c9c5a){var _0x5478be=_0x117013;warnlog(_0x1dcf19);if(_0x5478be(0xa1d)in _0x1dcf19){warnlog(_0x5478be(0x749)),_0xab9a9c['closeRPC'](_0x4c9c5a,!![]);return;}else{if('ping'in _0x1dcf19){var _0x36a7ef={};_0x36a7ef[_0x5478be(0xb7b)]=_0x1dcf19[_0x5478be(0xb5e)],_0xab9a9c[_0x5478be(0x936)](_0x36a7ef,_0x4c9c5a),warnlog(_0x5478be(0xaf3));return;}else{if(_0x5478be(0xb7b)in _0x1dcf19){try{_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa86)]=_0x1dcf19[_0x5478be(0xb7b)],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['lastPongAt']=Date[_0x5478be(0x30e)]());}catch(_0x4a132b){}warnlog(_0x5478be(0x7f8));return;}}}log('incoming\x20message\x20from\x20publisher');var _0x526497=![],_0x27a251=![];if('description'in _0x1dcf19)_0xab9a9c['processDescription'](_0x1dcf19);else{if(_0x5478be(0x4f8)in _0x1dcf19)_0x1dcf19[_0x5478be(0xab9)]=_0x4c9c5a,log(_0x5478be(0xc2e)),_0xab9a9c[_0x5478be(0x842)](_0x1dcf19);else _0x5478be(0x460)in _0x1dcf19&&(_0x1dcf19[_0x5478be(0xab9)]=_0x4c9c5a,log('GOT\x20ICES!!'),_0xab9a9c['processIceBundle'](_0x1dcf19));}_0x5478be(0x881)in _0x1dcf19&&_0x7b0bb9(_0x1dcf19[_0x5478be(0x881)]);if(_0x5478be(0x923)in _0x1dcf19){if(_0x1dcf19[_0x5478be(0x923)]===_0x5478be(0xb80))_0xab9a9c[_0x5478be(0x40a)]=![],!_0xab9a9c[_0x5478be(0xca4)]&&(warnUser(getTranslation(_0x5478be(0x1b5))),miniTranslate(getById('head4'),'not-the-director'));else{if(_0x1dcf19[_0x5478be(0x923)]===_0x5478be(0xa7b))!_0xab9a9c[_0x5478be(0xca4)]&&warnUser(getTranslation(_0x5478be(0xb8d)),0xbb8);else{if(!_0xab9a9c[_0x5478be(0xca4)]){if(_0xab9a9c['directorUUID']===_0x4c9c5a)warnUser(getTranslation('request-failed'),0x1388);else _0xab9a9c['remote']&&!_0xab9a9c[_0x5478be(0x827)]?warnUser(getTranslation(_0x5478be(0xb66)),0x1388):warnUser(getTranslation(_0x5478be(0x28d)),0x1388);}else{if(_0xab9a9c['director'])!_0xab9a9c[_0x5478be(0xca4)]&&warnUser('The\x20request\x20('+_0x1dcf19[_0x5478be(0x923)]+_0x5478be(0x86d),0x1388);else{if(!_0xab9a9c[_0x5478be(0xca4)])_0xab9a9c[_0x5478be(0x1d9)]?warnUser(getTranslation(_0x5478be(0x884)),0x1388):warnUser(getTranslation(_0x5478be(0xcac)),0x1388);else{}}}}}errorlog('ACTION\x20REJECTED:\x20'+_0x1dcf19[_0x5478be(0x923)]+',\x20isDirector:\x20'+_0xab9a9c[_0x5478be(0x827)]),pokeIframeAPI('rejected',_0x1dcf19[_0x5478be(0x923)],_0x4c9c5a);return;}else{if(_0x5478be(0xb61)in _0x1dcf19){if(_0x1dcf19[_0x5478be(0xb61)]==='requestCoDirector'){if(_0xab9a9c[_0x5478be(0x827)]){try{_0xab9a9c[_0x5478be(0x5e0)]===![]&&(document[_0x5478be(0xbb2)]=getTranslation('control-room-co-director'));}catch(_0x41c7e7){errorlog(_0x41c7e7);}!_0xab9a9c['cleanOutput']&&!_0xab9a9c[_0x5478be(0x40a)]&&(warnUser(getTranslation('approved-as-director'),0xbb8),miniTranslate(getById(_0x5478be(0x908)),_0x5478be(0x3d8)),miniTranslate(getById(_0x5478be(0xccb)),_0x5478be(0x8ab))),!_0xab9a9c[_0x5478be(0x40a)]&&(_0xab9a9c[_0x5478be(0x40a)]=!![],pokeAPI('codirector',!![]),_0xab9a9c[_0x5478be(0x705)](_0x4c9c5a));}}log(_0x5478be(0x733)+_0x1dcf19[_0x5478be(0xb61)]),pokeIframeAPI('approved',_0x1dcf19[_0x5478be(0xb61)],_0x4c9c5a);return;}}if(_0x5478be(0x958)in _0x1dcf19){typeof handleConnectionMapResponse===_0x5478be(0x32c)&&handleConnectionMapResponse(_0x1dcf19,_0x4c9c5a);return;}if(_0x5478be(0x7dc)in _0x1dcf19)try{_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x7dc)]=_0x1dcf19[_0x5478be(0x7dc)]||![];if(_0xab9a9c[_0x5478be(0x827)]){if(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['iframeSrc']){var _0x1c4da4=document[_0x5478be(0xc60)](_0x5478be(0xaff));_0x1c4da4[_0x5478be(0xae1)]=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7dc)],_0x1c4da4[_0x5478be(0xae1)]=_0x1c4da4['innerHTML'],_0x1c4da4=_0x1c4da4['textContent']||_0x1c4da4[_0x5478be(0xae1)]||'',getById(_0x5478be(0x634)+_0x4c9c5a)[_0x5478be(0x247)]='Shared\x20website:\x20=0x0&&(_0xab9a9c[_0x5478be(0x57b)]&&lowerhand()));_0x5478be(0x961)in _0x1dcf19&&(_0xab9a9c[_0x5478be(0x8a3)][_0x5478be(0x565)](_0x4c9c5a)>=0x0&&isolateIncomingChannel(_0x1dcf19[_0x5478be(0x961)],_0x4c9c5a));!_0xab9a9c[_0x5478be(0x536)]&&_0xab9a9c['directorList'][_0x5478be(0x565)](_0x4c9c5a)>=0x0&&(_0x5478be(0x624)in _0x1dcf19&&(_0xab9a9c[_0x5478be(0x624)]=_0x1dcf19[_0x5478be(0x624)],pokeIframeAPI(_0x5478be(0xabb),_0xab9a9c[_0x5478be(0x624)]),_0x526497=!![]),'layout_array'in _0x1dcf19&&(_0xab9a9c[_0x5478be(0x1a5)]=_0x1dcf19[_0x5478be(0x1a5)]));if(_0x5478be(0x940)in _0x1dcf19){if(!_0xab9a9c['ignoreHighlight']){_0xab9a9c[_0x5478be(0x940)]=![],_0xab9a9c[_0x5478be(0x641)]=![];if(_0xab9a9c[_0x5478be(0x4e2)]===![]){log(_0x1dcf19);if(_0xab9a9c[_0x5478be(0x8a3)][_0x5478be(0x565)](_0x4c9c5a)>=0x0){if(_0x1dcf19[_0x5478be(0x940)]!==![]){if(_0x1dcf19[_0x5478be(0x940)]===_0xab9a9c[_0x5478be(0xa96)])_0xab9a9c[_0x5478be(0x940)]=!![];else{if(_0xab9a9c[_0x5478be(0x4f1)][_0x5478be(0xade)]&&!(_0x1dcf19['infocus']in _0xab9a9c[_0x5478be(0x4f1)]))warnlog('NOT\x20IN\x20VIEW\x20SET'),_0xab9a9c[_0x5478be(0x940)]=![];else{if(_0xab9a9c[_0x5478be(0x7c7)]&&_0xab9a9c[_0x5478be(0x7c7)]!==_0x1dcf19[_0x5478be(0x940)])warnlog(_0x5478be(0x281)),_0xab9a9c[_0x5478be(0x940)]=![];else{if(_0xab9a9c[_0x5478be(0x268)]!==![]&&_0xab9a9c[_0x5478be(0xb38)]&&_0xab9a9c[_0x5478be(0xb38)]in _0xab9a9c[_0x5478be(0x404)]&&!_0xab9a9c[_0x5478be(0x404)][_0xab9a9c[_0x5478be(0xb38)]][_0x5478be(0x990)]&&_0x1dcf19[_0x5478be(0x940)]===_0xab9a9c[_0x5478be(0x404)][_0xab9a9c[_0x5478be(0xb38)]]['streamID'])warnlog(_0x5478be(0x49e)),_0xab9a9c[_0x5478be(0x940)]=![];else{for(var _0x524654 in _0xab9a9c[_0x5478be(0x404)]){if(_0xab9a9c[_0x5478be(0x404)][_0x524654][_0x5478be(0xa96)]===_0x1dcf19['infocus']){_0xab9a9c[_0x5478be(0x940)]=_0x524654;break;}}warnlog(_0x5478be(0x2fc));}}}}}else _0xab9a9c[_0x5478be(0x940)]=![];_0x526497=!![],_0x27a251=!![],_0xab9a9c['infocus']?_0xab9a9c[_0x5478be(0x5ef)]=!![]:_0xab9a9c[_0x5478be(0x5ef)]=![];}}}}else{if(_0x5478be(0x641)in _0x1dcf19){if(!_0xab9a9c['ignoreHighlight']){_0xab9a9c[_0x5478be(0x940)]=![],_0xab9a9c[_0x5478be(0x641)]=![];if(_0xab9a9c['broadcast']===![]){log(_0x1dcf19);if(_0xab9a9c['directorList']['indexOf'](_0x4c9c5a)>=0x0){if(_0x1dcf19[_0x5478be(0x641)]!==![]){if(_0x1dcf19['infocus2']===_0xab9a9c[_0x5478be(0xa96)])_0xab9a9c[_0x5478be(0x641)]=!![];else{if(_0xab9a9c['view_set'][_0x5478be(0xade)]&&!(_0x1dcf19[_0x5478be(0x641)]in _0xab9a9c['view_set']))warnlog('NOT\x20IN\x20VIEW\x20SET'),_0xab9a9c[_0x5478be(0x641)]=![];else{if(_0xab9a9c[_0x5478be(0x7c7)]&&_0xab9a9c['view']!==_0x1dcf19[_0x5478be(0x641)])warnlog(_0x5478be(0x281)),_0xab9a9c['infocus2']=![];else{if(_0xab9a9c[_0x5478be(0x268)]!==![]&&_0xab9a9c[_0x5478be(0xb38)]&&_0xab9a9c[_0x5478be(0xb38)]in _0xab9a9c[_0x5478be(0x404)]&&!_0xab9a9c['rpcs'][_0xab9a9c[_0x5478be(0xb38)]]['showDirector']&&_0x1dcf19[_0x5478be(0x641)]===_0xab9a9c[_0x5478be(0x404)][_0xab9a9c[_0x5478be(0xb38)]][_0x5478be(0xa96)])warnlog(_0x5478be(0x49e)),_0xab9a9c['infocus2']=![];else{for(var _0x524654 in _0xab9a9c[_0x5478be(0x404)]){if(_0xab9a9c[_0x5478be(0x404)][_0x524654][_0x5478be(0xa96)]===_0x1dcf19[_0x5478be(0x641)]){_0xab9a9c[_0x5478be(0x641)]=_0x524654;break;}}warnlog('ON\x20FOCUS\x20NOT\x20FOUND');}}}}}else _0xab9a9c[_0x5478be(0x641)]=![];_0xab9a9c[_0x5478be(0x641)]?_0xab9a9c[_0x5478be(0x5ef)]=!![]:_0xab9a9c[_0x5478be(0x5ef)]=![],_0x526497=!![],_0x27a251=!![];}}}}}_0x5478be(0x873)in _0x1dcf19&&_0x1dcf19['allowmidi']!==![]&&(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xc38)]=_0x1dcf19[_0x5478be(0x873)]);_0x5478be(0x472)in _0x1dcf19&&(log(_0x1dcf19),_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x287)][_0x5478be(0x472)]=_0x1dcf19[_0x5478be(0x472)],isIFrame&&parent[_0x5478be(0x3c6)]({'sensors':_0x1dcf19['sensors']},_0xab9a9c[_0x5478be(0x94f)]));'midi'in _0x1dcf19&&playbackMIDI(_0x1dcf19[_0x5478be(0x2fd)],![],_0x4c9c5a);_0x5478be(0x706)in _0x1dcf19&&_0x1dcf19['fileList']&&addDownloadLink(_0x1dcf19[_0x5478be(0x706)],_0x4c9c5a,_0xab9a9c[_0x5478be(0x404)]);'rotate_video'in _0x1dcf19&&(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x359)]!==_0x1dcf19[_0x5478be(0x353)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x359)]=_0x1dcf19[_0x5478be(0x353)],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7eb)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7eb)]['rotated']=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x359)],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7eb)][_0x5478be(0xa18)][_0x5478be(0x278)]=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x359)]),_0x526497=!![]));if(_0x5478be(0xa82)in _0x1dcf19){warnlog(_0x1dcf19),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x287)]['info']=_0x1dcf19[_0x5478be(0xa82)];_0x1dcf19[_0x5478be(0xa82)]['autoSync']&&(!_0xab9a9c['autoSyncObject']&&(_0xab9a9c[_0x5478be(0x2c9)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0xc75)],_0xab9a9c[_0x5478be(0xcb5)](_0x4c9c5a)));_0x5478be(0x8d5)in _0x1dcf19[_0x5478be(0xa82)]&&(_0xab9a9c['rpcs'][_0x4c9c5a]['pseudoguest']=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x8d5)]);_0x1dcf19['info'][_0x5478be(0xa88)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa88)]=!![]);_0xab9a9c['director']&&_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x83a)]&&document['querySelectorAll'](_0x5478be(0x61d)+_0x4c9c5a+_0x5478be(0xa1e))[_0x5478be(0x306)](_0x5bc1fd=>{var _0x196391=_0x5478be;_0x5bc1fd['classList'][_0x196391(0x50f)](_0x196391(0x63d));});if(_0xab9a9c[_0x5478be(0x827)]&&_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x52b)]){var _0x4bcb22=getById(_0x5478be(0x1ca)+_0x4c9c5a);if(_0x4bcb22){var _0x33986c=_0x4bcb22[_0x5478be(0xac5)]('[data-action-type=\x22restart-whip\x22]');_0x33986c&&(_0x33986c[_0x5478be(0xa18)]['UUID']=_0x4c9c5a,_0x33986c[_0x5478be(0xc93)][_0x5478be(0x50f)]('hidden'));}}if(_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0xb51)]){_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x9e1)]=_0x1dcf19[_0x5478be(0xa82)]['allowdrawing'];try{_0xab9a9c['rpcs'][_0x4c9c5a]['videoElement']&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7eb)][_0x5478be(0x201)]&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7eb)][_0x5478be(0x201)]();}catch(_0x1ef417){errorlog(_0x1ef417);}}if(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x4c7)]){if(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x287)]['info'][_0x5478be(0xb1f)])_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['signalMeter']['dataset']['cpu']='1';else'cpuLimited'in _0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['stats'][_0x5478be(0xa82)]&&(_0xab9a9c['rpcs'][_0x4c9c5a]['signalMeter'][_0x5478be(0xa18)][_0x5478be(0x484)]='0');}_0x5478be(0xa61)in _0x1dcf19[_0x5478be(0xa82)]&&(_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0xa61)]!==![]?(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x1e6)]=_0x1dcf19[_0x5478be(0xa82)]['obs_control'],_0xab9a9c['obsStateSync'](_0x5478be(0x83f),_0x4c9c5a)):_0xab9a9c['rpcs'][_0x4c9c5a]['obsControl']=![]);if(_0x5478be(0xab7)in _0x1dcf19[_0x5478be(0xa82)])try{typeof _0x1dcf19['info'][_0x5478be(0xab7)]==_0x5478be(0x537)?_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xab7)]=_0x1dcf19[_0x5478be(0xa82)]['meta']:_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xab7)]=![];}catch(_0x346c76){errorlog(_0x346c76);}try{if(_0xab9a9c[_0x5478be(0x827)]&&_0xab9a9c[_0x5478be(0x40a)]===!![])for(var _0x442820 in _0xab9a9c[_0x5478be(0x84f)]){try{if(_0xab9a9c['pcs'][_0x442820]&&_0xab9a9c[_0x5478be(0x84f)][_0x442820][_0x5478be(0x352)]===!![]){var _0x1dc746={'directorSettings':{'addCoDirector':[_0x442820]}};_0xab9a9c[_0x5478be(0x936)](_0x1dc746,_0x4c9c5a);}}catch(_0x1e96b1){}}}catch(_0xebc60c){errorlog(_0xebc60c);}if('label'in _0x1dcf19[_0x5478be(0xa82)])try{!_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5a1)]&&(typeof _0x1dcf19['info'][_0x5478be(0x5e0)]==_0x5478be(0x9cb)?_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['label']=sanitizeLabel(_0x1dcf19[_0x5478be(0xa82)]['label']):_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x5e0)]=![]);applyStyleEffect(_0x4c9c5a);_0xab9a9c[_0x5478be(0x827)]&&setupGuestLabelControl(_0x4c9c5a);try{_0xab9a9c[_0x5478be(0x840)](_0x4c9c5a);}catch(_0x2162a9){errorlog(_0x2162a9);}}catch(_0x22aefd){errorlog(_0x22aefd);}_0x5478be(0xc71)in _0x1dcf19[_0x5478be(0xa82)]&&_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0xc71)]&&(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xc71)]=!![],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x1a7)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x1a7)]||null,_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x21d)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x21d)]||_0xab9a9c[_0x5478be(0x21d)]||_0x5478be(0x4a4),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x3cf)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x3cf)]||[0x5,0xa,0x19,0x32,0x64],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xba3)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0xba3)]||_0x5478be(0x520),_0xab9a9c[_0x5478be(0x79c)]&&!_0xab9a9c['cleanOutput']&&(typeof addTipIconToVideo==='function'&&addTipIconToVideo(_0x4c9c5a)));if(_0x5478be(0x9ae)in _0x1dcf19['info'])try{_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x9ae)]=parseInt(_0x1dcf19[_0x5478be(0xa82)]['order'])||0x0;if(_0xab9a9c[_0x5478be(0x827)]){var _0xdee4f3=document[_0x5478be(0x1bb)](_0x5478be(0x632)+_0x4c9c5a+'\x22]');_0xdee4f3[0x0]&&(_0xdee4f3[0x0][_0x5478be(0xae1)]=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x9ae)]);}}catch(_0x1f8374){errorlog(_0x1f8374);}else _0xab9a9c['rpcs'][_0x4c9c5a]['order']=0x0;if(_0x5478be(0xa4c)in _0x1dcf19[_0x5478be(0xa82)]){var _0x965ea2=parseInt(_0x1dcf19['info'][_0x5478be(0xa4c)]);_0x965ea2>=0x1&&_0x965ea2<=0x8&&(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xa4c)]=_0x965ea2);}if(typeof _0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x8a2)]!==_0x5478be(0x2cf)){var _0x4b25aa=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x8a2)]!==![]&&_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x8a2)]!==null&&_0x1dcf19['info'][_0x5478be(0x8a2)]!==0x0&&_0x1dcf19['info'][_0x5478be(0x8a2)]!==0x3;_0xab9a9c[_0x5478be(0x4b0)](_0x4c9c5a,_0x4b25aa,_0x5478be(0x584));if(_0xab9a9c[_0x5478be(0x827)]&&!_0xab9a9c[_0x5478be(0x9fc)]){if(_0x4b25aa)_0x3447a7(_0x4c9c5a),_0xab9a9c[_0x5478be(0x55c)](_0x4c9c5a);else _0x1dcf19[_0x5478be(0xa82)]['queued']===0x3&&_0x4b9d8f(_0x4c9c5a);}if(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]&&_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xa96)]){var _0x34ba66=_0xab9a9c[_0x5478be(0x1dd)][_0x5478be(0x565)](_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa96)]);_0x34ba66>-0x1&&_0xab9a9c[_0x5478be(0x1dd)][_0x5478be(0x954)](_0x34ba66,0x1);}if(_0x4b25aa&&!_0xab9a9c['directorUUID']&&_0xab9a9c[_0x5478be(0x847)])try{_0xab9a9c[_0x5478be(0x8a3)][_0x5478be(0x306)](function(_0x38f0a8){var _0xd5ea13=_0x5478be;_0xab9a9c[_0xd5ea13(0x705)](_0x38f0a8);});}catch(_0xe49f89){errorlog(_0xe49f89);}}if(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['batteryMeter'])try{if(_0x5478be(0x481)in _0x1dcf19['info']){if(_0x1dcf19[_0x5478be(0xa82)]['power_level']!==null){var _0x3fd2c0=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)]['querySelector'](_0x5478be(0x22d));if(_0x3fd2c0){var _0x1c2b0b=parseInt(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['stats'][_0x5478be(0xa82)][_0x5478be(0x481)])||0x0;_0x1c2b0b>0x64&&(_0x1c2b0b=0x64);_0x1c2b0b<0x0&&(_0x1c2b0b=0x0);_0x3fd2c0[_0x5478be(0x1b0)][_0x5478be(0x43f)]=parseInt(_0x1c2b0b)+'%';if(_0x1c2b0b<0xa)_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['batteryMeter'][_0x5478be(0xc93)][_0x5478be(0x50f)](_0x5478be(0x2ea)),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)][_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x4e4));else _0x1c2b0b<0x19?(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xa72)][_0x5478be(0xc93)][_0x5478be(0x50f)](_0x5478be(0x4e4)),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)][_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x2ea))):(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)]['classList']['remove'](_0x5478be(0x4e4)),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)]['classList'][_0x5478be(0x50f)]('warn'));_0x1c2b0b<0x64&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)]['classList']['remove'](_0x5478be(0x63d)),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)][_0x5478be(0xbb2)]=_0x1c2b0b+'%\x20battery\x20remaining';}}}_0x5478be(0x29c)in _0x1dcf19['info']&&(_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x29c)]===![]?(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xa72)][_0x5478be(0xa18)][_0x5478be(0xa8d)]='0',_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)][_0x5478be(0xc93)][_0x5478be(0x50f)](_0x5478be(0x63d))):_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa72)]['dataset'][_0x5478be(0xa8d)]='1');}catch(_0x524157){errorlog(_0x524157);}if(_0x5478be(0x2af)in _0x1dcf19[_0x5478be(0xa82)])try{_0x1dcf19['info']['initial_group']?_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x34e)]=_0x1dcf19[_0x5478be(0xa82)]['initial_group'][_0x5478be(0x55a)](','):_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x34e)]=[],_0xab9a9c[_0x5478be(0x827)]?(initGroupButtons(_0x4c9c5a),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x34e)]['length']&&syncGroup(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x34e)],_0x4c9c5a)):_0x526497=!![];}catch(_0x155e5a){errorlog(_0x155e5a);}if(_0x5478be(0x8b2)in _0x1dcf19['info'])try{_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x53f)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x8b2)],(_0xab9a9c[_0x5478be(0xb64)]||_0xab9a9c[_0x5478be(0x36b)]||_0xab9a9c[_0x5478be(0x268)]===![])&&_0xab9a9c[_0x5478be(0x70d)]&&(!_0xab9a9c[_0x5478be(0xca4)]||_0xab9a9c['director'])?(!_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)]=getById('muteStateTemplate')[_0x5478be(0x5b5)](!![]),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['remoteMuteElement']['id']='remoteMuteState_'+_0x4c9c5a,_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x5bc)]['classList'][_0x5478be(0x50f)](_0x5478be(0x63d)),_0x526497=!![]),_0xab9a9c['rpcs'][_0x4c9c5a]['remoteMuteState']?_0xab9a9c[_0x5478be(0xb64)]||_0xab9a9c['scene']===![]?(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['remoteMuteElement']['classList'][_0x5478be(0x50f)](_0x5478be(0x1bd)),_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x5bc)]['classList']['remove'](_0x5478be(0x63d))):_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)][_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x63d)):_0xab9a9c[_0x5478be(0x36b)]?(_0xab9a9c['rpcs'][_0x4c9c5a]['remoteMuteElement'][_0x5478be(0xc93)]['add']('unmuted'),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['remoteMuteElement'][_0x5478be(0xc93)]['remove']('hidden')):_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)][_0x5478be(0xc93)]['add'](_0x5478be(0x63d))):_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)]&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['remoteMuteElement'][_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x63d)),pokeIframeAPI(_0x5478be(0xbbd),_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x53f)],_0x4c9c5a);}catch(_0x330003){errorlog(_0x330003);}if(_0xab9a9c['director']){try{_0x5478be(0x478)in _0x1dcf19['info']&&(_0x1dcf19[_0x5478be(0xa82)]['recording_audio_pipeline']==![]&&initRecordingImpossible(_0x4c9c5a));}catch(_0x5242ab){errorlog(_0x5242ab);}try{if(_0x5478be(0x1d7)in _0x1dcf19[_0x5478be(0xa82)]){if(_0x1dcf19[_0x5478be(0xa82)]['recording_audio_gain']!==![]){let _0x1907e3=parseInt(_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x1d7)])||0x0;initAudioButtons(_0x1907e3,_0x4c9c5a);}}}catch(_0x3aaad3){errorlog(_0x3aaad3);}try{_0x5478be(0x50b)in _0x1dcf19[_0x5478be(0xa82)]&&(_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x50b)]&&updateRemoteSpeakerMute(_0x4c9c5a));}catch(_0x537cc1){errorlog(_0x537cc1);}try{'directorDisplayMuted'in _0x1dcf19[_0x5478be(0xa82)]&&(_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x1ee)]&&updateRemoteDisplayMute(_0x4c9c5a));}catch(_0x3e6496){errorlog(_0x3e6496);}if(_0xab9a9c[_0x5478be(0x2a3)]&&_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0xa28)]&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa96)])try{_0x1dcf19['info'][_0x5478be(0xa28)]['forEach'](_0x318f28=>{var _0x19c3b4=_0x5478be,_0x161eb2=getGuestTargetScene(_0x318f28,_0xab9a9c[_0x19c3b4(0x404)][_0x4c9c5a]['streamID']);_0x161eb2&&directEnable(_0x161eb2,!![]);});}catch(_0x13d224){errorlog(_0x13d224);}}if(_0x5478be(0x8b4)in _0x1dcf19[_0x5478be(0xa82)])try{_0xab9a9c[_0x5478be(0x827)]?_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x8b4)]&&updateDirectorVideoMute(_0x4c9c5a):(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x8b4)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x8b4)],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x8b4)]&&(_0x4c9c5a in _0xab9a9c[_0x5478be(0x404)]&&_0xab9a9c[_0x5478be(0x5b3)](0x0,_0x4c9c5a)));}catch(_0x39343a){errorlog(_0x39343a);}let _0x1ea41d=![];if('directorMirror'in _0x1dcf19['info'])try{_0xab9a9c[_0x5478be(0x827)]&&(_0x1dcf19['info'][_0x5478be(0x58c)]&&(getById('container_'+_0x4c9c5a)['querySelector'](_0x5478be(0xb40))&&(getById(_0x5478be(0x66f)+_0x4c9c5a)[_0x5478be(0xac5)]('[data-action-type=\x22mirror-guest\x22]')['classList'][_0x5478be(0xada)](_0x5478be(0x804)),getById(_0x5478be(0x66f)+_0x4c9c5a)[_0x5478be(0xac5)](_0x5478be(0xb40))[_0x5478be(0xca8)]=_0x5478be(0x372)))),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x4d6)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x58c)],_0x1ea41d=!![];}catch(_0x5c7256){errorlog(_0x5c7256);}if(_0x5478be(0x83d)in _0x1dcf19[_0x5478be(0xa82)])try{_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xb30)]=_0x1dcf19[_0x5478be(0xa82)]['directorFlip'],_0x1ea41d=!![];}catch(_0x561382){errorlog(_0x561382);}if(_0x1ea41d&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7eb)])try{applyMirrorGuest(_0xab9a9c['rpcs'][_0x4c9c5a]['mirrorState'],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['videoElement'],_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xb30)]);}catch(_0x20d21b){errorlog(_0x20d21b);}if('video_muted_init'in _0x1dcf19[_0x5478be(0xa82)])try{_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x6cc)]=_0x1dcf19[_0x5478be(0xa82)]['video_muted_init'],_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x6cc)]&&(_0xab9a9c[_0x5478be(0x827)]&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['remoteVideoMuteElement'][_0x5478be(0xc93)]['remove']('hidden')),pokeIframeAPI(_0x5478be(0x742),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['videoMuted'],_0x4c9c5a);}catch(_0x2b8e7f){errorlog(_0x2b8e7f);}_0x5478be(0x353)in _0x1dcf19[_0x5478be(0xa82)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x359)]!==_0x1dcf19['info'][_0x5478be(0x353)]&&(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x359)]=_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x353)],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7eb)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['videoElement']['rotated']=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['rotate'],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x7eb)]['dataset'][_0x5478be(0x278)]=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x359)]),_0x526497=!![])),_0x5478be(0xbe8)in _0x1dcf19[_0x5478be(0xa82)]&&(_0x1dcf19['info'][_0x5478be(0xbe8)]===![]&&soloLinkGeneratorInit(_0x4c9c5a)),directorCoDirectorColoring(_0x4c9c5a),_0x27a251=!![],pokeAPI(_0x5478be(0x83f),getDetailedState(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['streamID'])),pokeIframeAPI(_0x5478be(0x995),_0x1dcf19[_0x5478be(0xa82)],_0x4c9c5a);}_0x5478be(0x964)in _0x1dcf19&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x287)]&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x287)][_0x5478be(0xa82)]&&processMiniInfoUpdate(_0x1dcf19[_0x5478be(0x964)],_0x4c9c5a));if(_0x1dcf19['directorSettings']){_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x827)]=!![];_0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x22e)]&&await checkToken();if(_0xab9a9c['directorUUID']===_0x4c9c5a){_0x5478be(0x92b)in _0x1dcf19[_0x5478be(0xc90)]&&(_0xab9a9c['totalRoomBitrate']=parseInt(_0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x92b)])||0x0,_0x526497=!![]);if(_0x1dcf19['directorSettings'][_0x5478be(0xccc)]){var _0x2148ff=![];'soloVideoMode'in _0x1dcf19[_0x5478be(0xc90)]&&(_0x2148ff=_0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x9df)]);if(_0xab9a9c[_0x5478be(0x4e2)]===![]){if(_0x2148ff===_0x5478be(0xc57))_0xab9a9c[_0x5478be(0x940)]=![],_0x526497=!![],_0x27a251=!![];else{if(_0x1dcf19[_0x5478be(0xc90)][_0x5478be(0xccc)]===_0xab9a9c['streamID'])_0xab9a9c[_0x5478be(0x940)]=!![];else for(var _0x524654 in _0xab9a9c[_0x5478be(0x404)]){if(_0xab9a9c[_0x5478be(0x404)][_0x524654][_0x5478be(0xa96)]===_0x1dcf19['directorSettings']['soloVideo']){if((_0xab9a9c['directorList'][_0x5478be(0xa4f)](_0x524654)||_0xab9a9c[_0x5478be(0x404)][_0x524654][_0x5478be(0x827)])&&!_0xab9a9c['showDirector'])break;_0xab9a9c[_0x5478be(0x940)]=_0x524654;break;}}_0x526497=!![],_0x27a251=!![];}}}if(_0x5478be(0x990)in _0x1dcf19[_0x5478be(0xc90)]){if(_0xab9a9c[_0x5478be(0x268)]!==![]){if(_0xab9a9c['showDirector'])_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x990)]=_0xab9a9c[_0x5478be(0x990)];else _0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x990)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x990)]=_0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x990)]);}}if(_0xab9a9c['scene']!==![]){if(_0x1dcf19['directorSettings']['scene'])for(var _0x524654 in _0x1dcf19['directorSettings'][_0x5478be(0x268)]){setTimeout(function(_0x4f385a){var _0x4832a4=_0x5478be;_0xab9a9c[_0x4832a4(0x29d)](_0x4f385a);},0x3e8,_0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x268)][_0x524654]);}if(_0x1dcf19['directorSettings'][_0x5478be(0x378)])for(var _0x524654 in _0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x378)]){setTimeout(function(_0x43192c){_0xab9a9c['directorActions'](_0x43192c);},0x3e8,_0x1dcf19['directorSettings'][_0x5478be(0x378)][_0x524654]);}}if('addCoDirector'in _0x1dcf19[_0x5478be(0xc90)])for(var _0x22a755=0x0;_0x22a755<_0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x770)][_0x5478be(0xade)];_0x22a755++){var _0x26aaaf=_0x1dcf19[_0x5478be(0xc90)][_0x5478be(0x770)][_0x22a755][_0x5478be(0xc7a)]();!_0xab9a9c['directorList']['includes'](_0x26aaaf)&&(_0xab9a9c[_0x5478be(0x8a3)][_0x5478be(0x5e7)](_0x26aaaf),addDirectorBlue(_0x26aaaf)),_0x26aaaf in _0xab9a9c[_0x5478be(0x84f)]&&_0xab9a9c[_0x5478be(0x84f)][_0x26aaaf][_0x5478be(0x98a)]&&_0xab9a9c[_0x5478be(0x5df)]==0x4&&_0xab9a9c['initialPublish'](_0x26aaaf);}}}if(_0xab9a9c[_0x5478be(0x8a3)][_0x5478be(0x565)](_0x4c9c5a)>=0x0){if(_0xab9a9c[_0x5478be(0x268)]!==![]){'action'in _0x1dcf19&&_0xab9a9c[_0x5478be(0x29d)](_0x1dcf19);if(_0x5478be(0x845)in _0x1dcf19&&_0x1dcf19['sid'])for(var _0x524654 in _0xab9a9c[_0x5478be(0x404)]){if(_0xab9a9c[_0x5478be(0x404)][_0x524654][_0x5478be(0xa96)]===_0x1dcf19['sid']){_0x1dcf19['audioOutputChannel']?(_0xab9a9c[_0x5478be(0x404)][_0x524654][_0x5478be(0x4a7)]=parseInt(_0x1dcf19[_0x5478be(0x845)])||![],_0xab9a9c[_0x5478be(0x404)][_0x524654][_0x5478be(0x4a7)]-=0x1):_0xab9a9c['rpcs'][_0x524654]['channelOffset']=![];updateIncomingVideoElement(_0x524654,![],!![]);break;}}}_0x5478be(0xc90)in _0x1dcf19&&_0x1dcf19[_0x5478be(0xc90)]['blindAllGuests']&&(!_0xab9a9c[_0x5478be(0x827)]&&(_0xab9a9c['scene']===![]&&(_0xab9a9c[_0x5478be(0x1ee)]=!![],_0xab9a9c[_0x5478be(0xcb9)]())));if('mirrorGuestState'in _0x1dcf19&&_0x5478be(0xc4a)in _0x1dcf19){if(_0x1dcf19['mirrorGuestTarget']&&_0x1dcf19[_0x5478be(0xc4a)]===!![]){_0xab9a9c['permaMirrored']=_0x1dcf19['mirrorGuestState'],_0xab9a9c[_0x5478be(0xa3c)]=_0x1dcf19[_0x5478be(0x7a5)],applyMirror(_0xab9a9c[_0x5478be(0x5c8)]);if(_0xab9a9c['director']){if(_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x58c)]){if(getById(_0x5478be(0x5d6))['querySelector'](_0x5478be(0xb40)))getById(_0x5478be(0x5d6))['querySelector']('[data-action-type=\x22mirror-guest\x22]')[_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x804)),getById(_0x5478be(0x5d6))[_0x5478be(0xac5)](_0x5478be(0xb40))[_0x5478be(0xca8)]=_0x5478be(0x372);else getById(_0x5478be(0x5d6))[_0x5478be(0xac5)](_0x5478be(0xb40))&&(getById(_0x5478be(0x5d6))[_0x5478be(0xac5)](_0x5478be(0xb40))[_0x5478be(0xc93)][_0x5478be(0x50f)](_0x5478be(0x804)),getById(_0x5478be(0x5d6))[_0x5478be(0xac5)](_0x5478be(0xb40))['ariaPressed']='false');}}}else{if(_0x1dcf19[_0x5478be(0xc4a)]&&_0x1dcf19['mirrorGuestTarget']in _0xab9a9c[_0x5478be(0x404)]){_0xab9a9c['rpcs'][_0x1dcf19[_0x5478be(0xc4a)]][_0x5478be(0x4d6)]=_0x1dcf19[_0x5478be(0x7a5)];_0xab9a9c[_0x5478be(0x404)][_0x1dcf19[_0x5478be(0xc4a)]]['videoElement']&&applyMirrorGuest(_0x1dcf19['mirrorGuestState'],_0xab9a9c['rpcs'][_0x1dcf19['mirrorGuestTarget']][_0x5478be(0x7eb)],_0xab9a9c[_0x5478be(0x404)][_0x1dcf19[_0x5478be(0xc4a)]]['flipState']);if(_0xab9a9c[_0x5478be(0x827)]){if(_0x1dcf19[_0x5478be(0xa82)][_0x5478be(0x58c)])getById('container_'+_0x4c9c5a)[_0x5478be(0xac5)](_0x5478be(0xb40))&&(getById(_0x5478be(0x66f)+_0x4c9c5a)[_0x5478be(0xac5)](_0x5478be(0xb40))[_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x804)),getById(_0x5478be(0x66f)+_0x4c9c5a)[_0x5478be(0xac5)](_0x5478be(0xb40))[_0x5478be(0xca8)]=_0x5478be(0x372));else getById(_0x5478be(0x66f)+_0x4c9c5a)[_0x5478be(0xac5)](_0x5478be(0xb40))&&(getById(_0x5478be(0x66f)+_0x4c9c5a)[_0x5478be(0xac5)](_0x5478be(0xb40))[_0x5478be(0xc93)][_0x5478be(0x50f)](_0x5478be(0x804)),getById('container_'+_0x4c9c5a)[_0x5478be(0xac5)](_0x5478be(0xb40))[_0x5478be(0xca8)]=_0x5478be(0x9a1));}}}}if(_0x5478be(0x40a)in _0x1dcf19){!_0xab9a9c[_0x5478be(0x39f)]&&(_0xab9a9c[_0x5478be(0x39f)]={});var _0x4ff5e0=_0x1dcf19[_0x5478be(0x40a)]||{};for(var _0x588215 in _0x4ff5e0){_0xab9a9c[_0x5478be(0x39f)][_0x588215]=_0x4ff5e0[_0x588215],syncSceneState(_0x588215),syncOtherState(_0x588215),syncLabelState(_0x588215);}log(_0x1dcf19),pokeAPI('details',_0xab9a9c[_0x5478be(0x39f)]);}if(_0x5478be(0x1e1)in _0x1dcf19){_0xab9a9c[_0x5478be(0x8cf)]=_0x1dcf19[_0x5478be(0x1e1)]||![];let _0x427ae1=document['getElementById']('widget');try{_0x427ae1?!_0xab9a9c['widget']?(document[_0x5478be(0x2b2)](_0x5478be(0x8cf))['remove'](),_0x526497=!![]):_0x427ae1[_0x5478be(0x5e6)]=parseURL4Iframe(_0xab9a9c[_0x5478be(0x8cf)]):_0x526497=!![],_0xab9a9c[_0x5478be(0x827)]&&(getById(_0x5478be(0x364))[_0x5478be(0xb58)]=_0xab9a9c[_0x5478be(0x8cf)]||'');}catch(_0x57cc7d){errorlog(_0x57cc7d);}pokeIframeAPI(_0x5478be(0x9e5),_0xab9a9c[_0x5478be(0x8cf)],_0x4c9c5a);}if('slotsUpdate'in _0x1dcf19){_0xab9a9c['currentSlots']=_0x1dcf19['slotsUpdate'];_0xab9a9c[_0x5478be(0x827)]&&updateSlotUI();if(_0xab9a9c[_0x5478be(0x536)])try{let _0x29d070=_0xab9a9c[_0x5478be(0x8fa)][_0xab9a9c[_0x5478be(0x536)]];if(_0x29d070)_0xab9a9c['layout']&&!_0xab9a9c[_0x5478be(0x624)][_0x29d070]&&(_0xab9a9c[_0x5478be(0x624)]={[_0x29d070]:{'h':0x64,'w':0x64,'x':0x0,'y':0x0,'c':_0xab9a9c[_0x5478be(0xb0c)]}},updateMixer());else _0xab9a9c[_0x5478be(0x624)]&&Object[_0x5478be(0x7db)](_0xab9a9c['layout'])[_0x5478be(0xade)]&&(_0xab9a9c['layout']={},updateMixer());}catch(_0x1cb5dd){errorlog(_0x1cb5dd);}else!_0xab9a9c[_0x5478be(0xa01)]()&&_0xab9a9c[_0x5478be(0x470)]&&(_0xab9a9c[_0x5478be(0x1a5)]&&(_0xab9a9c[_0x5478be(0x624)]=combinedLayout(_0xab9a9c[_0x5478be(0x1a5)]),updateMixer()),_0xab9a9c[_0x5478be(0x624)]&&(_0xab9a9c[_0x5478be(0x624)]=combinedLayoutSimple(_0xab9a9c[_0x5478be(0x624)]),updateMixer()));warnlog(_0x1dcf19);}'layouts'in _0x1dcf19&&(_0xab9a9c['layouts']=_0x1dcf19[_0x5478be(0x8e2)],_0x5478be(0x681)in _0x1dcf19?(_0xab9a9c[_0x5478be(0x681)]=_0x1dcf19['obsSceneTriggers'],_0xab9a9c['obsSceneSync']()):_0xab9a9c[_0x5478be(0x681)]=![]);_0x5478be(0xb6b)in _0x1dcf19&&stopClock();_0x5478be(0xc74)in _0x1dcf19&&resumeClock();_0x5478be(0x2d8)in _0x1dcf19&&setClock(_0x1dcf19['setClock']);_0x5478be(0x7cb)in _0x1dcf19&&hideClock();'showClock'in _0x1dcf19&&showClock();'startClock'in _0x1dcf19&&startClock();_0x5478be(0xa29)in _0x1dcf19&&pauseClock();if('showTime'in _0x1dcf19){if(_0xab9a9c[_0x5478be(0xa0f)]!==![]){if(_0x1dcf19[_0x5478be(0xa0f)]&&!_0xab9a9c[_0x5478be(0xa0f)])toggleClock(_0x1dcf19[_0x5478be(0xba2)]||![]);else!_0x1dcf19[_0x5478be(0xa0f)]&&_0xab9a9c['showTime']&&toggleClock(_0x1dcf19[_0x5478be(0xba2)]||![]);}}}if(_0x5478be(0x9ae)in _0x1dcf19){_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x9ae)]=parseInt(_0x1dcf19[_0x5478be(0x9ae)])||0x0;_0x4c9c5a in _0xab9a9c[_0x5478be(0x84f)]&&(_0xab9a9c[_0x5478be(0x84f)][_0x4c9c5a][_0x5478be(0x9ae)]=parseInt(_0x1dcf19[_0x5478be(0x9ae)])||0x0);if(_0xab9a9c['director']){var _0xdee4f3=document[_0x5478be(0x1bb)](_0x5478be(0x632)+_0x4c9c5a+'\x22]');_0xdee4f3[0x0]&&(_0xdee4f3[0x0][_0x5478be(0xae1)]=parseInt(_0x1dcf19[_0x5478be(0x9ae)])||0x0);}_0x526497=!![];}if(_0x5478be(0x898)in _0x1dcf19){log(_0x5478be(0x6d3));if(_0x5478be(0xb58)in _0x1dcf19){log(_0x5478be(0x2ff));if(typeof _0x1dcf19[_0x5478be(0xb58)]==_0x5478be(0x9cb)){_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5e0)]=sanitizeLabel(_0x1dcf19[_0x5478be(0xb58)]);_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5e0)][_0x5478be(0xade)]==0x0&&(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x5e0)]=![]);applyStyleEffect(_0x4c9c5a);if(_0xab9a9c[_0x5478be(0x827)])updateLabelDirectors(_0x4c9c5a);else _0xab9a9c[_0x5478be(0x6e5)]&&(_0x526497=!![]);}else{_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['label']=![],applyStyleEffect(_0x4c9c5a);if(_0xab9a9c[_0x5478be(0x827)])updateLabelDirectors2(_0x4c9c5a);else _0xab9a9c[_0x5478be(0x6e5)]&&(_0x526497=!![]);}_0x27a251=!![],pokeIframeAPI(_0x5478be(0x5c9),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5e0)],_0x4c9c5a);}}_0x5478be(0xb70)in _0x1dcf19&&(log(_0x1dcf19),_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x53f)]=_0x1dcf19[_0x5478be(0xb70)],_0xab9a9c[_0x5478be(0x5b3)](![],_0x4c9c5a),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x287)]['info']&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x287)][_0x5478be(0xa82)][_0x5478be(0x8b2)]=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x53f)]),(_0xab9a9c[_0x5478be(0xb64)]||_0xab9a9c[_0x5478be(0x36b)]||_0xab9a9c[_0x5478be(0x268)]===![])&&_0xab9a9c['roomid']&&(!_0xab9a9c[_0x5478be(0xca4)]||_0xab9a9c['director'])?(!_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)]=getById(_0x5478be(0xbdb))['cloneNode'](!![]),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)]['id']=_0x5478be(0x547)+_0x4c9c5a,_0x526497=!![]),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['remoteMuteState']?_0xab9a9c['showMuteState']||_0xab9a9c[_0x5478be(0x268)]===![]?(_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x5bc)][_0x5478be(0xc93)][_0x5478be(0x50f)](_0x5478be(0x1bd)),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)]['classList'][_0x5478be(0x50f)](_0x5478be(0x63d))):_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x5bc)][_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x63d)):_0xab9a9c['showUnMuteState']?(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)][_0x5478be(0xc93)]['add'](_0x5478be(0x1bd)),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x5bc)]['classList'][_0x5478be(0x50f)](_0x5478be(0x63d))):_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0x5bc)][_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x63d)),_0x27a251=!![]):_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['remoteMuteElement']&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['remoteMuteElement']['classList'][_0x5478be(0xada)](_0x5478be(0x63d)),pokeAPI(_0x5478be(0x7af),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x53f)],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa96)]),pokeIframeAPI(_0x5478be(0xbbd),_0x1dcf19[_0x5478be(0xb70)],_0x4c9c5a));if(_0x5478be(0x4ff)in _0x1dcf19){var _0x3c8ccf=getChromiumVersion();_0x3c8ccf&&(_0x3c8ccf<0x50&&(_0x526497=!![]));}if('videoMuted'in _0x1dcf19){log(_0x5478be(0x1d2)+_0x1dcf19[_0x5478be(0x6cc)]),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x6cc)]=_0x1dcf19[_0x5478be(0x6cc)];_0xab9a9c['rpcs'][_0x4c9c5a]['videoMuted']?(!_0xab9a9c[_0x5478be(0x744)]&&_0xab9a9c[_0x5478be(0x5b3)](0x0,_0x4c9c5a),_0xab9a9c['rpcs'][_0x4c9c5a][_0x5478be(0xc9a)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['imageElement'][_0x5478be(0x63d)]=!![],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xc9a)][_0x5478be(0x1b0)][_0x5478be(0x569)]=_0x5478be(0x63d))):(!_0xab9a9c[_0x5478be(0xa5d)]&&applyQualityDirector(_0x4c9c5a),updateIncomingVideoElement(_0x4c9c5a,!![],![]));_0x526497=!![];_0xab9a9c[_0x5478be(0x827)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['videoMuted']?_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x9ca)][_0x5478be(0xc93)][_0x5478be(0x50f)](_0x5478be(0x63d)):_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x9ca)][_0x5478be(0xc93)][_0x5478be(0xada)](_0x5478be(0x63d)));if(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x23c)]&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x6cc)])setTimeout(function(){activeSpeaker();},0x0);else!_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['videoMuted']&&setTimeout(function(){activeSpeaker();},0x0);_0x27a251=!![],pokeAPI(_0x5478be(0x4f9),_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['videoMuted'],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xa96)]),pokeIframeAPI(_0x5478be(0x742),_0x1dcf19[_0x5478be(0x6cc)],_0x4c9c5a);}if('screenStopped'in _0x1dcf19){if(_0x4c9c5a+_0x5478be(0x6f3)in _0xab9a9c[_0x5478be(0x404)]){_0xab9a9c['rpcs'][_0x4c9c5a+_0x5478be(0x6f3)]['virtualHangup']=_0x1dcf19['screenStopped'];try{_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a+_0x5478be(0x6f3)][_0x5478be(0x3a2)]&&(!(SafariVersion&&SafariVersion>0x10)&&(iPad||iOS)&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a+_0x5478be(0x6f3)][_0x5478be(0x7eb)][_0x5478be(0x2a9)]=!![]));}catch(_0x56c888){}_0xab9a9c[_0x5478be(0x827)]&&(_0x1dcf19['screenStopped']?getById(_0x5478be(0x66f)+_0x4c9c5a+'_screen')[_0x5478be(0xc93)][_0x5478be(0xada)]('screenshareNotActive'):getById(_0x5478be(0x66f)+_0x4c9c5a+_0x5478be(0x6f3))['classList'][_0x5478be(0x50f)]('screenshareNotActive')),_0x526497=!![],_0x27a251=!![];}if(_0x1dcf19[_0x5478be(0x854)]){try{_0x4c9c5a in _0xab9a9c['rpcs']&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0xc15)]=![]),_0x4c9c5a+_0x5478be(0x6f3)in _0xab9a9c[_0x5478be(0x404)]&&(_0xab9a9c['rpcs'][_0x4c9c5a+_0x5478be(0x6f3)]['screenShareState']=![]);}catch(_0x41e635){errorlog(_0x41e635);}stopScreenWhep(_0x4c9c5a);}}if(_0x5478be(0xc15)in _0x1dcf19){let _0x1b155a=![],_0x46659f=null;try{_0xab9a9c[_0x5478be(0x404)]&&_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a+_0x5478be(0x6f3)]&&(_0x46659f=_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a+_0x5478be(0x6f3)],_0x46659f[_0x5478be(0xca7)]&&(_0x1b155a=_0x46659f[_0x5478be(0xca7)][_0x5478be(0x5c7)]()[_0x5478be(0x238)](_0x320e19=>_0x320e19[_0x5478be(0x70b)]===_0x5478be(0x5eb))));}catch(_0x49ff3c){}_0x1dcf19[_0x5478be(0xc15)]===![]&&_0x1b155a&&(_0x1dcf19['screenShareState']=!![]),_0xab9a9c['rpcs'][_0x4c9c5a]['screenShareState']=_0x1dcf19[_0x5478be(0xc15)],_0x46659f&&_0x1dcf19['screenShareState']&&(_0x46659f[_0x5478be(0xc15)]=!![]),_0x1dcf19[_0x5478be(0xc15)]?maybeStartScreenWhep(_0x4c9c5a):stopScreenWhep(_0x4c9c5a),_0x526497=!![],pokeIframeAPI('remote-screenshare-state',_0x1dcf19[_0x5478be(0xc15)],_0x4c9c5a);}if(_0x5478be(0x3ac)in _0x1dcf19){if(!_0xab9a9c[_0x5478be(0x827)]){if(_0x5478be(0x357)in _0x1dcf19){if(_0xab9a9c[_0x5478be(0x8a3)][_0x5478be(0x565)](_0x4c9c5a)>=0x0){var _0x280fb6=_0x1dcf19[_0x5478be(0x357)];if(_0x280fb6===!![])_0xab9a9c[_0x5478be(0x8b4)]=_0x1dcf19[_0x5478be(0x3ac)];else _0x280fb6 in _0xab9a9c['rpcs']&&(_0xab9a9c[_0x5478be(0x404)][_0x280fb6][_0x5478be(0x8b4)]=_0x1dcf19[_0x5478be(0x3ac)],_0xab9a9c[_0x5478be(0x404)][_0x280fb6][_0x5478be(0x8b4)]&&_0xab9a9c[_0x5478be(0x5b3)](0x0,_0x280fb6),_0x526497=!![]);}}}_0x27a251=!![];}'virtualHangup'in _0x1dcf19&&(!_0xab9a9c[_0x5478be(0x827)]&&(_0xab9a9c[_0x5478be(0x8a3)][_0x5478be(0x565)](_0x4c9c5a)>=0x0&&(_0x4c9c5a in _0xab9a9c[_0x5478be(0x404)]&&(_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a]['virtualHangup']=_0x1dcf19[_0x5478be(0x3a2)],_0xab9a9c[_0x5478be(0x404)][_0x4c9c5a][_0x5478be(0x3a2)]&&(_0x4c9c5a in _0xab9a9c[_0x5478be(0x404)]&&_0xab9a9c[_0x5478be(0x5b3)](0x0,_0x4c9c5a)),_0x526497=!![]))),_0x27a251=!![]);if(_0x5478be(0x952)in _0x1dcf19){log('requestFile\x20in\x20reverse');try{_0xab9a9c[_0x5478be(0x90d)](_0x4c9c5a,_0x1dcf19[_0x5478be(0x952)]);}catch(_0x41e0eb){errorlog(_0x41e0eb);}}_0x5478be(0x63e)in _0x1dcf19&&remoteStats(_0x1dcf19,_0x4c9c5a);if(_0x526497)setTimeout(function(){updateMixer(),updateUserList();},0x1);else _0x27a251&&updateUserList();},_0xab9a9c[_0x117013(0x404)][_0x1b5982][_0x117013(0x7d4)][_0x117013(0x38c)]=()=>{var _0x374bb8=_0x117013;warnlog(_0x374bb8(0xbe6));};},_0xab9a9c[_0x27ae7a(0x404)][_0x1b5982][_0x27ae7a(0x45e)]=_0x5737c4=>{var _0x1a15dc=_0x27ae7a;warnlog(_0x1a15dc(0xc42)),_0xab9a9c['onTrack'](_0x5737c4,_0x1b5982);},log(_0x27ae7a(0x7cc));},_0xab9a9c['setupScreenShareAddon']=function(_0x16d44e,_0x482970){var _0xd9378d=_0xf92ab0;log('session.setupScreenShareAddon');if(!_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0xa3b)]){(!(_0x482970+_0xd9378d(0x6f3)in _0xab9a9c[_0xd9378d(0x404)])||typeof _0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]!==_0xd9378d(0x537))&&(_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen']={});_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x2a8)]=_0x482970,_0xab9a9c[_0xd9378d(0x404)][_0x482970]['screenElement']=createVideoElement(),_0xab9a9c['rpcs'][_0x482970][_0xd9378d(0xa3b)][_0xd9378d(0x2a9)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0xa3b)][_0xd9378d(0xcba)](_0xd9378d(0xac3),_0x9688f1=>{var _0x3776a4=_0xd9378d;log(_0x3776a4(0x4bf)),_0x9688f1['target'][_0x3776a4(0x2a9)]=![];}),_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0xa3b)]['srcObject']=createMediaStream(),_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x7eb)]=_0xab9a9c[_0xd9378d(0x404)][_0x482970]['screenElement'],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xca7)]=createMediaStream();_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0xa96)]&&(_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xa96)]=_0xab9a9c['rpcs'][_0x482970][_0xd9378d(0xa96)]+':s');try{_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0xc15)]=!![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]&&(_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xc15)]=!![]);}catch(_0x3cf45a){}_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['stats']={},_0xab9a9c['rpcs'][_0x482970][_0xd9378d(0x287)][_0xd9378d(0x8c1)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x466)]=function(){return new Promise((_0x13c611,_0x399b17)=>{_0x13c611([]);});},_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x80e)]=null,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x1db)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xc38)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x9e1)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x23c)]=![],_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xb96)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x971)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x888)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0xc05)]=null,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x9f4)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['buffer']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['bandwidth']=-0x1,_0xab9a9c['rpcs'][_0x482970+'_screen']['bandwidthMuted']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['showDirector']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x4a7)]=![],_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)]['channelWidth']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x3b9)]=-0x1,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x9b6)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xc9a)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xb1c)]=![],_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x34e)]=_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0x34e)]||[],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['videoMuted']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x33c)]=![],_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)]['directorVideoMuted']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x3a2)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x53f)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['remoteMuteElement']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x458)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['lockedAudioBitrate']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x808)]=null,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xc55)]=null,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['mutedStateMixer']=null,_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0xaa5)]=null,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x4d6)]=null,_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xb30)]=null,_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x447)]=![],_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x39a)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['scaleSnap']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['signalMeter']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen']['volumeControl']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xa65)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xc15)]=!![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x9eb)]=0x64,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x45d)]=0x0,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x87e)]=0x0,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x1d1)]='1',_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['opacityMuted']='1',_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['obsControl']=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x308)]=0x0,_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x5e0)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x9ae)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x6ed)]=null,_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['canvas']=null,_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)]['inboundAudioPipeline']={},_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x7dc)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x7ca)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x334)]=Date['now'](),_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x4f2)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x3d6)]=![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x8d5)]=![];(_0xab9a9c[_0xd9378d(0xbe9)]==0x2||_0xab9a9c[_0xd9378d(0xbe9)]==0x4)&&(_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['loudest']=!![]);_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0xa88)]?_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen']['smallScreen']=!![]:_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xa88)]=![];_0xab9a9c['rpcs'][_0x482970][_0xd9378d(0x422)]&&(_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xa88)]=![]);if(_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0x9e1)]){_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x9e1)]=_0xab9a9c[_0xd9378d(0x404)][_0x482970]['allowDrawing'];try{_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x7eb)]&&_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)]['videoElement'][_0xd9378d(0x201)]&&_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]['videoElement'][_0xd9378d(0x201)]();}catch(_0x593319){errorlog(_0x593319);}}_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x7eb)][_0xd9378d(0xa18)][_0xd9378d(0xab9)]=_0x482970+_0xd9378d(0x6f3),_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen']['videoElement']['id']=_0xd9378d(0x628)+_0x482970+_0xd9378d(0x6f3),_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xa96)]&&(_0xab9a9c['rpcs'][_0x482970+'_screen'][_0xd9378d(0x7eb)][_0xd9378d(0xa18)][_0xd9378d(0x2e8)]=_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xa96)]),_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x7eb)]['screenshare']=![],_0xab9a9c['rpcs'][_0x482970+'_screen'][_0xd9378d(0xb1c)]=![],setupIncomingScreenTracking(_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x7eb)],_0x482970+_0xd9378d(0x6f3)),_0x16d44e[_0xd9378d(0x306)](function(_0x1dfabb){var _0x230325=_0xd9378d;_0xab9a9c[_0x230325(0x404)][_0x482970][_0x230325(0xa3b)][_0x230325(0x5f6)][_0x230325(0x311)](_0x1dfabb),_0xab9a9c[_0x230325(0x404)][_0x482970+'_screen'][_0x230325(0xca7)]['addTrack'](_0x1dfabb);}),_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0x7eb)][_0xd9378d(0x653)]=!![],_0xab9a9c[_0xd9378d(0x404)][_0x482970+'_screen'][_0xd9378d(0x7eb)]['setAttribute'](_0xd9378d(0x515),''),mediaSourceUpdated(_0x482970+_0xd9378d(0x6f3),_0xab9a9c['rpcs'][_0x482970+_0xd9378d(0x6f3)]['streamID']);}else _0x16d44e[_0xd9378d(0x306)](function(_0x3531e4){var _0x39991a=_0xd9378d,_0x344c64=![];_0xab9a9c[_0x39991a(0x404)][_0x482970]['screenElement']['srcObject'][_0x39991a(0xb93)]()[_0x39991a(0x306)](function(_0x43c0b6){_0x43c0b6['id']==_0x3531e4['id']&&_0x43c0b6['kind']==_0x3531e4['kind']&&(_0x344c64=!![]);});!_0x344c64&&_0xab9a9c['rpcs'][_0x482970][_0x39991a(0xa3b)][_0x39991a(0x5f6)][_0x39991a(0x311)](_0x3531e4);var _0x344c64=![];_0xab9a9c['rpcs'][_0x482970+_0x39991a(0x6f3)]['streamSrc'][_0x39991a(0xb93)]()[_0x39991a(0x306)](function(_0xdeb5ad){var _0x12bc85=_0x39991a;_0xdeb5ad['id']==_0x3531e4['id']&&_0xdeb5ad[_0x12bc85(0xcd1)]==_0x3531e4['kind']&&(_0x344c64=!![]);}),!_0x344c64&&_0xab9a9c[_0x39991a(0x404)][_0x482970+_0x39991a(0x6f3)][_0x39991a(0xca7)][_0x39991a(0x311)](_0x3531e4);});try{_0xab9a9c[_0xd9378d(0x404)][_0x482970][_0xd9378d(0xc15)]=!![];}catch(_0x59cf4f){}try{_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)]&&(_0xab9a9c[_0xd9378d(0x404)][_0x482970+_0xd9378d(0x6f3)][_0xd9378d(0xc15)]=!![]);}catch(_0x3d3fdd){}},_0xab9a9c;}());var meshcastServer=![],meshcastServerList=![];const meshcastPingResults=new Map();function selectMeshcast(_0x471c6a){var _0x4aff97=_0x165386;meshcastServer={};const _0x248a13=_0x471c6a[_0x4aff97(0x7fc)][_0x471c6a[_0x4aff97(0x6b0)]];meshcastServer[_0x4aff97(0x241)]=_0x248a13['url'],meshcastServer[_0x4aff97(0x882)]=_0x248a13[_0x4aff97(0x882)]||null,meshcastServer['id']=_0x248a13['id']||null;}async function pingMeshcast(_0xaea993,_0x511600){return new Promise(_0x37bfc1=>{var _0x58594e=_0x2b9f;const _0x44ba6c=new XMLHttpRequest();_0x44ba6c[_0x58594e(0xa23)]=function(){var _0x8912dd=_0x58594e;const _0xfc214=parseFloat(this['responseText']);if(_0xfc214>=0x0){meshcastPingResults['set'](_0xaea993['id']||_0xaea993[_0x8912dd(0x882)],{'load':_0xfc214,'failed':![],'option':_0xaea993});if(_0xfc214>0x46)_0xaea993[_0x8912dd(0x247)]+='\x20(full)';else{if(_0xfc214>0x28)_0xaea993[_0x8912dd(0x247)]+=_0x8912dd(0xc79);else{if(_0xfc214>0xa)_0xaea993[_0x8912dd(0x247)]+=_0x8912dd(0x2d5);else _0xfc214>0x0?_0xaea993[_0x8912dd(0x247)]+=_0x8912dd(0x7b0):handleMeshcastFailure(_0xaea993);}}_0x37bfc1(!![]);}else handleMeshcastFailure(_0xaea993),_0x37bfc1(![]);},_0x44ba6c[_0x58594e(0x636)]=()=>{handleMeshcastFailure(_0xaea993),_0x37bfc1(![]);},_0x44ba6c[_0x58594e(0x38f)]=0x7d0,_0x44ba6c[_0x58594e(0x664)]=()=>{handleMeshcastFailure(_0xaea993,'timeout'),_0x37bfc1(![]);},_0x44ba6c[_0x58594e(0xa48)](_0x58594e(0x4c4),_0x511600,!![]),_0x44ba6c[_0x58594e(0x4c6)]();});}function handleMeshcastFailure(_0x9b7a34,_0x4cb52a=_0x165386(0x416)){var _0x3d09e2=_0x165386;meshcastPingResults[_0x3d09e2(0x43d)](_0x9b7a34['id']||_0x9b7a34[_0x3d09e2(0x882)],{'load':Infinity,'failed':!![],'option':_0x9b7a34}),_0x9b7a34['disabled']=!![],_0x9b7a34[_0x3d09e2(0x247)]+='\x20('+_0x4cb52a+')';}function sortMeshcastOptions(){var _0x161527=_0x165386;const _0x362587=document[_0x161527(0x2b2)]('edgelist'),_0x2ab37d=Array[_0x161527(0x208)](_0x362587[_0x161527(0x7fc)]);_0x2ab37d[_0x161527(0x84d)]((_0x422764,_0x25da0d)=>{var _0x16ee94=_0x161527;const _0x3be514=meshcastPingResults['get'](_0x422764['id']||_0x422764[_0x16ee94(0x882)])||{'load':Infinity,'failed':!![]},_0x440963=meshcastPingResults[_0x16ee94(0x666)](_0x25da0d['id']||_0x25da0d['code'])||{'load':Infinity,'failed':!![]};if(_0x3be514['failed']&&!_0x440963[_0x16ee94(0xa40)])return 0x1;if(!_0x3be514[_0x16ee94(0xa40)]&&_0x440963['failed'])return-0x1;const _0xa1cd59=meshcastServerList[_0x16ee94(0x54f)](_0x2eeea9=>(_0x2eeea9['id']||_0x2eeea9[_0x16ee94(0x882)])===(_0x422764['id']||_0x422764[_0x16ee94(0x882)])),_0x366c11=meshcastServerList[_0x16ee94(0x54f)](_0x5c5416=>(_0x5c5416['id']||_0x5c5416[_0x16ee94(0x882)])===(_0x25da0d['id']||_0x25da0d[_0x16ee94(0x882)])),_0x15c7cd=_0x3be514[_0x16ee94(0x2dd)]+(_0xa1cd59[_0x16ee94(0x610)]||0x0)/0x28,_0x26c985=_0x440963[_0x16ee94(0x2dd)]+(_0x366c11['delta']||0x0)/0x28;return _0x422764['dataset'][_0x16ee94(0xc5e)]=_0x15c7cd,_0x25da0d[_0x16ee94(0xa18)][_0x16ee94(0xc5e)]=_0x26c985,_0x422764[_0x16ee94(0xa18)][_0x16ee94(0x2dd)]=_0x3be514[_0x16ee94(0x2dd)],_0x25da0d[_0x16ee94(0xa18)]['load']=_0x440963[_0x16ee94(0x2dd)],_0x422764[_0x16ee94(0xa18)]['delta']=(_0xa1cd59['delta']||0x0)/0x28,_0x25da0d[_0x16ee94(0xa18)][_0x16ee94(0x610)]=(_0x366c11[_0x16ee94(0x610)]||0x0)/0x28,_0x15c7cd-_0x26c985;}),_0x2ab37d[_0x161527(0x306)](_0x3c2467=>_0x362587[_0x161527(0x5db)](_0x3c2467));}function selectBestMeshcastServer(){var _0x17070c=_0x165386;const _0x24b3b6=document[_0x17070c(0x2b2)](_0x17070c(0x4fd));let _0x29a8ad=Array['from'](_0x24b3b6[_0x17070c(0x7fc)])[_0x17070c(0x54f)](_0x2de5ca=>_0x2de5ca[_0x17070c(0xaab)]&&!_0x2de5ca[_0x17070c(0xca0)]);!_0x29a8ad&&(_0x29a8ad=Array[_0x17070c(0x208)](_0x24b3b6[_0x17070c(0x7fc)])[_0x17070c(0x54f)](_0x2214b6=>!_0x2214b6[_0x17070c(0xca0)])),_0x29a8ad?(_0x29a8ad[_0x17070c(0x818)]=!![],selectMeshcast(_0x24b3b6)):console['error'](_0x17070c(0x76a));}async function queryMeshcastServers(_0x1274dd=![]){var _0x5e9092=_0x165386;try{const _0x15070b=new Date(),_0x2a5556=urlParams[_0x5e9092(0x2f5)]('tz')?parseInt(urlParams[_0x5e9092(0x666)]('tz')):_0x15070b['getTimezoneOffset'](),_0x127a67=await fetch(_0x5e9092(0x698)+Date[_0x5e9092(0x30e)]()),_0x591eae=await _0x127a67['json']();meshcastServerList=_0x591eae;const _0x53da88=typeof session[_0x5e9092(0xb87)]===_0x5e9092(0x9cb)?session['meshcast'][_0x5e9092(0x8f2)]():session['meshcast'],_0x275c5f=['any','audio',_0x5e9092(0xa55)];let _0x27ff72=session[_0x5e9092(0x623)]||null;typeof _0x27ff72===_0x5e9092(0x9cb)&&(_0x27ff72=_0x27ff72[_0x5e9092(0x8f2)](),!_0x27ff72['length']&&(_0x27ff72=null));if(!_0x27ff72&&typeof _0x53da88===_0x5e9092(0x9cb)&&_0x53da88[_0x5e9092(0xade)]){const _0x1760f2=_0x53da88[_0x5e9092(0x85b)]();!_0x275c5f[_0x5e9092(0xa4f)](_0x1760f2)&&(_0x27ff72=_0x53da88);}if(_0x27ff72){const _0x5b88f2=_0x591eae[_0x5e9092(0x238)](_0x582043=>_0x582043[_0x5e9092(0x882)]&&_0x582043['code']===_0x27ff72||_0x582043['id']&&_0x582043['id']===_0x27ff72);if(!_0x5b88f2){const _0x4499e7=_0x27ff72[_0x5e9092(0xa4f)]('.');let _0x140ad5;_0x4499e7?_0x140ad5=_0x27ff72['startsWith']('http')?_0x27ff72[_0x5e9092(0x35b)](/\/$/,''):_0x5e9092(0x21f)+_0x27ff72[_0x5e9092(0x35b)](/\/$/,''):_0x140ad5=_0x5e9092(0x21f)+_0x27ff72+'.meshcast.io';const _0x9c55a0={'id':_0x27ff72,'code':_0x27ff72,'url':_0x140ad5,'label':_0x5e9092(0x6be)+_0x27ff72,'tz':_0x2a5556,'penalty':-0x989680,'preferred':!![]};meshcastServerList['unshift'](_0x9c55a0);}}meshcastServerList=meshcastServerList[_0x5e9092(0xb09)](_0x420b14=>{var _0x5dbb62=_0x5e9092;let _0x18e305=Math[_0x5dbb62(0x76f)](_0x420b14['tz']-_0x2a5556);Math[_0x5dbb62(0x76f)](_0x18e305-0x3c*0x18)<_0x18e305&&(_0x18e305=Math[_0x5dbb62(0x76f)](_0x18e305-0x3c*0x18));_0x420b14['delta']=_0x18e305+(_0x420b14[_0x5dbb62(0x355)]||0x0);if(_0x27ff72){const _0x255c15=_0x420b14[_0x5dbb62(0x882)]&&_0x420b14[_0x5dbb62(0x882)]===_0x27ff72||_0x420b14['id']&&_0x420b14['id']===_0x27ff72;!_0x255c15&&(_0x420b14[_0x5dbb62(0x610)]+=0xa1220),_0x420b14[_0x5dbb62(0xaab)]=!!_0x255c15;}else _0x420b14[_0x5dbb62(0xaab)]=!!(session['meshcastCode']&&(_0x420b14['id']===session['meshcastCode']||session[_0x5dbb62(0x623)]===_0x420b14[_0x5dbb62(0x882)]));return _0x420b14;})[_0x5e9092(0x84d)]((_0x1bc67e,_0x5bf0f2)=>_0x1bc67e[_0x5e9092(0x610)]-_0x5bf0f2[_0x5e9092(0x610)]);const _0x420e1c=meshcastServerList[_0x5e9092(0xb09)](_0x20a8cd=>{var _0xc95969=_0x5e9092;const _0x5a17be=document[_0xc95969(0xc60)](_0xc95969(0xc4b));if(_0x20a8cd[_0xc95969(0x882)])_0x5a17be[_0xc95969(0x882)]=_0x20a8cd[_0xc95969(0x882)];if(_0x20a8cd['id'])_0x5a17be['id']=_0x20a8cd['id'];return _0x5a17be[_0xc95969(0x241)]=_0x20a8cd['url'],_0x5a17be[_0xc95969(0x247)]=_0x20a8cd[_0xc95969(0x5e0)],_0x5a17be[_0xc95969(0xaab)]=_0x20a8cd[_0xc95969(0xaab)],document['getElementById'](_0xc95969(0x4fd))[_0xc95969(0x5db)](_0x5a17be),_0x5a17be;}),_0x5cc2c4=meshcastServerList[_0x5e9092(0xb09)]((_0x159bc9,_0x5f3275)=>pingMeshcast(_0x420e1c[_0x5f3275],_0x159bc9[_0x5e9092(0x241)]+_0x5e9092(0x9ce)));await Promise['all'](_0x5cc2c4),sortMeshcastOptions(),selectBestMeshcastServer(),_0x1274dd&&_0x1274dd(),session[_0x5e9092(0x827)]&&!session[_0x5e9092(0xca4)]&&!session[_0x5e9092(0xb6e)]&&document[_0x5e9092(0x2b2)](_0x5e9092(0x4fa))['classList'][_0x5e9092(0x50f)]('hidden');}catch(_0x37557f){console[_0x5e9092(0xca1)]('Error\x20fetching\x20meshcast\x20servers:',_0x37557f);}}async function meshcast2(){var _0x4b8ba0=_0x165386;if(!session['meshcast2'])return;if(session[_0x4b8ba0(0x69a)]!==![])return;if(!session[_0x4b8ba0(0x648)]&&!session['videoElement'][_0x4b8ba0(0x5f6)])return;const _0x5de576=_0x4b8ba0(0xaa6),_0xf3b51a=session['meshcast2']===!![]||session[_0x4b8ba0(0x821)]==='any'||session['meshcast2']===_0x4b8ba0(0xc4f)||session['meshcast2FallbackActive']&&!session[_0x4b8ba0(0xb02)];let _0xaf75ce=null;if(_0xf3b51a)try{const _0x237f30=await fetch(_0x5de576+_0x4b8ba0(0x3df),{'method':_0x4b8ba0(0xbb9),'headers':{'Content-Type':_0x4b8ba0(0xa9b)},'body':JSON[_0x4b8ba0(0x6d1)]({'duration_minutes':0x78})}),_0x5d1049=await _0x237f30[_0x4b8ba0(0x4e8)]()[_0x4b8ba0(0xacc)](()=>({}));if(!_0x237f30['ok']){session[_0x4b8ba0(0xb02)]=![],session[_0x4b8ba0(0xa58)]=!![],session[_0x4b8ba0(0xa0a)]=_0x5d1049&&_0x5d1049[_0x4b8ba0(0x882)]?_0x5d1049[_0x4b8ba0(0x882)]:_0x4b8ba0(0x563);if(!session[_0x4b8ba0(0xca4)]){if(session[_0x4b8ba0(0xa0a)]===_0x4b8ba0(0x304))promptAlt('Meshcast2\x20access\x20blocked\x20for\x20this\x20origin.\x20Create\x20an\x20account\x20to\x20continue.',![],![],![],0x5);else session['meshcast2LastError']===_0x4b8ba0(0x96c)?promptAlt(_0x4b8ba0(0x6e0),![],![],![],0x5):promptAlt('Meshcast2\x20anonymous\x20publish\x20failed.\x20Try\x20again\x20later.',![],![],![],0x5);}session[_0x4b8ba0(0x78c)]=![];return;}_0xaf75ce=_0x5d1049[_0x4b8ba0(0xc1d)],session[_0x4b8ba0(0xb02)]=!![],session[_0x4b8ba0(0xa0a)]=null,session[_0x4b8ba0(0x78c)]=![];}catch(_0x27e9ae){errorlog(_0x27e9ae);return;}else{if(typeof session[_0x4b8ba0(0x821)]===_0x4b8ba0(0x9cb)&&session[_0x4b8ba0(0x821)][_0x4b8ba0(0x8f2)]()[_0x4b8ba0(0xade)]){if(session[_0x4b8ba0(0x821)][_0x4b8ba0(0x8f2)]()['toLowerCase']()===_0x4b8ba0(0xc4f))return session[_0x4b8ba0(0x821)]=_0x4b8ba0(0x7d1),meshcast2();_0xaf75ce=session[_0x4b8ba0(0x821)]['trim'](),session[_0x4b8ba0(0xb02)]=_0xaf75ce[_0x4b8ba0(0x602)](_0x4b8ba0(0x988)),session[_0x4b8ba0(0xa0a)]=null;}else return;}if(!_0xaf75ce)return;const _0x4d29b1=_0x5de576+_0x4b8ba0(0x87f)+_0xaf75ce,_0x29d29e=_0x5de576+_0x4b8ba0(0xc84)+_0xaf75ce;session[_0x4b8ba0(0x824)]=_0x4d29b1,session['whipoutSettings']={'type':'whep','url':_0x29d29e,'token':_0xaf75ce,'media':_0x4b8ba0(0xbc0),'started':![]};if(session['whipPublishScreen']){const _0x190968=_0xaf75ce+'_s';session[_0x4b8ba0(0xc28)]=_0x5de576+_0x4b8ba0(0x87f)+_0x190968,session[_0x4b8ba0(0xb26)]={'type':'whep','url':_0x5de576+'/api/gateway/whep/'+_0x190968,'token':_0x190968,'media':_0x4b8ba0(0x30b),'started':![]},session[_0x4b8ba0(0xc15)]&&whipOutScreen();}else session[_0x4b8ba0(0xc28)]=![],session[_0x4b8ba0(0xb26)]=![];whipOut();}async function meshcast(_0x48fdbe=![]){var _0xa69915=_0x165386;if(!session[_0xa69915(0xb87)])return;if(_0x48fdbe){await queryMeshcastServers();return;}if(session[_0xa69915(0x69a)]!==![])return;if(!session[_0xa69915(0x648)]&&!session[_0xa69915(0x7eb)][_0xa69915(0x5f6)])return;session['whipoutSettings']=null;const _0xfc34c7=[],_0x10d7e5=session[_0xa69915(0x54d)](0xe),_0x397685=_0x10d7e5+'_s';async function _0x4da620(){var _0x21bb45=_0xa69915;document[_0x21bb45(0x2b2)](_0x21bb45(0x4fd))[_0x21bb45(0xca0)]=!![],document[_0x21bb45(0x2b2)]('edgelist')[_0x21bb45(0xbb2)]=_0x21bb45(0x9a3);!meshcastServer&&meshcastServerList&&meshcastServerList[_0x21bb45(0xade)]&&(meshcastServer=meshcastServerList[_0x21bb45(0xa17)]());if(!meshcastServer){handleMeshcastError();return;}if(meshcastServer['id']){if(session[_0x21bb45(0x6b3)]&&session[_0x21bb45(0xa06)]&&meshcastServer&&meshcastServer[_0x21bb45(0x241)])try{var _0x4bc3e4=new URL(meshcastServer[_0x21bb45(0x241)])['hostname'];session[_0x21bb45(0xa06)][_0x21bb45(0x250)]=_0x21bb45(0xb87),_0x4bc3e4&&!session[_0x21bb45(0xa06)][_0x21bb45(0x916)][_0x21bb45(0xa4f)](_0x4bc3e4)&&session['qosData'][_0x21bb45(0x916)]['push'](_0x4bc3e4);}catch(_0x2002df){}session['whipPublishPrimary']?(session[_0x21bb45(0x824)]=meshcastServer[_0x21bb45(0x241)]+'/'+_0x10d7e5+'/whip',session[_0x21bb45(0x69a)]={'type':_0x21bb45(0x97e),'url':meshcastServer[_0x21bb45(0x241)]+'/'+_0x10d7e5+'/whep','token':_0x10d7e5,'media':_0x21bb45(0xbc0),'started':![]},whipOut()):(session[_0x21bb45(0x824)]=![],session[_0x21bb45(0x69a)]=![]),session[_0x21bb45(0x870)]?(session['whipOutputScreen']=meshcastServer[_0x21bb45(0x241)]+'/'+_0x397685+_0x21bb45(0x1f1),session[_0x21bb45(0xb26)]={'type':'whep','url':meshcastServer[_0x21bb45(0x241)]+'/'+_0x397685+'/whep','token':_0x397685,'media':_0x21bb45(0x30b),'started':![]},session[_0x21bb45(0xc15)]&&whipOutScreen()):(session[_0x21bb45(0xc28)]=![],session['whipoutScreenSettings']=![]);}}!meshcastServerList?await queryMeshcastServers(_0x4da620):await _0x4da620();}function handleMeshcastError(){var _0x36ff74=_0x165386;errorlog(_0x36ff74(0x485));if(!session[_0x36ff74(0xca4)]){const _0x2adbfe=window[_0x36ff74(0xbf8)][_0x36ff74(0x4d2)];_0x2adbfe[_0x36ff74(0xa4f)]('?')?warnUser('Failed\x20to\x20connect\x20to\x20Meshcast.\x0a\x0aCheck\x20your\x20connection\x20or\x20switch\x20to\x20peer-to-peer\x20mode\x20instead.\x0a\x0a'+(_0x36ff74(0x756)+_0x2adbfe+'&meshcastfailed\x27>Click\x20here\x20to\x20reload\x20without\x20Meshcast\x20enabled'),![],![]):warnUser(_0x36ff74(0x4f3));}}function whepSettingsHasStarted(_0x25e645){var _0x9a4d9e=_0x165386;if(!_0x25e645||!(_0x9a4d9e(0x6d4)in _0x25e645))return![];const _0x389480=_0x25e645[_0x9a4d9e(0x6d4)];if(typeof _0x389480===_0x9a4d9e(0xab6))return _0x389480>0x0;if(typeof _0x389480===_0x9a4d9e(0x9cb)){const _0x59130d=_0x389480['trim']();if(!_0x59130d)return![];if(_0x59130d==='0')return![];if(_0x59130d[_0x9a4d9e(0x85b)]()===_0x9a4d9e(0x9a1))return![];return!![];}if(_0x389480===!![])return!![];return![];}function whepSettingsMarker(_0x9c452a){var _0x3d08f9=_0x165386;if(!_0x9c452a)return null;if(whepSettingsHasStarted(_0x9c452a))return String(_0x9c452a[_0x3d08f9(0x6d4)]);const _0x2bb4f1=typeof _0x9c452a[_0x3d08f9(0x241)]===_0x3d08f9(0x9cb)?_0x9c452a[_0x3d08f9(0x241)]:'',_0x4b7b6d=typeof _0x9c452a[_0x3d08f9(0xac0)]===_0x3d08f9(0x9cb)?_0x9c452a[_0x3d08f9(0xac0)]:'';if(_0x2bb4f1||_0x4b7b6d)return _0x2bb4f1+'|'+_0x4b7b6d;return null;}async function whepWatch(_0x4a09b7,_0x56c06c){var _0x12c1a4=_0x165386;if(session[_0x12c1a4(0x8c8)])return;console[_0x12c1a4(0x440)](_0x56c06c);if(_0x56c06c[_0x12c1a4(0x3c5)]==_0x12c1a4(0xb87))meshcastWatch(_0x4a09b7,_0x56c06c);else{if(_0x56c06c[_0x12c1a4(0x3c5)]=='whep'){if(_0x56c06c&&_0x56c06c[_0x12c1a4(0x241)]){const _0x27149d=_0x56c06c[_0x12c1a4(0x60b)]===_0x12c1a4(0x30b);if(_0x27149d){const _0xc200a8=_0x4a09b7+'_screen',_0x31229a=Object[_0x12c1a4(0x6fd)]({},_0x56c06c);try{!(_0xc200a8 in session[_0x12c1a4(0x404)])&&(session[_0x12c1a4(0x404)][_0xc200a8]={});const _0x349654=session[_0x12c1a4(0x404)][_0xc200a8];_0x349654[_0x12c1a4(0x2a8)]=_0x4a09b7;const _0x586c2f=_0x31229a[_0x12c1a4(0x241)]||'',_0x2521c5=_0x31229a[_0x12c1a4(0xac0)]||'',_0x51770a=whepSettingsMarker(_0x31229a),_0x3dc98b=whepSettingsHasStarted(_0x31229a),_0x266395=_0x349654['lastWhepUrl']||'',_0x46dfb4=_0x349654[_0x12c1a4(0x594)]||'',_0x2610f4=_0x349654[_0x12c1a4(0x970)]||null,_0x3a98f4=_0x266395!==_0x586c2f||_0x46dfb4!==_0x2521c5||_0x2610f4!==_0x51770a;if(_0x3a98f4&&_0x349654[_0x12c1a4(0xc0a)]){try{_0x349654[_0x12c1a4(0x97e)]&&_0x349654[_0x12c1a4(0x97e)][_0x12c1a4(0x3ef)]&&_0x349654[_0x12c1a4(0x97e)][_0x12c1a4(0x3ef)]();}catch(_0x53e262){warnlog(_0x53e262);}_0x349654['whep']=null,_0x349654[_0x12c1a4(0xc0a)]=![],_0x349654[_0x12c1a4(0xb35)]=![],_0x349654[_0x12c1a4(0x459)]=null;}_0x349654[_0x12c1a4(0x790)]=_0x31229a,_0x349654['lastWhepUrl']=_0x586c2f,_0x349654[_0x12c1a4(0x594)]=_0x2521c5,_0x349654[_0x12c1a4(0x970)]=_0x51770a,_0x349654[_0x12c1a4(0xc20)]=_0x51770a;_0x3dc98b?_0x349654[_0x12c1a4(0x299)]=!![]:delete _0x349654[_0x12c1a4(0x299)];_0x349654['suppressReconnect']=![],_0x349654['stats']=_0x349654['stats']||{};typeof _0x349654[_0x12c1a4(0xc15)]===_0x12c1a4(0x2cf)&&(_0x349654[_0x12c1a4(0xc15)]=!!(session[_0x12c1a4(0x404)][_0x4a09b7]&&session['rpcs'][_0x4a09b7]['screenShareState']));if(session[_0x12c1a4(0x404)][_0x4a09b7]&&session[_0x12c1a4(0x404)][_0x4a09b7][_0x12c1a4(0xa96)]){const _0x5e845a=session[_0x12c1a4(0x404)][_0x4a09b7][_0x12c1a4(0xa96)],_0xde9675=_0x5e845a+':s';_0x349654['streamID']!==_0xde9675&&(_0x349654[_0x12c1a4(0xa96)]=_0xde9675),_0x349654[_0x12c1a4(0x7eb)]&&(_0x349654[_0x12c1a4(0x7eb)][_0x12c1a4(0xa18)][_0x12c1a4(0x2e8)]=_0xde9675),session[_0x12c1a4(0x404)][_0x4a09b7][_0x12c1a4(0xa3b)]&&session[_0x12c1a4(0x404)][_0x4a09b7][_0x12c1a4(0xa3b)]!==_0x349654[_0x12c1a4(0x7eb)]&&(session[_0x12c1a4(0x404)][_0x4a09b7][_0x12c1a4(0xa3b)][_0x12c1a4(0xa18)]['sid']=_0xde9675);}_0x3dc98b&&session['rpcs'][_0x4a09b7]&&(session[_0x12c1a4(0x404)][_0x4a09b7]['screenShareState']=!![]),_0x3dc98b&&(_0x349654[_0x12c1a4(0xc15)]=!![]);}catch(_0x3ced8a){}maybeStartScreenWhep(_0x4a09b7);}else _0x56c06c['token']?whepIn(_0x56c06c[_0x12c1a4(0x241)],_0x56c06c['token'],_0x4a09b7):whepIn(_0x56c06c['url'],![],_0x4a09b7);}}}}function maybeStartScreenWhep(_0xd9b082){var _0x338250=_0x165386;try{if(!session||!session[_0x338250(0x404)])return;const _0x1dac11=_0xd9b082+_0x338250(0x6f3),_0x3f266c=session[_0x338250(0x404)][_0x1dac11];if(!_0x3f266c||!_0x3f266c[_0x338250(0x790)])return;const _0x196cbc=session[_0x338250(0x404)][_0xd9b082]||null,_0x189559=_0x3f266c[_0x338250(0x790)],{url:_0x13df43,token:_0x399fe1}=_0x189559;if(!_0x13df43)return;const _0x3c6932=_0x3f266c[_0x338250(0xc20)]||whepSettingsMarker(_0x189559),_0x1349d4=_0x3f266c['activeWhepMarker']||null,_0x33f288=whepSettingsHasStarted(_0x189559)||!!_0x3f266c[_0x338250(0x299)];_0x33f288&&(_0x196cbc&&(typeof _0x196cbc['__whepPrevSmallScreen']===_0x338250(0x2cf)&&(_0x196cbc['__whepPrevSmallScreen']=_0x196cbc['smallScreen']),!_0x196cbc['smallScreen']?(_0x196cbc['smallScreen']=!![],_0x196cbc[_0x338250(0x422)]=!![]):_0x196cbc[_0x338250(0x422)]=![]),_0x196cbc&&(_0x196cbc[_0x338250(0xc15)]=!![]),_0x3f266c[_0x338250(0xc15)]=!![],_0x196cbc&&_0x196cbc[_0x338250(0x422)]&&(_0x3f266c['smallScreen']=![]));if(!_0x33f288&&(!_0x196cbc||!_0x196cbc[_0x338250(0xc15)]))return;if(_0x3f266c['whepRequested']){if(_0x3c6932&&_0x1349d4&&_0x3c6932!==_0x1349d4){try{_0x3f266c[_0x338250(0x97e)]&&_0x3f266c['whep'][_0x338250(0x3ef)]&&_0x3f266c[_0x338250(0x97e)][_0x338250(0x3ef)]();}catch(_0x4de032){warnlog(_0x4de032);}_0x3f266c[_0x338250(0x97e)]=null,_0x3f266c[_0x338250(0xc0a)]=![],_0x3f266c['reconnecting']=![],_0x3f266c[_0x338250(0x459)]=null;}else return;}if(_0x3f266c['whepRequested'])return;_0x3f266c['whepRequested']=!![],_0x3f266c[_0x338250(0x844)]=![],_0x3f266c[_0x338250(0xc15)]=!![],_0x3f266c['activeWhepMarker']=_0x3c6932||(_0x33f288?_0x338250(0x60f):null),_0x399fe1?whepIn(_0x13df43,_0x399fe1,_0x1dac11):whepIn(_0x13df43,![],_0x1dac11);}catch(_0x1a2c9f){errorlog(_0x1a2c9f);}}function stopScreenWhep(_0x560409){var _0x372074=_0x165386;try{const _0x4bef54=_0x560409+_0x372074(0x6f3),_0x5bc3e4=session[_0x372074(0x404)]&&session[_0x372074(0x404)][_0x4bef54]?session['rpcs'][_0x4bef54]:null;if(!_0x5bc3e4)return;_0x5bc3e4['pendingWhepSettings']=null,_0x5bc3e4[_0x372074(0xc0a)]=![],_0x5bc3e4[_0x372074(0x844)]=!![],_0x5bc3e4[_0x372074(0xb35)]=![],_0x5bc3e4['screenShareState']=![],_0x5bc3e4[_0x372074(0x58e)]=null,_0x5bc3e4[_0x372074(0x594)]=null,_0x5bc3e4['lastWhepMarker']=null,_0x5bc3e4['pendingWhepMarker']=null,_0x5bc3e4[_0x372074(0x459)]=null,delete _0x5bc3e4[_0x372074(0x299)];try{session['rpcs'][_0x560409]&&(session[_0x372074(0x404)][_0x560409][_0x372074(0xc15)]=![]);}catch(_0x23cb27){errorlog(_0x23cb27);}try{session[_0x372074(0x404)][_0x560409]&&typeof session['rpcs'][_0x560409]['__whepPrevSmallScreen']!=='undefined'&&(session[_0x372074(0x404)][_0x560409][_0x372074(0xa88)]=session[_0x372074(0x404)][_0x560409][_0x372074(0x6a3)],delete session[_0x372074(0x404)][_0x560409][_0x372074(0x6a3)],delete session[_0x372074(0x404)][_0x560409][_0x372074(0x422)]);}catch(_0x39eef4){errorlog(_0x39eef4);}try{_0x5bc3e4[_0x372074(0x97e)]&&_0x5bc3e4[_0x372074(0x97e)][_0x372074(0x3ef)]&&_0x5bc3e4['whep'][_0x372074(0x3ef)]();}catch(_0x267f82){warnlog(_0x267f82);}_0x5bc3e4[_0x372074(0x97e)]=null;}catch(_0x3e6c13){errorlog(_0x3e6c13);}}async function meshcastWatch(_0x1ce820,_0x4441e1){var _0x27a49b=_0x165386;console[_0x27a49b(0x440)](_0x27a49b(0x331));!(_0x1ce820 in session[_0x27a49b(0x404)])&&(session[_0x27a49b(0x404)][_0x1ce820]={},session[_0x27a49b(0x404)][_0x1ce820]['stats']={},session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x1db)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x9e1)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0xc53)]={},session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x4a7)]=![],session[_0x27a49b(0x404)][_0x1ce820]['channelWidth']=![],session[_0x27a49b(0x404)][_0x1ce820]['settings']=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x971)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x23c)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x4d6)]=null,session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0xb30)]=null,session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0xb96)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x458)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0xcd7)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x657)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x9b6)]=![],session[_0x27a49b(0x404)][_0x1ce820]['getStatsTimeout']=null,session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0xa88)]=![],session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x8d5)]=![],errorlog(_0x27a49b(0x392)));var _0x5412dc=!![],_0x10b5e0=!![];if(session[_0x27a49b(0x4fe)]!==![]&&!session[_0x27a49b(0x4fe)][_0x27a49b(0xa4f)](session['rpcs'][_0x1ce820]['streamID']))_0x5412dc=![];else session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x4f2)]&&!session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x4f2)][_0x27a49b(0xa55)]&&(_0x5412dc=![]);if(session[_0x27a49b(0x5aa)]!==![]&&!session['noaudio']['includes'](session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0xa96)]))_0x10b5e0=![];else{if(session[_0x27a49b(0xaa4)]&&session['excludeaudio'][_0x27a49b(0xa4f)](session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0xa96)]))_0x10b5e0=![];else session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x4f2)]&&!session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x4f2)][_0x27a49b(0x544)]&&(_0x10b5e0=![]);}if(!_0x10b5e0&&!_0x5412dc){errorlog(_0x27a49b(0x73a));return;}disableQualityDirector(_0x1ce820);!session[_0x27a49b(0x7d3)]&&await chooseBestTURN();var _0xaf9c3c={...session['configuration']};_0xaf9c3c['bundlePolicy']&&delete _0xaf9c3c['bundlePolicy'];_0xaf9c3c['encodedInsertableStreams']&&delete _0xaf9c3c[_0x27a49b(0x288)];session['encodedInsertableStreams']&&console[_0x27a49b(0xca1)](_0x27a49b(0x60c));try{session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x97e)]=new RTCPeerConnection(_0xaf9c3c);try{session['rpcs'][_0x1ce820][_0x27a49b(0x97e)][_0x27a49b(0xb14)]=_0x33c8b0=>{warnlog(_0x33c8b0);};}catch(_0x57d148){warnlog(_0x57d148);}}catch(_0x1ce8c1){!session['cleanOutput']&&warnUser(_0x27a49b(0xc40));}session[_0x27a49b(0x404)][_0x1ce820][_0x27a49b(0x97e)][_0x27a49b(0x45e)]=function(_0x2d9485){var _0x49b863=_0x27a49b;session['onTrack'](_0x2d9485,_0x1ce820);let _0x53b81f=null;if(_0x2d9485['streams']&&_0x2d9485[_0x49b863(0x524)][0x0])try{let _0x1780a0=_0x2d9485[_0x49b863(0x524)][0x0];_0x53b81f=_0x1780a0[_0x49b863(0x5c7)]()[0x0];}catch(_0x25e5d9){}else _0x2d9485[_0x49b863(0x50d)]&&_0x2d9485[_0x49b863(0x50d)][_0x49b863(0xcd1)]&&_0x2d9485['track'][_0x49b863(0xcd1)]==_0x49b863(0xa55)&&(_0x53b81f=_0x2d9485[_0x49b863(0x50d)]);_0x53b81f&&(log(_0x53b81f),setTimeout(function(_0xb998c,_0x2c66bf){var _0x417b83=_0x49b863;if(session[_0x417b83(0x404)][_0x2c66bf]&&_0xb998c&&_0xb998c['id']){if(session[_0x417b83(0x404)][_0x2c66bf][_0x417b83(0x287)]&&session[_0x417b83(0x404)][_0x2c66bf][_0x417b83(0x287)][_0xb998c['id']]&&'keyFramesRequested_pli'in session['rpcs'][_0x2c66bf]['stats'][_0xb998c['id']]){}}},0x17d4,_0x53b81f,_0x1ce820));};var _0x105d47=session[_0x27a49b(0x54d)](0xe),_0x1a4a5b={};_0x1a4a5b[_0x27a49b(0xa96)]=_0x4441e1['token'],_0x1a4a5b['UUID']=_0x105d47;function _0x2c750d(_0x591bf3){var _0x35a5c2=_0x27a49b,_0x27580f=new XMLHttpRequest();_0x27580f[_0x35a5c2(0x1df)]=function(){var _0x112b16=_0x35a5c2;if(this[_0x112b16(0x70b)]==0x4&&(this[_0x112b16(0x650)]==0xc8||this[_0x112b16(0x650)]==0xc9)){var _0x59dcd8=this['getResponseHeader'](_0x112b16(0xc0b));if(_0x59dcd8==_0x112b16(0x433)){var _0x2263a5={};_0x2263a5[_0x112b16(0x728)]=this[_0x112b16(0x40d)],_0x2263a5['type']='offer',session[_0x112b16(0x38e)]&&(_0x2263a5[_0x112b16(0x728)]=filterSDPLAN(_0x2263a5[_0x112b16(0x728)])),session[_0x112b16(0x3d4)]&&(_0x2263a5[_0x112b16(0x728)]=filterStunOnly(_0x2263a5['sdp'])),session[_0x112b16(0x404)][_0x1ce820]['whep'][_0x112b16(0xa67)](_0x2263a5)[_0x112b16(0x998)](function(){_0x5527c4();})[_0x112b16(0xacc)](function(_0x5b8caa){log(_0x5b8caa);});}}else log(this);},_0x27580f[_0x35a5c2(0xa48)]('POST',_0x4441e1[_0x35a5c2(0x241)],!![]),_0x27580f[_0x35a5c2(0xb29)]('Content-Type',_0x35a5c2(0x9a4)),_0x27580f[_0x35a5c2(0xb29)](_0x35a5c2(0x521),_0x35a5c2(0x23e)+_0x105d47),_0x27580f['send'](JSON['stringify'](_0x591bf3));}function _0x5527c4(){var _0x2fe7ff=_0x27a49b;session[_0x2fe7ff(0x404)][_0x1ce820][_0x2fe7ff(0x97e)]['createAnswer']()['then'](function(_0x17beff){var _0x4654aa=_0x2fe7ff;return _0x17beff[_0x4654aa(0x728)]=CodecsHandler[_0x4654aa(0x276)](_0x17beff[_0x4654aa(0x728)],{'stereo':0x1}),session['rpcs'][_0x1ce820][_0x4654aa(0x97e)][_0x4654aa(0x93c)](_0x17beff);})[_0x2fe7ff(0x998)](function(){var _0x5c2972=_0x2fe7ff,_0x992c5={};_0x992c5[_0x5c2972(0xab9)]=_0x105d47;var _0x780f04=filterDescriptionIpv6(session['rpcs'][_0x1ce820]['whep'][_0x5c2972(0xb04)]);_0x992c5['answer']=_0x780f04[_0x5c2972(0x728)],_0x2c750d(_0x992c5);})[_0x2fe7ff(0xacc)](function(_0x104a01){});}_0x2c750d(_0x1a4a5b);}(function(){'use strict';var _0x4306f1=_0x165386;let _0x4a2088=function(_0x211205){var _0x1c8dff=_0x2b9f;this[_0x1c8dff(0x67b)]=new Uint8Array(_0x211205),this[_0x1c8dff(0x3d7)]=0x0;};_0x4a2088[_0x4306f1(0x479)]['seek']=function(_0x4f5e07){var _0x3409e1=_0x4306f1;this[_0x3409e1(0x3d7)]=_0x4f5e07;},_0x4a2088[_0x4306f1(0x479)]['writeBytes']=function(_0x5d1a38){var _0xcac528=_0x4306f1;for(let _0x13aa9d=0x0;_0x13aa9d<_0x5d1a38[_0xcac528(0xade)];_0x13aa9d++){this[_0xcac528(0x67b)][this[_0xcac528(0x3d7)]++]=_0x5d1a38[_0x13aa9d];}},_0x4a2088[_0x4306f1(0x479)][_0x4306f1(0x243)]=function(_0x235269){this['data'][this['pos']++]=_0x235269;},_0x4a2088['prototype']['writeU8']=_0x4a2088['prototype']['writeByte'],_0x4a2088[_0x4306f1(0x479)][_0x4306f1(0x911)]=function(_0x44221c){var _0x40938b=_0x4306f1;this[_0x40938b(0x67b)][this[_0x40938b(0x3d7)]++]=_0x44221c>>0x8,this[_0x40938b(0x67b)][this[_0x40938b(0x3d7)]++]=_0x44221c;},_0x4a2088[_0x4306f1(0x479)][_0x4306f1(0x312)]=function(_0x4ab205){var _0x1b6d4a=_0x4306f1;let _0x5ac1fe=new Uint8Array(new Float64Array([_0x4ab205])[_0x1b6d4a(0x657)]);for(let _0x42c54e=_0x5ac1fe[_0x1b6d4a(0xade)]-0x1;_0x42c54e>=0x0;_0x42c54e--){this['writeByte'](_0x5ac1fe[_0x42c54e]);}},_0x4a2088['prototype']['writeFloatBE']=function(_0x169db0){var _0x1065bd=_0x4306f1;let _0x43823e=new Uint8Array(new Float32Array([_0x169db0])['buffer']);for(let _0x4f5a50=_0x43823e['length']-0x1;_0x4f5a50>=0x0;_0x4f5a50--){this[_0x1065bd(0x243)](_0x43823e[_0x4f5a50]);}},_0x4a2088[_0x4306f1(0x479)]['writeString']=function(_0x3dbe80){var _0x4effac=_0x4306f1;for(let _0x5df0d3=0x0;_0x5df0d3<_0x3dbe80[_0x4effac(0xade)];_0x5df0d3++){this['data'][this[_0x4effac(0x3d7)]++]=_0x3dbe80[_0x4effac(0x712)](_0x5df0d3);}},_0x4a2088[_0x4306f1(0x479)]['writeEBMLVarIntWidth']=function(_0x5f309d,_0x27c745){var _0xfad6f5=_0x4306f1;switch(_0x27c745){case 0x1:this[_0xfad6f5(0x7cd)](0x1<<0x7|_0x5f309d);break;case 0x2:this[_0xfad6f5(0x7cd)](0x1<<0x6|_0x5f309d>>0x8),this[_0xfad6f5(0x7cd)](_0x5f309d);break;case 0x3:this[_0xfad6f5(0x7cd)](0x1<<0x5|_0x5f309d>>0x10),this['writeU8'](_0x5f309d>>0x8),this[_0xfad6f5(0x7cd)](_0x5f309d);break;case 0x4:this[_0xfad6f5(0x7cd)](0x1<<0x4|_0x5f309d>>0x18),this['writeU8'](_0x5f309d>>0x10),this[_0xfad6f5(0x7cd)](_0x5f309d>>0x8),this['writeU8'](_0x5f309d);break;case 0x5:this[_0xfad6f5(0x7cd)](0x1<<0x3|_0x5f309d/0x100000000&0x7),this['writeU8'](_0x5f309d>>0x18),this[_0xfad6f5(0x7cd)](_0x5f309d>>0x10),this[_0xfad6f5(0x7cd)](_0x5f309d>>0x8),this['writeU8'](_0x5f309d);break;default:throw new Error('Bad\x20EBML\x20VINT\x20size\x20'+_0x27c745);}},_0x4a2088['prototype'][_0x4306f1(0xb11)]=function(_0x5bc19c){var _0x4dcdc4=_0x4306f1;if(_0x5bc19c<(0x1<<0x7)-0x1)return 0x1;else{if(_0x5bc19c<(0x1<<0xe)-0x1)return 0x2;else{if(_0x5bc19c<(0x1<<0x15)-0x1)return 0x3;else{if(_0x5bc19c<(0x1<<0x1c)-0x1)return 0x4;else{if(_0x5bc19c<0x7ffffffff)return 0x5;else throw new Error(_0x4dcdc4(0x5f0)+_0x5bc19c);}}}}},_0x4a2088[_0x4306f1(0x479)][_0x4306f1(0xb8a)]=function(_0x3a1af7){var _0x2468ff=_0x4306f1;this['writeEBMLVarIntWidth'](_0x3a1af7,this[_0x2468ff(0xb11)](_0x3a1af7));},_0x4a2088[_0x4306f1(0x479)]['writeUnsignedIntBE']=function(_0x2dc3e2,_0xe68a8d){var _0x16f23a=_0x4306f1;_0xe68a8d===undefined&&(_0xe68a8d=this[_0x16f23a(0x4fb)](_0x2dc3e2));switch(_0xe68a8d){case 0x5:this[_0x16f23a(0x7cd)](Math[_0x16f23a(0x502)](_0x2dc3e2/0x100000000));case 0x4:this[_0x16f23a(0x7cd)](_0x2dc3e2>>0x18);case 0x3:this[_0x16f23a(0x7cd)](_0x2dc3e2>>0x10);case 0x2:this[_0x16f23a(0x7cd)](_0x2dc3e2>>0x8);case 0x1:this[_0x16f23a(0x7cd)](_0x2dc3e2);break;default:throw new Error(_0x16f23a(0x339)+_0xe68a8d);}},_0x4a2088[_0x4306f1(0x479)][_0x4306f1(0x4fb)]=function(_0x49aeb8){if(_0x49aeb8<0x1<<0x8)return 0x1;else{if(_0x49aeb8<0x1<<0x10)return 0x2;else{if(_0x49aeb8<0x1<<0x18)return 0x3;else return _0x49aeb8<0x100000000?0x4:0x5;}}},_0x4a2088[_0x4306f1(0x479)][_0x4306f1(0x4c2)]=function(){var _0x334e50=_0x4306f1;if(this[_0x334e50(0x3d7)]this['length'])throw new Error('Seeking\x20beyond\x20the\x20end\x20of\x20file\x20is\x20not\x20allowed');this['pos']=_0x2a05e7;},this[_0x2fad50(0x76b)]=function(_0x1e452f){var _0x192c36=_0x2fad50;let _0x554f52={'offset':this[_0x192c36(0x3d7)],'data':_0x1e452f,'length':_0x57e4ff(_0x1e452f)},_0x3dbe07=_0x554f52['offset']>=this['length'];this[_0x192c36(0x3d7)]+=_0x554f52[_0x192c36(0xade)],this[_0x192c36(0xade)]=Math[_0x192c36(0x642)](this['length'],this['pos']),_0x47b218=_0x47b218[_0x192c36(0x998)](async function(){var _0x24ed03=_0x192c36;if(_0x4d9001)return new Promise(function(_0x3cc64e,_0x4378c9){var _0x426a58=_0x2b9f;_0x517de1(_0x554f52['data'])[_0x426a58(0x998)](function(_0x212d0b){var _0x23a428=_0x426a58;let _0x1c9a44=0x0,_0x4a3a28=Buffer[_0x23a428(0x208)](_0x212d0b[_0x23a428(0x657)]),_0x24de0e=function(_0xe2d13,_0x578a71,_0x218784){var _0x5d4f6d=_0x23a428;_0x1c9a44+=_0x578a71,_0x1c9a44>=_0x218784[_0x5d4f6d(0xade)]?_0x3cc64e():_0x2bddce[_0x5d4f6d(0x76b)](_0x4d9001,_0x218784,_0x1c9a44,_0x218784[_0x5d4f6d(0xade)]-_0x1c9a44,_0x554f52[_0x5d4f6d(0x58a)]+_0x1c9a44,_0x24de0e);};_0x2bddce['write'](_0x4d9001,_0x4a3a28,0x0,_0x4a3a28[_0x23a428(0xade)],_0x554f52['offset'],_0x24de0e);});});else{if(_0x101b05)return new Promise(function(_0x431c40,_0x547668){var _0xd8017a=_0x2b9f;_0x101b05[_0xd8017a(0x75d)](_0x554f52[_0xd8017a(0x58a)])[_0xd8017a(0x998)](()=>{var _0x1483a5=_0xd8017a;_0x101b05[_0x1483a5(0x76b)](new Blob([_0x554f52[_0x1483a5(0x67b)]]));})[_0xd8017a(0x998)](()=>{_0x431c40();});});else{if(!_0x3dbe07)for(let _0x28ae1e=0x0;_0x28ae1e<_0x375581[_0x24ed03(0xade)];_0x28ae1e++){let _0x507814=_0x375581[_0x28ae1e];if(!(_0x554f52[_0x24ed03(0x58a)]+_0x554f52[_0x24ed03(0xade)]<=_0x507814[_0x24ed03(0x58a)]||_0x554f52['offset']>=_0x507814[_0x24ed03(0x58a)]+_0x507814[_0x24ed03(0xade)])){if(_0x554f52['offset']<_0x507814[_0x24ed03(0x58a)]||_0x554f52[_0x24ed03(0x58a)]+_0x554f52['length']>_0x507814['offset']+_0x507814[_0x24ed03(0xade)])throw new Error(_0x24ed03(0xb82));if(_0x554f52['offset']==_0x507814[_0x24ed03(0x58a)]&&_0x554f52['length']==_0x507814[_0x24ed03(0xade)]){_0x507814[_0x24ed03(0x67b)]=_0x554f52[_0x24ed03(0x67b)];return;}else return _0x517de1(_0x507814['data'])[_0x24ed03(0x998)](function(_0x109494){var _0x36e9ec=_0x24ed03;return _0x507814['data']=_0x109494,_0x517de1(_0x554f52[_0x36e9ec(0x67b)]);})[_0x24ed03(0x998)](function(_0x3cae20){var _0x39830e=_0x24ed03;_0x554f52[_0x39830e(0x67b)]=_0x3cae20,_0x507814[_0x39830e(0x67b)][_0x39830e(0x43d)](_0x554f52['data'],_0x554f52[_0x39830e(0x58a)]-_0x507814[_0x39830e(0x58a)]);});}}}}_0x375581[_0x24ed03(0x5e7)](_0x554f52);});},this[_0x2fad50(0x5fa)]=function(_0x29a281){var _0x35cff6=_0x2fad50;return _0x4d9001||_0x101b05?_0x47b218=_0x47b218[_0x35cff6(0x998)](function(){return null;}):_0x47b218=_0x47b218['then'](function(){var _0x2d0254=_0x35cff6;let _0x3235f4=[];for(let _0x16c96a=0x0;_0x16c96a<_0x375581[_0x2d0254(0xade)];_0x16c96a++){_0x3235f4['push'](_0x375581[_0x16c96a]['data']);}return new Blob(_0x3235f4,{'type':_0x29a281});}),_0x47b218;};};};window[_0xb7e7ef(0xc9d)]=_0xb46697(null);}()),(function(){'use strict';var _0x59f5f8=_0x165386;function _0x457e30(_0x1b7640){var _0x3fd28c=_0x2b9f;this[_0x3fd28c(0xb58)]=_0x1b7640;}function _0x4dea2f(_0x4092e4,_0x2ed6fe){var _0x407e8f=_0x2b9f;let _0x29012f={};return[_0x4092e4,_0x2ed6fe][_0x407e8f(0x306)](function(_0x4e5e5e){var _0x1ccf5a=_0x407e8f;for(let _0x1279ba in _0x4e5e5e){Object[_0x1ccf5a(0x479)][_0x1ccf5a(0x6ff)]['call'](_0x4e5e5e,_0x1279ba)&&(_0x29012f[_0x1279ba]=_0x4e5e5e[_0x1279ba]);}}),_0x29012f;}function _0x44698c(_0x416557,_0x1004fb,_0x30d2a2){var _0x487155=_0x2b9f;if(Array[_0x487155(0x5b1)](_0x30d2a2))for(let _0x2f1639=0x0;_0x2f1639<_0x30d2a2['length'];_0x2f1639++){_0x44698c(_0x416557,_0x1004fb,_0x30d2a2[_0x2f1639]);}else{if(typeof _0x30d2a2===_0x487155(0x9cb))_0x416557['writeString'](_0x30d2a2);else{if(_0x30d2a2 instanceof Uint8Array)_0x416557[_0x487155(0xba1)](_0x30d2a2);else{if(_0x30d2a2['id']){_0x30d2a2['offset']=_0x416557[_0x487155(0x3d7)]+_0x1004fb,_0x416557[_0x487155(0xac6)](_0x30d2a2['id']);if(Array[_0x487155(0x5b1)](_0x30d2a2[_0x487155(0x67b)])){let _0x12a87d,_0x1ea17b,_0x1d952b;_0x30d2a2['size']===-0x1?_0x416557['writeByte'](0xff):(_0x12a87d=_0x416557[_0x487155(0x3d7)],_0x416557[_0x487155(0xba1)]([0x0,0x0,0x0,0x0])),_0x1ea17b=_0x416557[_0x487155(0x3d7)],_0x30d2a2['dataOffset']=_0x1ea17b+_0x1004fb,_0x44698c(_0x416557,_0x1004fb,_0x30d2a2[_0x487155(0x67b)]),_0x30d2a2[_0x487155(0x53c)]!==-0x1&&(_0x1d952b=_0x416557[_0x487155(0x3d7)],_0x30d2a2[_0x487155(0x53c)]=_0x1d952b-_0x1ea17b,_0x416557[_0x487155(0x75d)](_0x12a87d),_0x416557[_0x487155(0xc4d)](_0x30d2a2['size'],0x4),_0x416557[_0x487155(0x75d)](_0x1d952b));}else{if(typeof _0x30d2a2['data']===_0x487155(0x9cb))_0x416557[_0x487155(0xb8a)](_0x30d2a2['data'][_0x487155(0xade)]),_0x30d2a2[_0x487155(0x9d9)]=_0x416557[_0x487155(0x3d7)]+_0x1004fb,_0x416557[_0x487155(0x270)](_0x30d2a2['data']);else{if(typeof _0x30d2a2['data']===_0x487155(0xab6))!_0x30d2a2[_0x487155(0x53c)]&&(_0x30d2a2[_0x487155(0x53c)]=_0x416557['measureUnsignedInt'](_0x30d2a2[_0x487155(0x67b)])),_0x416557['writeEBMLVarInt'](_0x30d2a2[_0x487155(0x53c)]),_0x30d2a2['dataOffset']=_0x416557[_0x487155(0x3d7)]+_0x1004fb,_0x416557[_0x487155(0xac6)](_0x30d2a2[_0x487155(0x67b)],_0x30d2a2[_0x487155(0x53c)]);else{if(_0x30d2a2[_0x487155(0x67b)]instanceof _0x457e30)_0x416557['writeEBMLVarInt'](0x8),_0x30d2a2['dataOffset']=_0x416557[_0x487155(0x3d7)]+_0x1004fb,_0x416557['writeDoubleBE'](_0x30d2a2[_0x487155(0x67b)][_0x487155(0xb58)]);else{if(_0x30d2a2[_0x487155(0x67b)]instanceof _0x457e30)_0x416557[_0x487155(0xb8a)](0x4),_0x30d2a2[_0x487155(0x9d9)]=_0x416557[_0x487155(0x3d7)]+_0x1004fb,_0x416557[_0x487155(0x86c)](_0x30d2a2[_0x487155(0x67b)][_0x487155(0xb58)]);else{if(_0x30d2a2[_0x487155(0x67b)]instanceof Uint8Array)_0x416557[_0x487155(0xb8a)](_0x30d2a2['data'][_0x487155(0xc04)]),_0x30d2a2[_0x487155(0x9d9)]=_0x416557[_0x487155(0x3d7)]+_0x1004fb,_0x416557[_0x487155(0xba1)](_0x30d2a2['data']);else throw new Error(_0x487155(0xc46)+typeof _0x30d2a2['data']);}}}}}}else throw new Error(_0x487155(0xc46)+typeof _0x30d2a2[_0x487155(0x67b)]);}}}}let _0x210dd7=function(_0x3f9fee,_0x2ad3a2){return function(_0x59c6b0){var _0x589419=_0x2b9f;let _0x28c83e=0x1388,_0x414e26=![],_0x52fe69=0x0,_0x1c59dc=0x0,_0x556410=!![],_0x4b4dc9=0x0,_0x1f5e3b=0xbb80,_0x2f7697=0x1,_0x578492=[],_0x466592=0x0,_0x36fc14=0x0,_0x2437fd=0x0,_0x430187={'fileWriter':null,'codec':_0x59c6b0[_0x589419(0x44b)]||_0x589419(0x546)},_0x5079f2,_0x5d10b0={'id':0x4489,'data':new _0x457e30(0x0)},_0x653e78=new _0x2ad3a2(_0x59c6b0[_0x589419(0x787)]);function _0x91ca89(_0x30e95b,_0x3545bd){var _0x10c978=_0x589419;return _0x3545bd=new Uint8Array(_0x3545bd),_0x1af5ea(_0x46d93a(_0x30e95b),_0x349705(_0x3545bd[_0x10c978(0xc04)]),_0x3545bd);}function _0x1af5ea(){var _0x10c500=_0x589419,_0x25ebb9,_0x38d46b=0x0,_0x2f70a9;for(_0x25ebb9=0x0;_0x25ebb9>>0x18&0xff,_0x32dba9>>>0x10&0xff,_0x32dba9>>>0x8&0xff,_0x32dba9&0xff]);if((_0x32dba9&0xff0000)!=0x0)return new Uint8Array([_0x32dba9>>>0x10&0xff,_0x32dba9>>>0x8&0xff,_0x32dba9&0xff]);if((_0x32dba9&0xff00)!=0x0)return new Uint8Array([_0x32dba9>>>0x8&0xff,_0x32dba9&0xff]);if((_0x32dba9&0xff)!=0x0)return new Uint8Array([_0x32dba9&0xff]);throw'InvalidOperationException';}function _0x349705(_0x606bdf){if(_0x606bdf<=0x7f)return new Uint8Array([0x80|_0x606bdf&0x7f]);if(_0x606bdf<=0x3fff)return new Uint8Array([0x40|_0x606bdf>>0x8&0x3f,_0x606bdf&0xff]);return new Uint8Array([0x8,_0x606bdf>>>0x18&0xff,_0x606bdf>>>0x10&0xff,_0x606bdf>>>0x8&0xff,_0x606bdf&0xff]);}function _0x4478ae(_0x1f7262,_0x10c77d){var _0x4d5dcf=_0x589419,_0x3d08f0=new DataView(new ArrayBuffer(0x4));return _0x3d08f0[_0x4d5dcf(0x39c)](0x0,_0x10c77d,![]),_0x91ca89(_0x1f7262,new Uint8Array(_0x3d08f0[_0x4d5dcf(0x657)]));}function _0x2cc066(_0x4c34a5){var _0x59249c=_0x589419;if(_0x4c34a5<=0xff)return new Uint8Array([_0x4c34a5&0xff]);if(_0x4c34a5<=0xffff)return new Uint8Array([_0x4c34a5>>>0x8&0xff,_0x4c34a5&0xff]);if(_0x4c34a5<=0xffffff)return new Uint8Array([_0x4c34a5>>0x10&0xff,_0x4c34a5>>0x8&0xff,_0x4c34a5&0xff]);return new Uint8Array([_0x4c34a5>>>0x18&0xff,_0x4c34a5>>>0x10&0xff,_0x4c34a5>>>0x8&0xff,_0x4c34a5&0xff]);var _0x3b7bb0=new DataView(new ArrayBuffer(0x4));return _0x3b7bb0[_0x59249c(0x3e2)](0x0,_0x4c34a5,![]),_0x3b7bb0;}function _0x39cb08(_0x42f745,_0x1f3fe1){return _0x91ca89(_0x42f745,_0x2cc066(_0x1f3fe1));}function _0xbc360f(_0x556732,_0x3b33be){var _0x2a062c=_0x589419;return _0x91ca89(_0x556732,new TextEncoder()[_0x2a062c(0x736)](_0x3b33be));}function _0xd86a40(){var _0x458be5=_0x589419;let _0x49d1b6={'id':0x1a45dfa3,'data':[_0x39cb08(0x4286,0x1),_0x39cb08(0x42f7,0x1),_0x39cb08(0x42f2,0x4),_0x39cb08(0x42f3,0x8),_0xbc360f(0x4282,'webm'),_0x39cb08(0x4287,0x4),_0x39cb08(0x4285,0x2)]},_0x3a3054={'id':0x1549a966,'data':[_0x39cb08(0x2ad7b1,0xf4240),_0xbc360f(0x4d80,_0x458be5(0x5a6)),_0xbc360f(0x5741,_0x458be5(0x5a6)),_0x5d10b0]},_0x4bfbd0=[{'id':0xb0,'data':_0x52fe69},{'id':0xba,'data':_0x1c59dc}],_0x242afd={'id':0x1654ae6b,'data':[{'id':0xae,'data':[_0x39cb08(0xd7,0x1),_0x39cb08(0x73c5,0x1),_0x39cb08(0x9c,0x0),_0xbc360f(0x22b59c,'und'),_0xbc360f(0x86,'V_'+_0x59c6b0[_0x458be5(0x44b)]),_0x39cb08(0x83,0x1),{'id':0xe0,'data':[_0x39cb08(0xb0,_0x52fe69),_0x39cb08(0xba,_0x1c59dc)]}]},{'id':0xae,'data':[_0x39cb08(0xd7,0x2),_0x39cb08(0x73c5,0x2),_0x39cb08(0x9c,0x0),_0xbc360f(0x22b59c,_0x458be5(0x7c3)),_0xbc360f(0x86,_0x458be5(0xa47)),_0x39cb08(0x83,0x2),{'id':0xe1,'data':[_0x4478ae(0xb5,_0x1f5e3b),_0x39cb08(0x9f,_0x2f7697)]},_0x91ca89(0x63a2,new Uint8Array(['O'[_0x458be5(0x712)](0x0),'p'[_0x458be5(0x712)](0x0),'u'[_0x458be5(0x712)](0x0),'s'[_0x458be5(0x712)](0x0),'H'[_0x458be5(0x712)](0x0),'e'['charCodeAt'](0x0),'a'['charCodeAt'](0x0),'d'[_0x458be5(0x712)](0x0),0x1,_0x2f7697&0xff,0x38,0x1,_0x1f5e3b>>>0x0&0xff,_0x1f5e3b>>>0x8&0xff,_0x1f5e3b>>>0x10&0xff,_0x1f5e3b>>>0x18&0xff,0x0,0x0,0x0]))]}]};_0x5079f2={'id':0x18538067,'size':-0x1,'data':[_0x3a3054,_0x242afd]};let _0x52cc6f=new _0x3f9fee(0x200);_0x44698c(_0x52cc6f,_0x653e78[_0x458be5(0x3d7)],[_0x49d1b6,_0x5079f2]),_0x653e78[_0x458be5(0x76b)](_0x52cc6f[_0x458be5(0x4c2)]()),_0x414e26=!![];}function _0x6b871a(_0x16fa4f){var _0x443850=_0x589419;let _0x1e4cfd=new _0x3f9fee(0x1+0x2+0x1);if(!(_0x16fa4f['trackNumber']>0x0&&_0x16fa4f[_0x443850(0xa4d)]<0x7f))throw new Error(_0x443850(0x579));return _0x1e4cfd['writeEBMLVarInt'](_0x16fa4f[_0x443850(0xa4d)]),_0x1e4cfd['writeU16BE'](_0x16fa4f[_0x443850(0xa1f)]),_0x1e4cfd[_0x443850(0x243)]((_0x16fa4f[_0x443850(0x3c5)]==_0x443850(0xa27)?0x1:0x0)<<0x7),{'id':0xa3,'data':[_0x1e4cfd[_0x443850(0x4c2)](),_0x16fa4f['frame']]};}function _0x58d23c(_0x385226){var _0x8139f1=_0x589419;return{'id':0x1f43b675,'data':[{'id':0xe7,'data':Math[_0x8139f1(0xc45)](_0x385226['timecode'])}]};}function _0xc5441e(){var _0x5c07e0=_0x589419;if(_0x578492['length']===0x0)return;let _0x43adf6=0x0;for(let _0x4d621a=0x0;_0x4d621a<_0x578492[_0x5c07e0(0xade)];_0x4d621a++){_0x43adf6+=_0x578492[_0x4d621a]['frame'][_0x5c07e0(0xc04)];}let _0x41b725=new _0x3f9fee(_0x43adf6+_0x578492['length']*0x40),_0x5b696b=_0x58d23c({'timecode':Math[_0x5c07e0(0xc45)](_0x466592)});for(let _0x502bce=0x0;_0x502bce<_0x578492['length'];_0x502bce++){_0x5b696b[_0x5c07e0(0x67b)][_0x5c07e0(0x5e7)](_0x6b871a(_0x578492[_0x502bce]));}_0x44698c(_0x41b725,_0x653e78[_0x5c07e0(0x3d7)],_0x5b696b),_0x653e78[_0x5c07e0(0x76b)](_0x41b725[_0x5c07e0(0x4c2)]()),_0x578492=[],_0x36fc14=0x0;}function _0x174e0f(_0x38240d,_0x138d32){var _0x507076=_0x589419;_0x38240d[_0x507076(0xa4d)]=_0x138d32;var _0x57233f=_0x38240d[_0x507076(0xc9c)]/0x3e8;_0x556410?(_0x4b4dc9=_0x57233f,_0x57233f=0x0,_0x556410=![]):_0x57233f=_0x57233f-_0x4b4dc9;_0x2437fd=_0x57233f;if(_0x36fc14==0x0)_0x466592=_0x57233f;_0x38240d['timecode']=Math[_0x507076(0xc45)](_0x57233f-_0x466592),_0x578492['push'](_0x38240d),_0x36fc14=_0x38240d[_0x507076(0xa1f)]+0x1,_0x36fc14>=_0x28c83e&&_0xc5441e();}function _0x4232fb(){var _0x101a6a=_0x589419;let _0x4f6a7a=new _0x3f9fee(seekHead[_0x101a6a(0x53c)]),_0x161521=_0x653e78['pos'];_0x44698c(_0x4f6a7a,seekHead[_0x101a6a(0x9d9)],seekHead[_0x101a6a(0x67b)]),_0x653e78['seek'](seekHead[_0x101a6a(0x9d9)]),_0x653e78['write'](_0x4f6a7a[_0x101a6a(0x4c2)]()),_0x653e78[_0x101a6a(0x75d)](_0x161521);}function _0x12972d(){var _0x3f5d43=_0x589419;let _0x1e9438=new _0x3f9fee(0x8),_0x27a65c=_0x653e78[_0x3f5d43(0x3d7)];_0x1e9438[_0x3f5d43(0x312)](_0x2437fd),_0x653e78[_0x3f5d43(0x75d)](_0x5d10b0[_0x3f5d43(0x9d9)]),_0x653e78[_0x3f5d43(0x76b)](_0x1e9438['getAsDataArray']()),_0x653e78[_0x3f5d43(0x75d)](_0x27a65c);}this['addFrame']=function(_0x31a158){var _0x37f601=_0x589419;!_0x414e26&&(_0x52fe69=_0x59c6b0[_0x37f601(0x28e)],_0x1c59dc=_0x59c6b0[_0x37f601(0x43f)],_0x1f5e3b=_0x59c6b0[_0x37f601(0x2df)],_0x2f7697=_0x59c6b0[_0x37f601(0x867)],_0xd86a40());if(_0x31a158['constructor']['name']==_0x37f601(0xbc8)){let _0x45f80f=new Uint8Array(_0x31a158[_0x37f601(0xc04)]);_0x31a158[_0x37f601(0x1c1)](_0x45f80f),_0x174e0f({'frame':_0x45f80f,'intime':_0x31a158['timestamp'],'type':_0x31a158['type']},0x1);return;}else{if(_0x31a158[_0x37f601(0x8c5)]['name']==_0x37f601(0x7e2)){let _0x25e3fd=new Uint8Array(_0x31a158[_0x37f601(0xc04)]);_0x31a158['copyTo'](_0x25e3fd),_0x174e0f({'frame':_0x25e3fd,'intime':_0x31a158[_0x37f601(0x9f5)],'type':_0x31a158['type']},0x2);return;}}},this[_0x589419(0x5fa)]=function(){var _0x2796e7=_0x589419;return!_0x414e26&&_0xd86a40(),_0x556410=!![],_0xc5441e(),_0x12972d(),_0x653e78[_0x2796e7(0x5fa)](_0x2796e7(0x67f));},this[_0x589419(0x3e3)]=function(){return _0x653e78['length'];},_0x59c6b0=_0x4dea2f(_0x430187,_0x59c6b0||{});};};window[_0x59f5f8(0xa30)]=_0x210dd7(window[_0x59f5f8(0xaf6)],window[_0x59f5f8(0xc9d)]);}()); \ No newline at end of file